using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace QuanTAlib;

/// <summary>
/// YZVAMA: Yang-Zhang Volatility Adjusted Moving Average
/// </summary>
/// <remarks>
/// YZVAMA adjusts the SMA length based on the percentile rank of short-term
/// Yang-Zhang volatility (YZV) observed over a rolling lookback window.
///
/// Calculation (per bar):
/// 1) Compute Yang-Zhang daily variance proxy from OHLC (log returns).
/// 2) Smooth variance with bias-compensated RMA for short and long periods (sqrt -> volatility).
/// 3) Compute percentile rank of current short YZV within the lookback window.
/// 4) Map percentile to adjusted SMA length: higher volatility -> shorter length.
/// 5) Output SMA(source, adjusted_length) over a circular buffer.
/// </remarks>
[SkipLocalsInit]
public sealed class Yzvama : AbstractBase
{
    [StructLayout(LayoutKind.Auto)]
    private record struct RmaState(double Ema, double E, bool IsCompensated);

    [StructLayout(LayoutKind.Auto)]
    private record struct YzvamaState(
        RmaState ShortVar,
        RmaState LongVar,
        double PrevClose,
        int SourceHead,
        double SourceSum,
        int SourceValidCount,
        int YzvHead,
        int YzvCount,
        bool IsInitialized)
    {
        public static YzvamaState New() => new()
        {
            ShortVar = new RmaState(Ema: 0, E: 1.0, IsCompensated: false),
            LongVar = new RmaState(Ema: 0, E: 1.0, IsCompensated: false),
            PrevClose = double.NaN,
            SourceHead = 0,
            SourceSum = 0,
            SourceValidCount = 0,
            YzvHead = 0,
            YzvCount = 0,
            IsInitialized = false
        };
    }

    private readonly int _percentileLookback;
    private readonly int _minLength;
    private readonly int _maxLength;

    private readonly double _shortAlpha;
    private readonly double _longAlpha;
    private readonly double _shortDecay;
    private readonly double _longDecay;
    private readonly double _kShort;
    private readonly double _kLong;

    private YzvamaState _state;
    private YzvamaState _p_state;

    // Dual-buffer approach for O(1) state transitions instead of O(n) Array.Copy
    private double[] _activeSourceBuffer;
    private double[] _backupSourceBuffer;
    private double[] _activeYzvBuffer;
    private double[] _backupYzvBuffer;

    // Sorted YZV buffer for O(n) insert/remove instead of O(n log n) sort
    private double[] _activeSortedYzv;
    private double[] _backupSortedYzv;

    private double _lastValidSource;
    private double _p_lastValidSource;

    private const double EPSILON = 1e-10;

