#nullable enable

using System.Globalization;

namespace MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

public enum CandleCutAlias
{
    Cut8035 = 8035,
    Cut8061 = 8061,
    Cut8040 = 8040,
    Cut8046 = 8046,
    Cut8051 = 8051,
    Cut8056 = 8056
}

public enum CandlePeriod
{
    Intraday,
    FiveDays,
    TwentyDays,
    SixtyDays,
    OneHundredTwentyDays,
    TwoHundredFortyDays
}

public enum CandleChartMode
{
    Price,
    PriceWithVolume,
    ExpectedExecution,
    TradingHalt
}

public enum CandleMarketTarget
{
    KospiIndex,
    KosdaqIndex,
    Kospi200Index,
    Krx100Index,
    FuturesIndex,
    KospiIndustry,
    KosdaqIndustry,
    OverseasIndex,
    OverseasStock,
    KospiStock,
    KosdaqStock
}

public static class CandleCutAliasMetadata
{
    public static CandlePeriod GetPeriod(CandleCutAlias alias) => alias switch
    {
        CandleCutAlias.Cut8035 => CandlePeriod.Intraday,
        CandleCutAlias.Cut8061 => CandlePeriod.FiveDays,
        CandleCutAlias.Cut8040 => CandlePeriod.TwentyDays,
        CandleCutAlias.Cut8046 => CandlePeriod.SixtyDays,
        CandleCutAlias.Cut8051 => CandlePeriod.OneHundredTwentyDays,
        CandleCutAlias.Cut8056 => CandlePeriod.TwoHundredFortyDays,
        _ => throw new LegacySceneDataException("Unknown candle cut alias.")
    };

    internal static double GetMovingAverageWidth(CandleCutAlias alias) => alias switch
    {
        CandleCutAlias.Cut8035 => 0d,
        CandleCutAlias.Cut8061 => 1188d,
        CandleCutAlias.Cut8040 => 1398d,
        CandleCutAlias.Cut8046 => 1442d,
        CandleCutAlias.Cut8051 => 1452d,
        CandleCutAlias.Cut8056 => 1464d,
        _ => throw new LegacySceneDataException("Unknown candle cut alias.")
    };

    internal static int GetAssetObjectCapacity(CandleCutAlias alias) => alias switch
    {
        CandleCutAlias.Cut8035 => 78,
        CandleCutAlias.Cut8061 => 5,
        CandleCutAlias.Cut8040 => 20,
        CandleCutAlias.Cut8046 => 60,
        CandleCutAlias.Cut8051 => 120,
        CandleCutAlias.Cut8056 => 240,
        _ => throw new LegacySceneDataException("Unknown candle cut alias.")
    };

    internal static bool SupportsMovingAveragePaths(CandleCutAlias alias) =>
        alias != CandleCutAlias.Cut8035;

    internal static bool SupportsGuide(CandleCutAlias alias) => alias is not
        (CandleCutAlias.Cut8035 or CandleCutAlias.Cut8061);
}

public sealed record CandleQuoteData(
    double CurrentPrice,
    double ChangePrice,
    double Rate,
    ScenePriceDirection Direction);

public sealed record CandleRowData(
    DateOnly TradingDate,
    TimeOnly? TradingTime,
    double Open,
    double High,
    double Low,
    double Close,
    double? Volume = null,
    double? MovingAverage5 = null,
    double? MovingAverage20 = null);

public sealed record S8010SceneData(
    CandleCutAlias CutAlias,
    CandleMarketTarget MarketTarget,
    CandleChartMode ChartMode,
    string DisplayName,
    CandleQuoteData Quote,
    IReadOnlyList<CandleRowData> Rows,
    bool ShowMovingAverage5,
    bool ShowMovingAverage20) : ILegacySceneData;

public sealed class S8010SceneMutationBuilder : ILegacySceneMutationBuilder<S8010SceneData>
{
    private const double GraphStartX = -730.57d;
    private const double GraphEndX = 738.65d;
    private const double TagMinimumX = -657.57d;
    private const double TagMaximumX = 670.65d;

    public string BuilderKey => "s8010";

