using System.Globalization; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.LegacyApplication; public enum LegacyIndustryComparisonSlot { First, Second } public sealed record LegacyIndustrySelectionSnapshot( long Revision, IndustryMarket? Market, IReadOnlyList Industries, IReadOnlyList Actions, int? SelectedIndex, IndustrySelection? SelectedIndustry, IndustrySelection? FirstIndustry, IndustrySelection? SecondIndustry, string FirstText, string SecondText); public sealed record LegacyIndustrySceneSelectionDraft( string GroupCode, string Subject, string GraphicType, string Subtype, string DataCode); /// /// Safe, generic playlist fields produced entirely from the C# catalog and the /// authoritative UC2 selection state. It intentionally has no caller-supplied row id. /// public sealed record LegacyIndustryPlaylistDraft( string ActionId, string BuilderKey, string SceneAlias, int PageSize, string Title, string Detail, string Category, IndustryMarket Market, LegacyIndustrySceneSelectionDraft Selection, string RuntimeAssetRelativePath, string RuntimeAssetSha256, bool IsEnabled, int FadeDuration); public sealed class LegacyIndustryWorkflowException : Exception { public LegacyIndustryWorkflowException(string message) : base(message) { } } /// /// C# authority for the legacy UC2 selector and the UC1 industry actions. /// public sealed class LegacyIndustrySelectionWorkflow { private const int DefaultFadeDuration = 6; public const int MaximumComparisonTextLength = 128; private static readonly IReadOnlyList NoIndustries = Array.AsReadOnly(Array.Empty()); private static readonly IReadOnlyList NoActions = Array.AsReadOnly(Array.Empty()); private readonly IIndustrySelectionService _selectionService; private readonly TimeProvider _timeProvider; private readonly LegacyIndustryActionLayout _actionLayout; private IReadOnlyList _industries = NoIndustries; private IReadOnlyList _actions = NoActions; private IndustryMarket? _market; private int? _selectedIndex; private IndustrySelection? _selectedIndustry; private IndustrySelection? _firstIndustry; private IndustrySelection? _secondIndustry; private string _firstText = string.Empty; private string _secondText = string.Empty; private long _revision; private long _loadGeneration; public LegacyIndustrySelectionWorkflow( IIndustrySelectionService selectionService, TimeProvider? timeProvider = null, LegacyIndustryActionLayout? actionLayout = null) { _selectionService = selectionService ?? throw new ArgumentNullException(nameof(selectionService)); _timeProvider = timeProvider ?? TimeProvider.System; _actionLayout = actionLayout ?? LegacyIndustryActionLayout.Default; } public LegacyIndustrySelectionSnapshot Current => CreateSnapshot(); /// /// Recreates the observable UC2 state for one tab activation. The generation /// increment prevents a request started by the discarded control from restoring /// its rows after the operator has re-entered the tab. /// public LegacyIndustrySelectionSnapshot ResetForTabActivation(IndustryMarket market) { ValidateMarket(market); Interlocked.Increment(ref _loadGeneration); _market = market; _industries = NoIndustries; _actions = _actionLayout.GetActions(market); _selectedIndex = null; _selectedIndustry = null; _firstIndustry = null; _secondIndustry = null; _firstText = string.Empty; _secondText = string.Empty; Touch(); return CreateSnapshot(); } public async Task LoadMarketAsync( IndustryMarket market, CancellationToken cancellationToken = default) { ValidateMarket(market); var generation = Interlocked.Increment(ref _loadGeneration); var serviceRows = await _selectionService.GetAsync(market, cancellationToken) .ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); // A slower previous request must never replace a newer market request. if (generation != Volatile.Read(ref _loadGeneration)) { return CreateSnapshot(); } var normalizedRows = NormalizeRows(serviceRows, market); if (generation != Volatile.Read(ref _loadGeneration)) { return CreateSnapshot(); } var changingMarket = _market != market; var previousSelected = changingMarket ? null : _selectedIndustry; var previousFirst = changingMarket ? null : _firstIndustry; var previousSecond = changingMarket ? null : _secondIndustry; var previousFirstText = changingMarket ? string.Empty : _firstText; var previousSecondText = changingMarket ? string.Empty : _secondText; _market = market; _industries = normalizedRows; _actions = _actionLayout.GetActions(market); _selectedIndustry = Reconcile(previousSelected, normalizedRows); _selectedIndex = _selectedIndustry is null ? null : FindIdentityIndex(normalizedRows, _selectedIndustry); _firstIndustry = Reconcile(previousFirst, normalizedRows); _secondIndustry = Reconcile(previousSecond, normalizedRows); _firstText = previousFirstText; _secondText = previousSecondText; Touch(); return CreateSnapshot(); } public LegacyIndustrySelectionSnapshot SelectIndustry(int index) { if (!TryGetIndustry(index, out var industry)) { return CreateSnapshot(); } _selectedIndex = index; _selectedIndustry = industry; Touch(); return CreateSnapshot(); } public LegacyIndustrySelectionSnapshot DoubleClickIndustry(int index) { if (!TryGetIndustry(index, out var industry)) { return CreateSnapshot(); } _selectedIndex = index; _selectedIndustry = industry; // UC2.listbox_DoubleClick copied txt1 to txt2 before assigning the newly // double-clicked item to txt1. _secondIndustry = _firstIndustry; _secondText = _firstText; _firstIndustry = industry; _firstText = industry.IndustryName; Touch(); return CreateSnapshot(); } public LegacyIndustrySelectionSnapshot SetComparisonText( LegacyIndustryComparisonSlot slot, string text) { ArgumentNullException.ThrowIfNull(text); if (slot is not (LegacyIndustryComparisonSlot.First or LegacyIndustryComparisonSlot.Second)) { throw new ArgumentOutOfRangeException(nameof(slot), slot, "The comparison slot must be 1 or 2."); } if (text.Length > MaximumComparisonTextLength || text.Any(static character => char.IsControl(character) || char.IsSurrogate(character) || character == '\uFFFD')) { throw new LegacyIndustryWorkflowException("The industry comparison text is invalid."); } if (slot == LegacyIndustryComparisonSlot.First) { if (string.Equals(_firstText, text, StringComparison.Ordinal)) { return CreateSnapshot(); } _firstText = text; _firstIndustry = null; } else { if (string.Equals(_secondText, text, StringComparison.Ordinal)) { return CreateSnapshot(); } _secondText = text; _secondIndustry = null; } Touch(); return CreateSnapshot(); } public LegacyIndustrySelectionSnapshot SwapIndustries() { if (_firstText.Length == 0 && _secondText.Length == 0) { return CreateSnapshot(); } (_firstIndustry, _secondIndustry) = (_secondIndustry, _firstIndustry); (_firstText, _secondText) = (_secondText, _firstText); Touch(); return CreateSnapshot(); } public LegacyIndustrySelectionSnapshot ClearSelections() { if (_selectedIndustry is null && _firstText.Length == 0 && _secondText.Length == 0) { return CreateSnapshot(); } _selectedIndex = null; _selectedIndustry = null; _firstIndustry = null; _secondIndustry = null; _firstText = string.Empty; _secondText = string.Empty; Touch(); return CreateSnapshot(); } public LegacyIndustryPlaylistDraft MaterializePlaylistDraft(string actionId) { ArgumentNullException.ThrowIfNull(actionId); if (_market is not { } market) { throw new LegacyIndustryWorkflowException("Select an industry market first."); } if (!LegacyIndustryActionCatalog.TryGetAction(market, actionId, out var definition) || definition is null) { throw new LegacyIndustryWorkflowException("The industry action is not registered."); } var projection = ProjectSelection(definition, market); var asset = LegacyIndustryActionCatalog.GetRuntimeAsset(market); return new LegacyIndustryPlaylistDraft( definition.ActionId, definition.BuilderKey, definition.SceneAlias, definition.PageSize, projection.Title, definition.Label, definition.Category, market, projection.Selection, asset.RelativePath, asset.Sha256, IsEnabled: true, FadeDuration: DefaultFadeDuration); } private IndustryProjection ProjectSelection( LegacyIndustryActionDefinition definition, IndustryMarket market) { var industryGroup = market == IndustryMarket.Kospi ? "INDUSTRY_KOSPI" : "INDUSTRY_KOSDAQ"; var marketName = market == IndustryMarket.Kospi ? "KOSPI" : "KOSDAQ"; return definition.Kind switch { LegacyIndustryActionKind.Single => ForSelected( definition, industryGroup, "1열판", "CURRENT"), LegacyIndustryActionKind.Pair => ForPair(definition, industryGroup), LegacyIndustryActionKind.Yield => ForSelected( definition, industryGroup, "YIELD_GRAPH", definition.YieldPeriod!), LegacyIndustryActionKind.Candle => ForSelected( definition, market == IndustryMarket.Kospi ? "KOSPI_INDUSTRY" : "KOSDAQ_INDUSTRY", string.Empty, definition.CandleMode == LegacyIndustryCandleMode.Volume ? "VOLUME" : "PRICE"), LegacyIndustryActionKind.FiveRow => new IndustryProjection( $"{MarketLabel(market)} {definition.Label}", new LegacyIndustrySceneSelectionDraft( market == IndustryMarket.Kospi ? "PAGED_DOMESTIC_KOSPI" : "PAGED_DOMESTIC_KOSDAQ", "INDEX", string.Empty, "INDUSTRY", CurrentLocalDate())), LegacyIndustryActionKind.Square => new IndustryProjection( $"{MarketLabel(market)} {definition.Label}", new LegacyIndustrySceneSelectionDraft( industryGroup, marketName, "네모그래프", string.Empty, string.Empty)), LegacyIndustryActionKind.Sector => new IndustryProjection( $"{MarketLabel(market)} {definition.Label}", new LegacyIndustrySceneSelectionDraft( industryGroup, marketName, "SECTOR_INDEX", string.Empty, string.Empty)), _ => throw new LegacyIndustryWorkflowException("The industry action kind is unsupported.") }; } private IndustryProjection ForSelected( LegacyIndustryActionDefinition definition, string groupCode, string graphicType, string subtype) { if (_selectedIndustry is not { } selected) { throw new LegacyIndustryWorkflowException( $"The {definition.ActionId} action requires one selected industry."); } return new IndustryProjection( selected.IndustryName, new LegacyIndustrySceneSelectionDraft( groupCode, selected.IndustryName, graphicType, subtype, string.Empty)); } private IndustryProjection ForPair( LegacyIndustryActionDefinition definition, string groupCode) { if (_selectedIndustry is null || string.IsNullOrWhiteSpace(_firstText) || string.IsNullOrWhiteSpace(_secondText) || _firstText.Contains('-', StringComparison.Ordinal) || _secondText.Contains('-', StringComparison.Ordinal)) { throw new LegacyIndustryWorkflowException( $"The {definition.ActionId} action requires a selected industry and two delimiter-safe industries."); } var subject = $"{_firstText}-{_secondText}"; return new IndustryProjection( subject, new LegacyIndustrySceneSelectionDraft( groupCode, subject, "2열판", string.Empty, string.Empty)); } private string CurrentLocalDate() { var local = _timeProvider.GetLocalNow(); return local.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } private bool TryGetIndustry(int index, out IndustrySelection industry) { if (index < 0 || index >= _industries.Count) { industry = null!; return false; } industry = _industries[index]; return true; } private static IReadOnlyList NormalizeRows( IReadOnlyList? rows, IndustryMarket market) { if (rows is null || rows.Count > LegacyIndustrySelectionService.MaximumResults) { throw new LegacyIndustryWorkflowException("The industry list is invalid."); } var normalized = new IndustrySelection[rows.Count]; for (var index = 0; index < rows.Count; index++) { var row = rows[index]; if (row is null || row.Market != market) { throw new LegacyIndustryWorkflowException( "The industry list contains a cross-market row."); } var name = row.IndustryName?.Trim(); var code = row.IndustryCode?.Trim(); if (string.IsNullOrEmpty(name) || name.Length > LegacyIndustrySelectionService.MaximumNameLength || name.Any(char.IsControl) || string.IsNullOrEmpty(code) || code.Length > 32 || code.Any(character => !char.IsAsciiLetterOrDigit(character))) { throw new LegacyIndustryWorkflowException( "The industry list contains an unsafe row."); } normalized[index] = new IndustrySelection(market, name, code); } return Array.AsReadOnly(normalized); } private static IndustrySelection? Reconcile( IndustrySelection? previous, IReadOnlyList rows) { if (previous is null) { return null; } return rows.FirstOrDefault(row => SameIdentity(row, previous)); } private static int? FindIdentityIndex( IReadOnlyList rows, IndustrySelection selected) { for (var index = 0; index < rows.Count; index++) { if (SameIdentity(rows[index], selected)) { return index; } } return null; } private static bool SameIdentity(IndustrySelection left, IndustrySelection right) => left.Market == right.Market && string.Equals(left.IndustryName, right.IndustryName, StringComparison.Ordinal) && string.Equals(left.IndustryCode, right.IndustryCode, StringComparison.Ordinal); private static void ValidateMarket(IndustryMarket market) { if (market is not (IndustryMarket.Kospi or IndustryMarket.Kosdaq)) { throw new ArgumentOutOfRangeException( nameof(market), market, "Unsupported industry market."); } } private static string MarketLabel(IndustryMarket market) => market switch { IndustryMarket.Kospi => "코스피", IndustryMarket.Kosdaq => "코스닥", _ => throw new ArgumentOutOfRangeException(nameof(market), market, null) }; private LegacyIndustrySelectionSnapshot CreateSnapshot() => new( _revision, _market, _industries, _actions, _selectedIndex, _selectedIndustry, _firstIndustry, _secondIndustry, _firstText, _secondText); private void Touch() => _revision++; private sealed record IndustryProjection( string Title, LegacyIndustrySceneSelectionDraft Selection); }