    /// <summary>
    /// Creates YZVAMA with specified parameters.
    /// </summary>
    /// <param name="yzvShortPeriod">Short-term YZV period for current volatility (default: 3)</param>
    /// <param name="yzvLongPeriod">Long-term YZV period for baseline volatility (default: 50)</param>
    /// <param name="percentileLookback">Lookback window for percentile calculation (default: 100)</param>
    /// <param name="minLength">Minimum allowed adjusted length (default: 5)</param>
    /// <param name="maxLength">Maximum allowed adjusted length (default: 100)</param>
    public Yzvama(int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100)
    {
        if (yzvShortPeriod <= 0)
        {
            throw new ArgumentException("Short YZV period must be greater than 0", nameof(yzvShortPeriod));
        }

        if (yzvLongPeriod <= 0)
        {
            throw new ArgumentException("Long YZV period must be greater than 0", nameof(yzvLongPeriod));
        }

        if (percentileLookback <= 0)
        {
            throw new ArgumentException("Percentile lookback must be greater than 0", nameof(percentileLookback));
        }

        if (minLength <= 0)
        {
            throw new ArgumentException("Min length must be greater than 0", nameof(minLength));
        }

        if (maxLength <= 0)
        {
            throw new ArgumentException("Max length must be greater than 0", nameof(maxLength));
        }

        if (minLength > maxLength)
        {
            throw new ArgumentException("Min length must be less than or equal to max length", nameof(minLength));
        }

        _percentileLookback = percentileLookback;
        _minLength = minLength;
        _maxLength = maxLength;

        _shortAlpha = 1.0 / yzvShortPeriod;
        _longAlpha = 1.0 / yzvLongPeriod;
        _shortDecay = 1.0 - _shortAlpha;
        _longDecay = 1.0 - _longAlpha;

        _kShort = ComputeYangZhangK(yzvShortPeriod);
        _kLong = ComputeYangZhangK(yzvLongPeriod);

        // Dual buffers for pointer-swap on state transitions
        _activeSourceBuffer = new double[maxLength];
        _backupSourceBuffer = new double[maxLength];
        Array.Fill(_activeSourceBuffer, double.NaN);
        Array.Fill(_backupSourceBuffer, double.NaN);

        _activeYzvBuffer = new double[percentileLookback];
        _backupYzvBuffer = new double[percentileLookback];
        Array.Fill(_activeYzvBuffer, double.NaN);
        Array.Fill(_backupYzvBuffer, double.NaN);

        // Sorted buffer maintained incrementally
        _activeSortedYzv = new double[percentileLookback];
        _backupSortedYzv = new double[percentileLookback];
        Array.Fill(_activeSortedYzv, double.NaN);
        Array.Fill(_backupSortedYzv, double.NaN);

        _state = YzvamaState.New();
        _p_state = _state;

        // Initialize last valid source to NaN to preserve invalid state until a valid input arrives
        _lastValidSource = double.NaN;
        _p_lastValidSource = double.NaN;

        Name = $"Yzvama({yzvShortPeriod},{yzvLongPeriod},{percentileLookback},{minLength},{maxLength})";
        WarmupPeriod = Math.Max(Math.Max(yzvLongPeriod, maxLength), percentileLookback);
    }

    /// <summary>
    /// Creates YZVAMA with specified source and parameters.
    /// Subscribes to source.Pub event.
    /// </summary>
    public Yzvama(ITValuePublisher source, int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100)
        : this(yzvShortPeriod, yzvLongPeriod, percentileLookback, minLength, maxLength)
    {
        source.Pub += Handle;
    }