    public IReadOnlyList<PlayoutMutation> Build(S8010SceneData data)
    {
        ArgumentNullException.ThrowIfNull(data);
        ValidateEnums(data);
        var displayName = LegacySceneBuilderGuard.Text(
            data.DisplayName,
            nameof(data.DisplayName));
        var quote = LegacySceneBuilderGuard.NotNull(data.Quote, nameof(data.Quote));
        ValidateQuote(quote);
        var rows = NormalizeRows(data.Rows, data.CutAlias, data.ChartMode);
        var period = CandleCutAliasMetadata.GetPeriod(data.CutAlias);
        var lineWidth = CandleCutAliasMetadata.GetMovingAverageWidth(data.CutAlias);
        var format = UsesIntegerPrices(data.MarketTarget) ? "#,##0" : "#,##0.00";
        var mutations = new List<PlayoutMutation>(
            (CandleCutAliasMetadata.GetAssetObjectCapacity(data.CutAlias) * 2) +
            60 + (rows.Count * 12))
        {
            new PlayoutSetValue("title", InitialTitle(displayName, data.ChartMode))
        };
        if (CandleCutAliasMetadata.SupportsMovingAveragePaths(data.CutAlias))
        {
            // A path mutation with no points reproduces Begin/Clear/End from the legacy setup.
            mutations.Add(new PlayoutSetPathPoints("ma5", Array.Empty<PlayoutPoint>()));
            mutations.Add(new PlayoutSetPathPoints("ma20", Array.Empty<PlayoutPoint>()));
        }
        else
        {
            // The intraday asset exposes ma5/ma20 as ordinary visible objects,
            // not editable paths. Hide the baked defaults without PATH_POINT.
            mutations.Add(new PlayoutSetVisible("ma5", false));
            mutations.Add(new PlayoutSetVisible("ma20", false));
        }

        AddResolvedTitleAndUnit(
            mutations,
            displayName,
            data.MarketTarget,
            data.ChartMode);
        AddQuote(mutations, quote, format);

        var range = CreateRange(rows, data);
        AddInitialVisibility(mutations, data.CutAlias, data.ChartMode);
        var paths = AddCandles(
            mutations,
            rows,
            data,
            period,
            lineWidth,
            format,
            range);
        AddMovingAveragePaths(mutations, paths, data);
        return mutations;
    }

    private static void ValidateEnums(S8010SceneData data)
    {
        if (!Enum.IsDefined(data.CutAlias) || !Enum.IsDefined(data.MarketTarget) ||
            !Enum.IsDefined(data.ChartMode))
        {
            throw new LegacySceneDataException("Unknown candle scene selection.");
        }

        if (data.ChartMode == CandleChartMode.TradingHalt &&
            data.MarketTarget is not (CandleMarketTarget.KospiStock or
                CandleMarketTarget.KosdaqStock))
        {
            throw new LegacySceneDataException(
                "Trading-halt candles require a domestic stock target.");
        }
    }

    private static void ValidateQuote(CandleQuoteData quote)
    {
        if (!double.IsFinite(quote.CurrentPrice) ||
            !double.IsFinite(quote.ChangePrice) ||
            !double.IsFinite(quote.Rate) ||
            !Enum.IsDefined(quote.Direction))
        {
            throw new LegacySceneDataException("Candle quote data is invalid.");
        }
    }

    private static IReadOnlyList<CandleRowData> NormalizeRows(
        IReadOnlyList<CandleRowData>? rows,
        CandleCutAlias alias,
        CandleChartMode chartMode)
    {
        var values = LegacySceneBuilderGuard.Range(
            rows,
            1,
            CandleCutAliasMetadata.GetAssetObjectCapacity(alias),
            nameof(rows));
        var requiresTime = CandleCutAliasMetadata.GetPeriod(alias) == CandlePeriod.Intraday;
        return values.Select((row, index) =>
        {
            var value = LegacySceneBuilderGuard.NotNull(row, $"Rows[{index}]");
            if (requiresTime && value.TradingTime is null)
            {
                throw new LegacySceneDataException(
                    $"Scene data field 'Rows[{index}].TradingTime' is required for 8035.");
            }

            if (!double.IsFinite(value.Open) || !double.IsFinite(value.High) ||
                !double.IsFinite(value.Low) || !double.IsFinite(value.Close) ||
                value.High < value.Low || value.High < value.Open ||
                value.High < value.Close || value.Low > value.Open ||
                value.Low > value.Close)
            {
                throw new LegacySceneDataException(
                    $"Scene data field 'Rows[{index}]' has an invalid OHLC range.");
            }

            ValidateOptionalFinite(value.Volume, $"Rows[{index}].Volume");
            ValidateOptionalFinite(value.MovingAverage5, $"Rows[{index}].MovingAverage5");
            ValidateOptionalFinite(value.MovingAverage20, $"Rows[{index}].MovingAverage20");
            if (value.Volume < 0)
            {
                throw new LegacySceneDataException(
                    $"Scene data field 'Rows[{index}].Volume' cannot be negative.");
            }

            if (chartMode == CandleChartMode.PriceWithVolume && value.Volume is null)
            {
                // The vendor source treats DB null as zero. Normalize it before calculating max.
                return value with { Volume = 0d };
            }

            return value;
        }).ToArray();
    }

