712 lines
25 KiB
C#
712 lines
25 KiB
C#
using MMoneyCoderSharp.Data;
|
|
|
|
namespace MBN_STOCK_WEBVIEW.LegacyApplication;
|
|
|
|
/// <summary>
|
|
/// C# authority for the original UC3 comparison workflow. Search results and fixed targets
|
|
/// are selected by opaque ids, saved bytes are parsed by the strict Core CP949 parser, and
|
|
/// an action id is the only caller-supplied value used to materialize a playlist draft.
|
|
/// </summary>
|
|
public sealed class LegacyComparisonWorkflowController
|
|
{
|
|
private readonly ILegacyComparisonDataService _dataService;
|
|
private readonly ILegacyComparisonPairStore _pairStore;
|
|
private List<SearchResultState> _domesticResults = [];
|
|
private List<SearchResultState> _worldResults = [];
|
|
private List<SavedPairState> _savedPairs = [];
|
|
private LegacyComparisonTarget? _firstTarget;
|
|
private LegacyComparisonTarget? _secondTarget;
|
|
private string? _selectedDomesticResultId;
|
|
private string? _selectedWorldResultId;
|
|
private string? _selectedFixedTargetId;
|
|
private string? _selectedPairId;
|
|
private string _domesticQuery = string.Empty;
|
|
private string _worldQuery = string.Empty;
|
|
private string _domesticError = string.Empty;
|
|
private string _worldError = string.Empty;
|
|
private string _statusMessage = string.Empty;
|
|
private LegacyComparisonSearchStatus _domesticStatus = LegacyComparisonSearchStatus.Idle;
|
|
private LegacyComparisonSearchStatus _worldStatus = LegacyComparisonSearchStatus.Idle;
|
|
private LegacyOperatorStatusKind _statusKind = LegacyOperatorStatusKind.Neutral;
|
|
private long _revision;
|
|
private long _pairSequence;
|
|
private long _draftSequence;
|
|
private int _domesticSearchVersion;
|
|
private int _worldSearchVersion;
|
|
private int _activationGeneration;
|
|
private bool _isBusy;
|
|
|
|
public LegacyComparisonWorkflowController(
|
|
ILegacyComparisonDataService dataService,
|
|
ILegacyComparisonPairStore pairStore)
|
|
{
|
|
_dataService = dataService ?? throw new ArgumentNullException(nameof(dataService));
|
|
_pairStore = pairStore ?? throw new ArgumentNullException(nameof(pairStore));
|
|
}
|
|
|
|
public LegacyComparisonWorkflowSnapshot Current => CreateSnapshot();
|
|
|
|
/// <summary>
|
|
/// Recreates UC3's per-control state before its saved CP949 pairs are loaded for
|
|
/// a new tab activation. Search and pair-load generations invalidate completions
|
|
/// that belong to the discarded control instance.
|
|
/// </summary>
|
|
public LegacyComparisonWorkflowSnapshot ResetForTabActivation()
|
|
{
|
|
Interlocked.Increment(ref _activationGeneration);
|
|
Interlocked.Increment(ref _domesticSearchVersion);
|
|
Interlocked.Increment(ref _worldSearchVersion);
|
|
_domesticResults = [];
|
|
_worldResults = [];
|
|
_savedPairs = [];
|
|
_firstTarget = null;
|
|
_secondTarget = null;
|
|
_selectedDomesticResultId = null;
|
|
_selectedWorldResultId = null;
|
|
_selectedFixedTargetId = null;
|
|
_selectedPairId = null;
|
|
_domesticQuery = string.Empty;
|
|
_worldQuery = string.Empty;
|
|
_domesticError = string.Empty;
|
|
_worldError = string.Empty;
|
|
_statusMessage = string.Empty;
|
|
_domesticStatus = LegacyComparisonSearchStatus.Idle;
|
|
_worldStatus = LegacyComparisonSearchStatus.Idle;
|
|
_statusKind = LegacyOperatorStatusKind.Neutral;
|
|
_isBusy = false;
|
|
Touch();
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
public async Task<LegacyComparisonWorkflowSnapshot> SearchDomesticAsync(
|
|
string query,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var normalizedQuery = NormalizeSearchQuery(query);
|
|
var version = Interlocked.Increment(ref _domesticSearchVersion);
|
|
_domesticQuery = normalizedQuery;
|
|
_domesticStatus = LegacyComparisonSearchStatus.Loading;
|
|
_domesticError = string.Empty;
|
|
_domesticResults = [];
|
|
_selectedDomesticResultId = null;
|
|
Touch();
|
|
|
|
try
|
|
{
|
|
var results = await _dataService.SearchDomesticAsync(
|
|
normalizedQuery,
|
|
LegacyComparisonWorkflowCatalog.DomesticMaximumResults,
|
|
cancellationToken).ConfigureAwait(false);
|
|
if (version != Volatile.Read(ref _domesticSearchVersion))
|
|
{
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
_domesticResults = ValidateSearchResults(
|
|
results,
|
|
LegacyComparisonWorkflowCatalog.DomesticMaximumResults,
|
|
domestic: true,
|
|
version);
|
|
_domesticStatus = LegacyComparisonSearchStatus.Ready;
|
|
_domesticError = string.Empty;
|
|
Touch();
|
|
return CreateSnapshot();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
if (version == Volatile.Read(ref _domesticSearchVersion))
|
|
{
|
|
_domesticStatus = LegacyComparisonSearchStatus.Idle;
|
|
_domesticError = string.Empty;
|
|
Touch();
|
|
}
|
|
|
|
throw;
|
|
}
|
|
catch
|
|
{
|
|
if (version == Volatile.Read(ref _domesticSearchVersion))
|
|
{
|
|
_domesticResults = [];
|
|
_domesticStatus = LegacyComparisonSearchStatus.Error;
|
|
_domesticError = "국내·NXT 종목 검색 결과를 안전하게 확인하지 못했습니다.";
|
|
Touch();
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<LegacyComparisonWorkflowSnapshot> SearchWorldAsync(
|
|
string query,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
// UC3's blank world query loaded the complete US/TW universe, so blank remains valid.
|
|
var normalizedQuery = NormalizeSearchQuery(query);
|
|
var version = Interlocked.Increment(ref _worldSearchVersion);
|
|
_worldQuery = normalizedQuery;
|
|
_worldStatus = LegacyComparisonSearchStatus.Loading;
|
|
_worldError = string.Empty;
|
|
_worldResults = [];
|
|
_selectedWorldResultId = null;
|
|
Touch();
|
|
|
|
try
|
|
{
|
|
var results = await _dataService.SearchWorldAsync(
|
|
normalizedQuery,
|
|
LegacyComparisonWorkflowCatalog.WorldMaximumResults,
|
|
cancellationToken).ConfigureAwait(false);
|
|
if (version != Volatile.Read(ref _worldSearchVersion))
|
|
{
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
_worldResults = ValidateSearchResults(
|
|
results,
|
|
LegacyComparisonWorkflowCatalog.WorldMaximumResults,
|
|
domestic: false,
|
|
version);
|
|
_worldStatus = LegacyComparisonSearchStatus.Ready;
|
|
_worldError = string.Empty;
|
|
Touch();
|
|
return CreateSnapshot();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
if (version == Volatile.Read(ref _worldSearchVersion))
|
|
{
|
|
_worldStatus = LegacyComparisonSearchStatus.Idle;
|
|
_worldError = string.Empty;
|
|
Touch();
|
|
}
|
|
|
|
throw;
|
|
}
|
|
catch
|
|
{
|
|
if (version == Volatile.Read(ref _worldSearchVersion))
|
|
{
|
|
_worldResults = [];
|
|
_worldStatus = LegacyComparisonSearchStatus.Error;
|
|
_worldError = "미국·대만 종목 검색 결과를 안전하게 확인하지 못했습니다.";
|
|
Touch();
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public LegacyComparisonWorkflowSnapshot SelectDomesticResult(string selectionId) =>
|
|
SelectSearchResult(_domesticResults, selectionId, domestic: true);
|
|
|
|
public LegacyComparisonWorkflowSnapshot SelectWorldResult(string selectionId) =>
|
|
SelectSearchResult(_worldResults, selectionId, domestic: false);
|
|
|
|
public LegacyComparisonWorkflowSnapshot SelectFixedTarget(string targetId)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(targetId);
|
|
if (LegacyComparisonWorkflowCatalog.GetFixedTarget(targetId) is not null)
|
|
{
|
|
_selectedFixedTargetId = targetId;
|
|
Touch();
|
|
}
|
|
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
public LegacyComparisonWorkflowSnapshot ChooseDomesticResult(string selectionId) =>
|
|
ChooseSearchResult(_domesticResults, selectionId, domestic: true);
|
|
|
|
public LegacyComparisonWorkflowSnapshot ChooseWorldResult(string selectionId) =>
|
|
ChooseSearchResult(_worldResults, selectionId, domestic: false);
|
|
|
|
public LegacyComparisonWorkflowSnapshot ChooseFixedTarget(string targetId)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(targetId);
|
|
var target = LegacyComparisonWorkflowCatalog.GetFixedTarget(targetId);
|
|
if (target is not null)
|
|
{
|
|
_selectedFixedTargetId = targetId;
|
|
RotateDraft(target);
|
|
Touch();
|
|
}
|
|
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
public LegacyComparisonWorkflowSnapshot SwapDraftTargets()
|
|
{
|
|
(_firstTarget, _secondTarget) = (_secondTarget, _firstTarget);
|
|
Touch();
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
public LegacyComparisonWorkflowSnapshot ClearDraftTargets()
|
|
{
|
|
_firstTarget = null;
|
|
_secondTarget = null;
|
|
Touch();
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
public async Task<LegacyComparisonWorkflowSnapshot> LoadSavedPairsAsync(
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var activationGeneration = Volatile.Read(ref _activationGeneration);
|
|
BeginBusy();
|
|
try
|
|
{
|
|
var source = await _pairStore.ReadCp949Async(cancellationToken).ConfigureAwait(false);
|
|
var candidate = new List<SavedPairState>();
|
|
if (source is { Length: > 0 })
|
|
{
|
|
var bytes = source.ToArray();
|
|
var rows = LegacyComparisonFileParser.Parse(bytes);
|
|
foreach (var row in rows)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var firstReference = new LegacyComparisonStoredTargetReference(
|
|
row.First.Name,
|
|
row.First.Market);
|
|
var secondReference = new LegacyComparisonStoredTargetReference(
|
|
row.Second.Name,
|
|
row.Second.Market);
|
|
var first = await ResolveStoredTargetAsync(
|
|
firstReference,
|
|
cancellationToken).ConfigureAwait(false);
|
|
var second = await ResolveStoredTargetAsync(
|
|
secondReference,
|
|
cancellationToken).ConfigureAwait(false);
|
|
candidate.Add(new SavedPairState(
|
|
$"comparison-pair-{row.RowNumber:D8}",
|
|
first,
|
|
second));
|
|
}
|
|
}
|
|
|
|
if (activationGeneration != Volatile.Read(ref _activationGeneration))
|
|
{
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
_savedPairs = candidate;
|
|
_pairSequence = Math.Max(_pairSequence, candidate.Count);
|
|
_selectedPairId = candidate.FirstOrDefault()?.RowId;
|
|
_statusMessage = candidate.Count == 0
|
|
? "저장된 종목 비교쌍이 없습니다."
|
|
: $"저장된 종목 비교쌍 {candidate.Count}개를 불러왔습니다.";
|
|
_statusKind = LegacyOperatorStatusKind.Information;
|
|
}
|
|
catch
|
|
{
|
|
if (activationGeneration != Volatile.Read(ref _activationGeneration))
|
|
{
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
_statusMessage = "CP949 종목비교.dat를 안전하게 불러오지 못했습니다.";
|
|
_statusKind = LegacyOperatorStatusKind.Error;
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
if (activationGeneration == Volatile.Read(ref _activationGeneration))
|
|
{
|
|
EndBusy();
|
|
}
|
|
}
|
|
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
public async Task<LegacyComparisonWorkflowSnapshot> AddDraftPairAsync(
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (_firstTarget is null || _secondTarget is null)
|
|
{
|
|
throw new InvalidOperationException("Select two comparison targets first.");
|
|
}
|
|
|
|
if (_savedPairs.Count >= LegacyComparisonFileParser.MaximumRows)
|
|
{
|
|
throw new InvalidOperationException("The saved comparison pair limit was reached.");
|
|
}
|
|
|
|
var signature = LegacyComparisonWorkflowCatalog.PairSignature(
|
|
_firstTarget,
|
|
_secondTarget);
|
|
if (_savedPairs.Any(pair => string.Equals(
|
|
LegacyComparisonWorkflowCatalog.PairSignature(pair.First, pair.Second),
|
|
signature,
|
|
StringComparison.Ordinal)))
|
|
{
|
|
throw new InvalidOperationException("The exact comparison pair is already saved.");
|
|
}
|
|
|
|
var rowId = $"comparison-pair-{++_pairSequence:D8}";
|
|
var candidate = new List<SavedPairState>(_savedPairs)
|
|
{
|
|
new(rowId, _firstTarget, _secondTarget)
|
|
};
|
|
return await CommitPairsAsync(
|
|
candidate,
|
|
rowId,
|
|
"종목 비교쌍을 저장했습니다.",
|
|
cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public LegacyComparisonWorkflowSnapshot SelectSavedPair(string rowId)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(rowId);
|
|
if (_savedPairs.Any(pair => string.Equals(pair.RowId, rowId, StringComparison.Ordinal)))
|
|
{
|
|
_selectedPairId = rowId;
|
|
Touch();
|
|
}
|
|
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
public async Task<LegacyComparisonWorkflowSnapshot> MoveSelectedPairAsync(
|
|
LegacyComparisonPairMoveDirection direction,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!Enum.IsDefined(direction))
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(direction));
|
|
}
|
|
|
|
var index = SelectedPairIndex();
|
|
var destination = index + (direction == LegacyComparisonPairMoveDirection.Up ? -1 : 1);
|
|
if (index < 0 || destination < 0 || destination >= _savedPairs.Count)
|
|
{
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
var candidate = new List<SavedPairState>(_savedPairs);
|
|
(candidate[index], candidate[destination]) = (candidate[destination], candidate[index]);
|
|
return await CommitPairsAsync(
|
|
candidate,
|
|
_selectedPairId,
|
|
"종목 비교쌍 순서를 변경했습니다.",
|
|
cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<LegacyComparisonWorkflowSnapshot> DeleteSelectedPairAsync(
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var index = SelectedPairIndex();
|
|
if (index < 0)
|
|
{
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
var candidate = new List<SavedPairState>(_savedPairs);
|
|
candidate.RemoveAt(index);
|
|
var nextSelection = candidate.Count == 0
|
|
? null
|
|
: candidate[Math.Min(index, candidate.Count - 1)].RowId;
|
|
return await CommitPairsAsync(
|
|
candidate,
|
|
nextSelection,
|
|
"선택한 종목 비교쌍을 삭제했습니다.",
|
|
cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public Task<LegacyComparisonWorkflowSnapshot> DeleteAllPairsAsync(
|
|
CancellationToken cancellationToken = default) =>
|
|
_savedPairs.Count == 0
|
|
? Task.FromResult(CreateSnapshot())
|
|
: CommitPairsAsync(
|
|
[],
|
|
null,
|
|
"종목 비교쌍을 모두 삭제했습니다.",
|
|
cancellationToken);
|
|
|
|
public LegacyGenericPlaylistDraft MaterializeSelectedAction(string actionId)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(actionId);
|
|
if (_isBusy)
|
|
{
|
|
throw new InvalidOperationException("The comparison workflow is busy.");
|
|
}
|
|
|
|
var index = SelectedPairIndex();
|
|
if (index < 0)
|
|
{
|
|
throw new InvalidOperationException("Select a saved comparison pair first.");
|
|
}
|
|
|
|
var pair = _savedPairs[index];
|
|
var draftId = $"comparison-draft-{++_draftSequence:D8}";
|
|
return LegacyComparisonWorkflowCatalog.Materialize(
|
|
actionId,
|
|
pair.First,
|
|
pair.Second,
|
|
draftId);
|
|
}
|
|
|
|
private LegacyComparisonWorkflowSnapshot ChooseSearchResult(
|
|
IReadOnlyList<SearchResultState> results,
|
|
string selectionId,
|
|
bool domestic)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(selectionId);
|
|
var result = results.FirstOrDefault(item =>
|
|
string.Equals(item.SelectionId, selectionId, StringComparison.Ordinal));
|
|
if (result is not null)
|
|
{
|
|
if (domestic)
|
|
{
|
|
_selectedDomesticResultId = selectionId;
|
|
}
|
|
else
|
|
{
|
|
_selectedWorldResultId = selectionId;
|
|
}
|
|
|
|
RotateDraft(result.Target);
|
|
Touch();
|
|
}
|
|
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
private LegacyComparisonWorkflowSnapshot SelectSearchResult(
|
|
IReadOnlyList<SearchResultState> results,
|
|
string selectionId,
|
|
bool domestic)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(selectionId);
|
|
if (results.Any(item => string.Equals(
|
|
item.SelectionId,
|
|
selectionId,
|
|
StringComparison.Ordinal)))
|
|
{
|
|
if (domestic)
|
|
{
|
|
_selectedDomesticResultId = selectionId;
|
|
}
|
|
else
|
|
{
|
|
_selectedWorldResultId = selectionId;
|
|
}
|
|
|
|
Touch();
|
|
}
|
|
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
private void RotateDraft(LegacyComparisonTarget selected)
|
|
{
|
|
_secondTarget = _firstTarget;
|
|
_firstTarget = selected;
|
|
}
|
|
|
|
private async Task<LegacyComparisonTarget> ResolveStoredTargetAsync(
|
|
LegacyComparisonStoredTargetReference stored,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var fixedTarget = LegacyComparisonWorkflowCatalog.TryResolveFixedStoredTarget(stored);
|
|
var resolved = fixedTarget ?? await _dataService.ResolveStoredTargetAsync(
|
|
stored,
|
|
cancellationToken).ConfigureAwait(false);
|
|
if (resolved is null)
|
|
{
|
|
throw new InvalidOperationException("A stored comparison target no longer resolves.");
|
|
}
|
|
|
|
resolved = LegacyComparisonWorkflowCatalog.RequireCanonicalTarget(resolved);
|
|
if (!LegacyComparisonWorkflowCatalog.MatchesStoredReference(resolved, stored))
|
|
{
|
|
throw new InvalidOperationException("A stored comparison target resolved to a different identity.");
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
private async Task<LegacyComparisonWorkflowSnapshot> CommitPairsAsync(
|
|
IReadOnlyList<SavedPairState> candidate,
|
|
string? nextSelection,
|
|
string successMessage,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
BeginBusy();
|
|
try
|
|
{
|
|
var writeRows = candidate.Select((pair, index) =>
|
|
{
|
|
var first = LegacyComparisonWorkflowCatalog.ToStoredReference(pair.First);
|
|
var second = LegacyComparisonWorkflowCatalog.ToStoredReference(pair.Second);
|
|
return new LegacyComparisonPairWriteRow(
|
|
index + 1,
|
|
first.Name,
|
|
first.Market,
|
|
second.Name,
|
|
second.Market);
|
|
}).ToArray();
|
|
await _pairStore.WriteCp949Async(
|
|
Array.AsReadOnly(writeRows),
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
_savedPairs = candidate.ToList();
|
|
_selectedPairId = nextSelection;
|
|
_statusMessage = successMessage;
|
|
_statusKind = LegacyOperatorStatusKind.Information;
|
|
}
|
|
catch
|
|
{
|
|
_statusMessage = "종목 비교쌍 파일 쓰기를 완료하지 못해 변경을 적용하지 않았습니다.";
|
|
_statusKind = LegacyOperatorStatusKind.Error;
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
EndBusy();
|
|
}
|
|
|
|
return CreateSnapshot();
|
|
}
|
|
|
|
private static List<SearchResultState> ValidateSearchResults(
|
|
IReadOnlyList<LegacyComparisonTarget>? results,
|
|
int maximumResults,
|
|
bool domestic,
|
|
int version)
|
|
{
|
|
if (results is null || results.Count > maximumResults)
|
|
{
|
|
throw new InvalidOperationException("The comparison search result count is invalid.");
|
|
}
|
|
|
|
var validated = new List<SearchResultState>(results.Count);
|
|
var targetIds = new HashSet<string>(StringComparer.Ordinal);
|
|
for (var index = 0; index < results.Count; index++)
|
|
{
|
|
var target = LegacyComparisonWorkflowCatalog.RequireCanonicalTarget(results[index]);
|
|
var allowedKind = domestic
|
|
? target.Kind is LegacyComparisonTargetKind.KrxStock or LegacyComparisonTargetKind.NxtStock
|
|
: target.Kind == LegacyComparisonTargetKind.WorldStock;
|
|
if (!allowedKind || !targetIds.Add(target.TargetId))
|
|
{
|
|
throw new InvalidOperationException("The comparison search result identity is invalid.");
|
|
}
|
|
|
|
validated.Add(new SearchResultState(
|
|
$"comparison-{(domestic ? "domestic" : "world")}-{version:D4}-{index + 1:D4}",
|
|
target));
|
|
}
|
|
|
|
return validated;
|
|
}
|
|
|
|
private LegacyComparisonWorkflowSnapshot CreateSnapshot()
|
|
{
|
|
var selectedPair = SelectedPairIndex() is var selectedIndex && selectedIndex >= 0
|
|
? _savedPairs[selectedIndex]
|
|
: null;
|
|
var domesticResults = _domesticResults
|
|
.Select(result => LegacyComparisonWorkflowCatalog.ToView(
|
|
result.Target,
|
|
result.SelectionId))
|
|
.ToArray();
|
|
var worldResults = _worldResults
|
|
.Select(result => LegacyComparisonWorkflowCatalog.ToView(
|
|
result.Target,
|
|
result.SelectionId))
|
|
.ToArray();
|
|
var fixedTargets = LegacyComparisonWorkflowCatalog.FixedTargets
|
|
.Select(target => LegacyComparisonWorkflowCatalog.ToView(target, target.TargetId))
|
|
.ToArray();
|
|
var savedPairs = _savedPairs.Select((pair, index) =>
|
|
new LegacyComparisonSavedPairView(
|
|
pair.RowId,
|
|
index + 1,
|
|
$"{pair.First.DisplayName} ↔ {pair.Second.DisplayName}",
|
|
LegacyComparisonWorkflowCatalog.ToView(pair.First, $"{pair.RowId}-first"),
|
|
LegacyComparisonWorkflowCatalog.ToView(pair.Second, $"{pair.RowId}-second"),
|
|
string.Equals(pair.RowId, _selectedPairId, StringComparison.Ordinal)))
|
|
.ToArray();
|
|
return new LegacyComparisonWorkflowSnapshot(
|
|
_revision,
|
|
_isBusy,
|
|
new LegacyComparisonSearchSnapshot(
|
|
_domesticQuery,
|
|
_domesticStatus,
|
|
Array.AsReadOnly(domesticResults),
|
|
_selectedDomesticResultId,
|
|
domesticResults.Length,
|
|
_domesticError),
|
|
new LegacyComparisonSearchSnapshot(
|
|
_worldQuery,
|
|
_worldStatus,
|
|
Array.AsReadOnly(worldResults),
|
|
_selectedWorldResultId,
|
|
worldResults.Length,
|
|
_worldError),
|
|
Array.AsReadOnly(fixedTargets),
|
|
_selectedFixedTargetId,
|
|
_firstTarget is null
|
|
? null
|
|
: LegacyComparisonWorkflowCatalog.ToView(_firstTarget, "comparison-draft-first"),
|
|
_secondTarget is null
|
|
? null
|
|
: LegacyComparisonWorkflowCatalog.ToView(_secondTarget, "comparison-draft-second"),
|
|
Array.AsReadOnly(savedPairs),
|
|
_selectedPairId,
|
|
LegacyComparisonWorkflowCatalog.CreateActionViews(
|
|
selectedPair?.First,
|
|
selectedPair?.Second),
|
|
_statusMessage,
|
|
_statusKind);
|
|
}
|
|
|
|
private int SelectedPairIndex() => _selectedPairId is null
|
|
? -1
|
|
: _savedPairs.FindIndex(pair =>
|
|
string.Equals(pair.RowId, _selectedPairId, StringComparison.Ordinal));
|
|
|
|
private static string NormalizeSearchQuery(string query)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(query);
|
|
var normalized = query.Trim();
|
|
if (normalized.Length > 64 ||
|
|
normalized.Any(static character => char.IsControl(character) || char.IsSurrogate(character)))
|
|
{
|
|
throw new ArgumentException("A comparison query must be at most 64 safe characters.", nameof(query));
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
private void BeginBusy()
|
|
{
|
|
if (_isBusy)
|
|
{
|
|
throw new InvalidOperationException("The comparison workflow is already busy.");
|
|
}
|
|
|
|
_isBusy = true;
|
|
Touch();
|
|
}
|
|
|
|
private void EndBusy()
|
|
{
|
|
_isBusy = false;
|
|
Touch();
|
|
}
|
|
|
|
private void Touch() => _revision++;
|
|
|
|
private sealed record SearchResultState(
|
|
string SelectionId,
|
|
LegacyComparisonTarget Target);
|
|
|
|
private sealed record SavedPairState(
|
|
string RowId,
|
|
LegacyComparisonTarget First,
|
|
LegacyComparisonTarget Second);
|
|
}
|