    /// <summary>
    /// True if the YZVAMA has warmed up and is providing valid results.
    /// </summary>
    public override bool IsHot => _state.SourceValidCount >= _minLength && _state.IsInitialized;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static double ComputeYangZhangK(int period)
    {
        if (period <= 1)
        {
            return 0.34 / (1.34 + 1.0);
        }

        double ratioN = (period + 1.0) / (period - 1.0);
        return 0.34 / (1.34 + ratioN);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static int LowerBound(ReadOnlySpan<double> sorted, int length, double value)
    {
        int lo = 0;
        int hi = length;
        while (lo < hi)
        {
            int mid = lo + ((hi - lo) >> 1);
            if (sorted[mid] < value)
            {
                lo = mid + 1;
            }
            else
            {
                hi = mid;
            }
        }
        return lo;
    }

    /// <summary>
    /// Inserts a value into the sorted buffer at the correct position.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void InsertSorted(double value, int currentCount)
    {
        int insertPos = LowerBound(_activeSortedYzv, currentCount, value);
        if (insertPos < currentCount)
        {
            Array.Copy(_activeSortedYzv, insertPos, _activeSortedYzv, insertPos + 1, currentCount - insertPos);
        }
        _activeSortedYzv[insertPos] = value;
    }

    /// <summary>
    /// Removes a value from the sorted buffer.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void RemoveSorted(double value, int currentCount)
    {
        int removePos = LowerBound(_activeSortedYzv, currentCount, value);
        if (removePos < currentCount && Math.Abs(_activeSortedYzv[removePos] - value) < EPSILON && removePos < currentCount - 1)
        {
            Array.Copy(_activeSortedYzv, removePos + 1, _activeSortedYzv, removePos, currentCount - 1 - removePos);
        }
    }

    /// <summary>
    /// Updates YZVAMA with a TBar input (uses OHLC for YZV, Close as source).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public TValue Update(TBar input, bool isNew = true) => Update(input, input.Close, isNew);

    /// <summary>
    /// Updates YZVAMA with a TBar input (uses OHLC for YZV and provided source for SMA).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public TValue Update(TBar input, double sourceValue, bool isNew = true)
    {
        if (isNew)
        {
            // Save state - pointer swap instead of O(n) Array.Copy
            _p_state = _state;
            _p_lastValidSource = _lastValidSource;

            // Swap buffer references for backup
            (_activeSourceBuffer, _backupSourceBuffer) = (_backupSourceBuffer, _activeSourceBuffer);
            (_activeYzvBuffer, _backupYzvBuffer) = (_backupYzvBuffer, _activeYzvBuffer);
            (_activeSortedYzv, _backupSortedYzv) = (_backupSortedYzv, _activeSortedYzv);

            // Copy current state to active buffer (only needed for first update after swap)
            Array.Copy(_backupSourceBuffer, _activeSourceBuffer, _maxLength);
            Array.Copy(_backupYzvBuffer, _activeYzvBuffer, _percentileLookback);
            Array.Copy(_backupSortedYzv, _activeSortedYzv, _percentileLookback);
        }
        else
        {
            // Restore state - pointer swap back
            _state = _p_state;
            _lastValidSource = _p_lastValidSource;

            // Swap back to restore backup as active
            (_activeSourceBuffer, _backupSourceBuffer) = (_backupSourceBuffer, _activeSourceBuffer);
            (_activeYzvBuffer, _backupYzvBuffer) = (_backupYzvBuffer, _activeYzvBuffer);
            (_activeSortedYzv, _backupSortedYzv) = (_backupSortedYzv, _activeSortedYzv);
        }

        // Sanitize source
        if (!double.IsFinite(sourceValue))
        {
            sourceValue = double.IsFinite(_lastValidSource) ? _lastValidSource : 0.0;
        }
        else
        {
            _lastValidSource = sourceValue;
        }

        // Compute Yang-Zhang variance components (log returns)
        double yzvShort = double.NaN;
        bool canComputeVol = double.IsFinite(input.Open) && double.IsFinite(input.High) && double.IsFinite(input.Low) && double.IsFinite(input.Close)
            && input.Open > 0 && input.High > 0 && input.Low > 0 && input.Close > 0;

        var shortVar = _state.ShortVar;
        var longVar = _state.LongVar;
        double prevClose = _state.PrevClose;

        if (canComputeVol)
        {
            double pc = (!double.IsFinite(prevClose) || prevClose <= 0) ? input.Open : prevClose;

            if (pc > 0)
            {
                double ro = Math.Log(input.Open / pc);
                double rc = Math.Log(input.Close / input.Open);
                double rh = Math.Log(input.High / input.Open);
                double rl = Math.Log(input.Low / input.Open);

                double sOSq = ro * ro;
                double sCSq = rc * rc;
                double sRsSq = rh * (rh - rc) + rl * (rl - rc);

                // Use FMA for sSqDailyShort and sSqDailyLong computations
                // Original: sOSq + _kShort * sCSq + (1.0 - _kShort) * sRsSq
                double sSqDailyShort = Math.FusedMultiplyAdd(_kShort, sCSq, Math.FusedMultiplyAdd(1.0 - _kShort, sRsSq, sOSq));
                double sSqDailyLong = Math.FusedMultiplyAdd(_kLong, sCSq, Math.FusedMultiplyAdd(1.0 - _kLong, sRsSq, sOSq));

                // Update short RMA variance
                shortVar.Ema = Math.FusedMultiplyAdd(shortVar.Ema, _shortDecay, _shortAlpha * sSqDailyShort);
                shortVar.E *= _shortDecay;
                if (shortVar.E <= EPSILON)
                {
                    shortVar.IsCompensated = true;
                }

                double shortVarValue = shortVar.IsCompensated ? shortVar.Ema : shortVar.Ema / (1.0 - shortVar.E);
                yzvShort = shortVarValue >= 0 ? Math.Sqrt(shortVarValue) : double.NaN;

                // Update long RMA variance (kept for parity with Pine implementation)
                longVar.Ema = Math.FusedMultiplyAdd(longVar.Ema, _longDecay, _longAlpha * sSqDailyLong);
                longVar.E *= _longDecay;
                if (longVar.E <= EPSILON)
                {
                    longVar.IsCompensated = true;
                }
            }
        }

        // Update YZV percentile buffer with incremental sorted maintenance
        int yzvHead = _state.YzvHead;
        int yzvCount = _state.YzvCount;

        if (double.IsFinite(yzvShort))
        {
            // If buffer is full, remove the oldest value from sorted
            if (yzvCount == _percentileLookback && double.IsFinite(_activeYzvBuffer[yzvHead]))
            {
                RemoveSorted(_activeYzvBuffer[yzvHead], yzvCount);
                yzvCount--;
            }

            // Insert new value into sorted buffer
            InsertSorted(yzvShort, yzvCount);
            yzvCount++;

            _activeYzvBuffer[yzvHead] = yzvShort;
            yzvHead = (yzvHead + 1) % _percentileLookback;
        }

        // Percentile rank of current yzvShort within lookback window using sorted buffer
        double percentileValue = 50.0;
        if (yzvCount > 1 && double.IsFinite(yzvShort))
        {
            int rankPos = LowerBound(_activeSortedYzv, yzvCount, yzvShort);
            percentileValue = (rankPos / (double)(yzvCount - 1)) * 100.0;
        }

        double lengthRange = _maxLength - _minLength;
        // Use FMA for adjustedLengthF: _maxLength - (percentileValue/100.0)*lengthRange
        double adjustedLengthF = Math.FusedMultiplyAdd(percentileValue / 100.0, -lengthRange, _maxLength);
        int adjustedLength = (int)Math.Max(_minLength, Math.Min(_maxLength, adjustedLengthF));

        // Update source circular buffer and rolling sum
        double oldest = _activeSourceBuffer[_state.SourceHead];
        int validCount = _state.SourceValidCount;
        double sourceSum = _state.SourceSum;

        if (double.IsFinite(oldest))
        {
            sourceSum -= oldest;
            validCount--;
        }

        if (double.IsFinite(sourceValue))
        {
            sourceSum += sourceValue;
            validCount++;
        }

        _activeSourceBuffer[_state.SourceHead] = sourceValue;
        int newHead = (_state.SourceHead + 1) % _maxLength;

        // Calculate SMA over adjustedLength most recent values
        double result = 0;
        int actualCount = Math.Min(validCount, adjustedLength);
        if (actualCount > 0)
        {
            double partialSum = 0.0;
            int partialCount = 0;
            for (int i = 0; i < actualCount; i++)
            {
                int idx = (newHead - 1 - i + _maxLength) % _maxLength;
                double val = _activeSourceBuffer[idx];
                if (double.IsFinite(val))
                {
                    partialSum += val;
                    partialCount++;
                }
            }

            result = partialCount > 0 ? partialSum / partialCount : sourceValue;
        }
        else
        {
            result = sourceValue;
        }

        _state = new YzvamaState(
            shortVar,
            longVar,
            input.Close,
            newHead,
            sourceSum,
            validCount,
            yzvHead,
            yzvCount,
            IsInitialized: true);

        Last = new TValue(input.Time, result);
        PubEvent(Last, isNew);
        return Last;
    }

    /// <summary>
    /// Updates YZVAMA with a TValue input.
    /// Note: YZVAMA ideally needs OHLC data to compute Yang-Zhang volatility.
    /// When only a single value is provided, a synthetic bar is created (O=H=L=C),
    /// resulting in zero volatility and a tendency toward longer adjusted lengths.
    /// </summary>
    public override TValue Update(TValue input, bool isNew = true)
    {
        var syntheticBar = new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0);
        return Update(syntheticBar, input.Value, isNew);
    }