    private static void ValidateOptionalFinite(double? value, string name)
    {
        if (value.HasValue && !double.IsFinite(value.Value))
        {
            throw new LegacySceneDataException($"Scene data field '{name}' must be finite.");
        }
    }

    private static string InitialTitle(string displayName, CandleChartMode mode) =>
        mode == CandleChartMode.ExpectedExecution
            ? displayName + " \uC608\uC0C1\uCCB4\uACB0"
            : displayName;

    private static void AddResolvedTitleAndUnit(
        ICollection<PlayoutMutation> mutations,
        string displayName,
        CandleMarketTarget target,
        CandleChartMode mode)
    {
        if (target == CandleMarketTarget.OverseasStock)
        {
            mutations.Add(new PlayoutSetValue("unit", "\uB2E8\uC704 : \uB2EC\uB7EC"));
            return;
        }

        if (target == CandleMarketTarget.OverseasIndex)
        {
            mutations.Add(new PlayoutSetValue("unit", "\uB2E8\uC704 : p"));
            return;
        }

        if (IsIndex(target))
        {
            var title = MarketLabel(target) + (mode == CandleChartMode.ExpectedExecution
                ? " \uC608\uC0C1\uCCB4\uACB0\uC9C0\uC218"
                : " \uC9C0\uC218");
            mutations.Add(new PlayoutSetValue("title", title));
            mutations.Add(new PlayoutSetValue("unit", "\uB2E8\uC704 : p"));
            return;
        }

        if (mode == CandleChartMode.ExpectedExecution)
        {
            // The legacy non-index expected-price branch only rewrites the title.
            mutations.Add(new PlayoutSetValue(
                "title",
                displayName + " \uC608\uC0C1\uCCB4\uACB0"));
            return;
        }

        var resolvedTitle = target switch
        {
            CandleMarketTarget.KospiIndustry => "\uCF54\uC2A4\uD53C - " + displayName,
            CandleMarketTarget.KosdaqIndustry => "\uCF54\uC2A4\uB2E5 - " + displayName,
            _ => displayName
        };
        var unit = IsIndustry(target) ? "\uB2E8\uC704 : p" : "\uB2E8\uC704 : \uC6D0";
        // The legacy stock/industry branch writes unit before its resolved title.
        mutations.Add(new PlayoutSetValue("unit", unit));
        mutations.Add(new PlayoutSetValue("title", resolvedTitle));
    }

    private static string MarketLabel(CandleMarketTarget target) => target switch
    {
        CandleMarketTarget.KospiIndex => "\uCF54\uC2A4\uD53C",
        CandleMarketTarget.KosdaqIndex => "\uCF54\uC2A4\uB2E5",
        CandleMarketTarget.Kospi200Index => "\uCF54\uC2A4\uD53C200",
        CandleMarketTarget.Krx100Index => "KRX100",
        CandleMarketTarget.FuturesIndex => "\uC120\uBB3C",
        _ => throw new LegacySceneDataException("The target is not a domestic index.")
    };

    private static void AddQuote(
        ICollection<PlayoutMutation> mutations,
        CandleQuoteData quote,
        string format)
    {
        mutations.Add(new PlayoutSetValue(
            "price",
            quote.CurrentPrice.ToString(format, CultureInfo.InvariantCulture)));
        mutations.Add(new PlayoutSetValue(
            "changePrice",
            quote.ChangePrice.ToString(format, CultureInfo.InvariantCulture)));
        mutations.Add(new PlayoutSetValue(
            "rate",
            quote.Rate.ToString("##0.00", CultureInfo.InvariantCulture)));
        CandleQuoteDirection.Add(mutations, quote.Direction);
    }