    /// <summary>
    /// Updates YZVAMA with a TBarSeries (Close as source).
    /// </summary>
    public TSeries Update(TBarSeries source)
    {
        if (source.Count == 0)
        {
            return [];
        }

        int len = source.Count;
        var t = new List<long>(len);
        var v = new List<double>(len);
        CollectionsMarshal.SetCount(t, len);
        CollectionsMarshal.SetCount(v, len);

        var tSpan = CollectionsMarshal.AsSpan(t);
        var vSpan = CollectionsMarshal.AsSpan(v);

        for (int i = 0; i < len; i++)
        {
            var bar = source[i];
            var result = Update(bar, isNew: true);
            tSpan[i] = bar.Time;
            vSpan[i] = result.Value;
        }

        return new TSeries(t, v);
    }

    /// <summary>
    /// Updates YZVAMA with a TSeries (single values).
    /// </summary>
    public override TSeries Update(TSeries source)
    {
        if (source.Count == 0)
        {
            return [];
        }

        int len = source.Count;
        var t = new List<long>(len);
        var v = new List<double>(len);
        CollectionsMarshal.SetCount(t, len);
        CollectionsMarshal.SetCount(v, len);

        var tSpan = CollectionsMarshal.AsSpan(t);
        var vSpan = CollectionsMarshal.AsSpan(v);

        var sourceTimes = source.Times;
        var sourceValues = source.Values;

        for (int i = 0; i < len; i++)
        {
            var result = Update(new TValue(sourceTimes[i], sourceValues[i]), isNew: true);
            tSpan[i] = sourceTimes[i];
            vSpan[i] = result.Value;
        }

        return new TSeries(t, v);
    }