    private static CandleRange CreateRange(
        IReadOnlyList<CandleRowData> rows,
        S8010SceneData data)
    {
        var candleMinimum = rows.Min(row => row.Low);
        var candleMaximum = rows.Max(row => row.High);
        var minimum = candleMinimum;
        var maximum = candleMaximum;
        if (SupportsMovingAverages(data.MarketTarget))
        {
            if (data.ShowMovingAverage5)
            {
                var values = rows.Where(row => row.MovingAverage5.HasValue)
                    .Select(row => row.MovingAverage5!.Value)
                    .ToArray();
                if (values.Length != 0)
                {
                    minimum = Math.Min(minimum, values.Min());
                    maximum = Math.Max(maximum, values.Max());
                }
            }

            if (data.ShowMovingAverage20)
            {
                var values = rows.Where(row => row.MovingAverage20.HasValue)
                    .Select(row => row.MovingAverage20!.Value)
                    .ToArray();
                if (values.Length != 0)
                {
                    minimum = Math.Min(minimum, values.Min());
                    maximum = Math.Max(maximum, values.Max());
                }
            }
        }

        var gap = maximum - minimum;
        if (!double.IsFinite(gap))
        {
            throw new LegacySceneDataException(
                "Candle price range cannot be represented as a finite value.");
        }

        if (gap == 0d)
        {
            // Equal OHLC input formerly divided by zero. A unit range keeps every crop finite.
            gap = 1d;
        }

        var maximumVolume = data.ChartMode == CandleChartMode.PriceWithVolume
            ? rows.Max(row => row.Volume ?? 0d)
            : 0d;
        return new CandleRange(
            minimum,
            maximum,
            gap,
            candleMinimum,
            candleMaximum,
            maximumVolume);
    }

    private static void AddInitialVisibility(
        ICollection<PlayoutMutation> mutations,
        CandleCutAlias alias,
        CandleChartMode mode)
    {
        mutations.Add(new PlayoutSetVisible(
            "volrumeG",
            mode == CandleChartMode.PriceWithVolume));
        var capacity = CandleCutAliasMetadata.GetAssetObjectCapacity(alias);
        for (var index = 1; index <= capacity; index++)
        {
            mutations.Add(new PlayoutSetVisible("volume" + index, false));
        }

        for (var index = 1; index <= capacity; index++)
        {
            mutations.Add(new PlayoutSetVisible("candle" + index, false));
        }
    }

    private static CandlePaths AddCandles(
        ICollection<PlayoutMutation> mutations,
        IReadOnlyList<CandleRowData> rows,
        S8010SceneData data,
        CandlePeriod period,
        double lineWidth,
        string format,
        CandleRange range)
    {
        var ma5Points = new List<PlayoutPoint>(rows.Count);
        var ma20Points = new List<PlayoutPoint>(rows.Count);
        var needMovingAverageChecker = false;
        var movingAverageCounter = 0;
        var originalGraphLength = GraphEndX - GraphStartX;
        var endX = GraphEndX;
        var tagMaximum = TagMaximumX;
        var length = originalGraphLength;
        var lengthPerCandle = length / rows.Count;
        if (period == CandlePeriod.Intraday)
        {
            endX = (originalGraphLength * rows.Count / 78d) + GraphStartX;
            tagMaximum = endX - 68d;
            length = endX - GraphStartX;
            lengthPerCandle = length / rows.Count;
        }

        for (var zeroBased = 0; zeroBased < rows.Count; zeroBased++)
        {
            var row = rows[zeroBased];
            var index = zeroBased + 1;
            AddDateLabel(mutations, rows, period, zeroBased);

            if (row.High.Equals(range.CandleMaximum))
            {
                var x = TagPosition(index, lengthPerCandle, tagMaximum);
                mutations.Add(new PlayoutSetPosition(
                    "highTag", (float)x, 0, 0, PlayoutVectorComponents.X));
                mutations.Add(new PlayoutSetValue(
                    "highPrice", row.High.ToString(format, CultureInfo.InvariantCulture)));
            }

            if (row.Low.Equals(range.CandleMinimum))
            {
                var x = TagPosition(index, lengthPerCandle, tagMaximum);
                mutations.Add(new PlayoutSetPosition(
                    "lowTag", (float)x, 0, 0, PlayoutVectorComponents.X));
                mutations.Add(new PlayoutSetValue(
                    "lowPrice", row.Low.ToString(format, CultureInfo.InvariantCulture)));
            }

            mutations.Add(new PlayoutSetVisible("candle" + index, true));
            mutations.Add(Crop(
                "candlebar" + index,
                Normalize(row.High, range),
                Normalize(row.Low, range)));

            if (row.MovingAverage5.HasValue)
            {
                var include = true;
                if (needMovingAverageChecker)
                {
                    movingAverageCounter++;
                    include = movingAverageCounter >= 4;
                }

                if (include)
                {
                    ma5Points.Add(MovingAveragePoint(
                        row.MovingAverage5.Value,
                        zeroBased,
                        rows.Count,
                        lineWidth,
                        range));
                }
            }
            else
            {
                needMovingAverageChecker = true;
            }

            if (row.MovingAverage20.HasValue &&
                (!needMovingAverageChecker || movingAverageCounter >= 19))
            {
                ma20Points.Add(MovingAveragePoint(
                    row.MovingAverage20.Value,
                    zeroBased,
                    rows.Count,
                    lineWidth,
                    range));
            }

            AddCandleBody(mutations, row, index, range);
            if (data.ChartMode == CandleChartMode.PriceWithVolume)
            {
                var volume = row.Volume ?? 0d;
                var percentage = range.MaximumVolume == 0d
                    ? 0d
                    : Math.Abs(volume / range.MaximumVolume * 100d);
                mutations.Add(new PlayoutSetVisible("volume" + index, true));
                mutations.Add(Crop("volume" + index, 100d - percentage, 100d));
            }
        }

        return new CandlePaths(ma5Points, ma20Points);
    }

    private static void AddDateLabel(
        ICollection<PlayoutMutation> mutations,
        IReadOnlyList<CandleRowData> rows,
        CandlePeriod period,
        int zeroBasedIndex)
    {
        if (period == CandlePeriod.Intraday)
        {
            if (zeroBasedIndex != 0)
            {
                return;
            }

            var startsAtNine = rows[0].TradingTime!.Value.Hour == 9;
            mutations.Add(new PlayoutSetValue("date_begin", startsAtNine ? "09:00" : "10:00"));
            mutations.Add(new PlayoutSetValue("date_middle", startsAtNine ? "12:00" : "13:00"));
            mutations.Add(new PlayoutSetValue("date_end", startsAtNine ? "15:30" : "16:30"));
            return;
        }

        var index = zeroBasedIndex + 1;
        var value = rows[zeroBasedIndex].TradingDate.ToString("MM/dd", CultureInfo.InvariantCulture);
        if (index == 1)
        {
            mutations.Add(new PlayoutSetValue("date_begin", value));
        }
        else if (index == rows.Count)
        {
            mutations.Add(new PlayoutSetValue("date_end", value));
        }
        else if (index == (int)Math.Ceiling(rows.Count / 2d))
        {
            mutations.Add(new PlayoutSetValue("date_middle", value));
        }
    }

    private static double TagPosition(
        int oneBasedIndex,
        double lengthPerCandle,
        double tagMaximum)
    {
        var x = GraphStartX + (oneBasedIndex * lengthPerCandle) - (lengthPerCandle / 2d);
        if (x > tagMaximum)
        {
            x = tagMaximum;
        }
        else if (x < TagMinimumX)
        {
            x = TagMinimumX;
        }

        return x;
    }

    private static double Normalize(double value, CandleRange range) =>
        Math.Abs((value - range.Maximum) / range.Gap * 100d);

    private static PlayoutSetCropKey Crop(string objectName, double top, double bottom) =>
        new(
            objectName,
            1,
            0,
            (float)top,
            0,
            (float)bottom,
            PlayoutCropEdges.Vertical);

    private static PlayoutPoint MovingAveragePoint(
        double value,
        int zeroBasedIndex,
        int count,
        double lineWidth,
        CandleRange range)
    {
        var x = count <= 1
            ? 0f
            : (float)(lineWidth / (count - 1d)) * zeroBasedIndex;
        var y = (float)((value - range.Maximum) / range.Gap * 196d) + 98f;
        return new PlayoutPoint(x, y, 0);
    }

    private static void AddCandleBody(
        ICollection<PlayoutMutation> mutations,
        CandleRowData row,
        int index,
        CandleRange range)
    {
        var open = Normalize(row.Open, range);
        var close = Normalize(row.Close, range);
        var bodyName = "candlebody" + index;
        var barName = "candlebar" + index;
        if (row.Open > row.Close)
        {
            mutations.Add(Crop(bodyName, open, close));
            mutations.Add(new PlayoutSetFaceColor(bodyName, 15, 99, 189));
            mutations.Add(new PlayoutSetFaceColor(barName, 15, 99, 189));
        }
        else if (row.Open < row.Close)
        {
            mutations.Add(Crop(bodyName, close, open));
            mutations.Add(new PlayoutSetFaceColor(bodyName, 170, 0, 0));
            mutations.Add(new PlayoutSetFaceColor(barName, 170, 0, 0));
        }
        else
        {
            mutations.Add(Crop(bodyName, close, open + 1d));
            mutations.Add(new PlayoutSetFaceColor(bodyName, 95, 95, 95));
            mutations.Add(new PlayoutSetFaceColor(barName, 95, 95, 95));
        }
    }