    /// <summary>
    /// Initializes the indicator state using the provided history (lossy - timestamps discarded).
    /// For timestamp-preserving priming, use Prime(ReadOnlySpan&lt;TValue&gt;) overload.
    /// </summary>
    public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
    {
        Reset();
        foreach (double val in source)
        {
            Update(new TValue(DateTime.MinValue, val), isNew: true);
        }
    }

    /// <summary>
    /// Initializes the indicator state using the provided timestamped history.
    /// Preserves original timestamps.
    /// </summary>
    public void Prime(ReadOnlySpan<TValue> source)
    {
        Reset();
        foreach (TValue tv in source)
        {
            Update(tv, isNew: true);
        }
    }

    /// <summary>
    /// Calculates YZVAMA for the entire bar series using a new instance.
    /// </summary>
    public static TSeries Batch(TBarSeries source, int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100)
    {
        var yzvama = new Yzvama(yzvShortPeriod, yzvLongPeriod, percentileLookback, minLength, maxLength);
        return yzvama.Update(source);
    }

    /// <summary>
    /// Calculates YZVAMA for the entire series using a new instance.
    /// </summary>
    public static TSeries Batch(TSeries source, int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100)
    {
        var yzvama = new Yzvama(yzvShortPeriod, yzvLongPeriod, percentileLookback, minLength, maxLength);
        return yzvama.Update(source);
    }

    /// <summary>
    /// Resets the YZVAMA state.
    /// </summary>
    public override void Reset()
    {
        _state = YzvamaState.New();
        _p_state = _state;
        Array.Fill(_activeSourceBuffer, double.NaN);
        Array.Fill(_backupSourceBuffer, double.NaN);
        Array.Fill(_activeYzvBuffer, double.NaN);
        Array.Fill(_backupYzvBuffer, double.NaN);
        Array.Fill(_activeSortedYzv, double.NaN);
        Array.Fill(_backupSortedYzv, double.NaN);
        // Initialize to NaN to preserve invalid state until valid input arrives
        _lastValidSource = double.NaN;
        _p_lastValidSource = double.NaN;
        Last = default;
    }
}