    private static void AddMovingAveragePaths(
        ICollection<PlayoutMutation> mutations,
        CandlePaths paths,
        S8010SceneData data)
    {
        if (!CandleCutAliasMetadata.SupportsMovingAveragePaths(data.CutAlias))
        {
            return;
        }

        mutations.Add(new PlayoutSetPathPoints(
            "ma5",
            data.ShowMovingAverage5 ? paths.MovingAverage5 : Array.Empty<PlayoutPoint>()));
        mutations.Add(new PlayoutSetPathPoints(
            "ma20",
            data.ShowMovingAverage20 ? paths.MovingAverage20 : Array.Empty<PlayoutPoint>()));
        if (CandleCutAliasMetadata.SupportsGuide(data.CutAlias))
        {
            mutations.Add(new PlayoutSetVisible("guide", data.ShowMovingAverage20));
        }

        if (!SupportsMovingAverages(data.MarketTarget))
        {
            if (CandleCutAliasMetadata.SupportsGuide(data.CutAlias))
            {
                mutations.Add(new PlayoutSetVisible("guide", false));
            }
            mutations.Add(new PlayoutSetPathPoints("ma5", Array.Empty<PlayoutPoint>()));
            mutations.Add(new PlayoutSetPathPoints("ma20", Array.Empty<PlayoutPoint>()));
        }
    }

    private static bool UsesIntegerPrices(CandleMarketTarget target) => target is
        CandleMarketTarget.KospiStock or CandleMarketTarget.KosdaqStock;

    private static bool IsIndex(CandleMarketTarget target) => target is
        CandleMarketTarget.KospiIndex or CandleMarketTarget.KosdaqIndex or
        CandleMarketTarget.Kospi200Index or CandleMarketTarget.Krx100Index or
        CandleMarketTarget.FuturesIndex;

    private static bool IsIndustry(CandleMarketTarget target) => target is
        CandleMarketTarget.KospiIndustry or CandleMarketTarget.KosdaqIndustry;

    private static bool SupportsMovingAverages(CandleMarketTarget target) =>
        !IsIndex(target) && !IsIndustry(target) && target != CandleMarketTarget.OverseasIndex;

    private sealed record CandleRange(
        double Minimum,
        double Maximum,
        double Gap,
        double CandleMinimum,
        double CandleMaximum,
        double MaximumVolume);

    private sealed record CandlePaths(
        IReadOnlyList<PlayoutPoint> MovingAverage5,
        IReadOnlyList<PlayoutPoint> MovingAverage20);
}

internal static class CandleQuoteDirection
{
    private static readonly string[] DirectionObjects =
        ["upup", "up", "flat", "down", "downdown"];

    public static void Add(
        ICollection<PlayoutMutation> mutations,
        ScenePriceDirection direction)
    {
        foreach (var name in DirectionObjects)
        {
            mutations.Add(new PlayoutSetVisible(name, false));
        }

        string? active = direction switch
        {
            ScenePriceDirection.LimitUp => "upup",
            ScenePriceDirection.Up => "up",
            ScenePriceDirection.Flat or ScenePriceDirection.Unknown => null,
            ScenePriceDirection.Down => "down",
            ScenePriceDirection.LimitDown => "downdown",
            _ => throw new LegacySceneDataException("Unknown candle quote direction.")
        };
        if (active is not null)
        {
            mutations.Add(new PlayoutSetVisible(active, true));
        }

        var selected = direction switch
        {
            ScenePriceDirection.LimitUp or ScenePriceDirection.Up => 1,
            ScenePriceDirection.Flat or ScenePriceDirection.Unknown => 2,
            ScenePriceDirection.Down or ScenePriceDirection.LimitDown => 3,
            _ => throw new LegacySceneDataException("Unknown candle quote direction.")
        };
        for (var index = 1; index <= 3; index++)
        {
            mutations.Add(new PlayoutSetVisible("bg" + index, index == selected));
        }
    }
}
