// PRE-GENERATED (design session). ***Highest token-sink piece (M7).***
// Custom interactive EQ graph: drag node = Fc(x)/Gain(y), wheel = Q,
// double-click empty = add band, right-click node = delete.
// Compile & visually verify in the dev session. References DansoriEQ.Core.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using DansoriEQ.Core.Eq;

namespace DansoriEQ.App.Controls;

public class EqGraphControl : Control
{
    private const double MinHz = 20, MaxHz = 20000;
    private const double NodeRadius = 6;
    private const double Fs = 48000;
    public const int MaxBands = 40;                 // per-channel band cap (Peace uses 31)
    private double _dbRange = 12;      // +/- dB shown
    private int _dragIndex = -1;
    private int _selectedIndex = -1;
    private double _dragStartGain;     // filter gain at grab (for relative drag)
    private double _dragStartMouseDb;  // pointer dB at grab

    /// <summary>Index of the highlighted band (-1 = none). Setting this redraws.</summary>
    public int SelectedIndex
    {
        get => _selectedIndex;
        set { _selectedIndex = value; InvalidateVisual(); }
    }

    /// <summary>Fired when the user clicks (or starts dragging) a node. Arg = filter index.</summary>
    public event EventHandler<int>? NodeSelected;

    /// <summary>Fired continuously while dragging a node. Arg = filter index being dragged.</summary>
    public event EventHandler<int>? NodeDragging;

    private EqState _state = new();
    public EqState State
    {
        get => _state;
        set { _state = value ?? new EqState(); InvalidateVisual(); }
    }

    /// <summary>Optional target curve points (Hz, dB), drawn dashed.</summary>
    public (double Hz, double Db)[]? TargetCurve { get; set; }

    /// <summary>Raised when the user finishes an edit (drag end / wheel / add / delete).</summary>
    public event EventHandler? EqChanged;

    /// <summary>Current band count (variable, capped at MaxBands).</summary>
    public int BandCount => _state.Filters.Count;

    /// <summary>Add a peaking band. Returns false if already at MaxBands.</summary>
    public bool AddBand(double fc = 1000, double gainDb = 0, double q = 1.0)
    {
        if (_state.Filters.Count >= MaxBands) return false;
        _state.Filters.Add(new Filter { Type = FilterType.Peaking, Fc = fc, GainDb = gainDb, Q = q, Enabled = true });
        InvalidateVisual();
        EqChanged?.Invoke(this, EventArgs.Empty);
        return true;
    }

    /// <summary>Remove all bands.</summary>
    public void ClearBands()
    {
        _state.Filters.Clear();
        InvalidateVisual();
        EqChanged?.Invoke(this, EventArgs.Empty);
    }

    // Theme-swappable brushes (bind to theme resources in M5).
    public Brush GridBrush   { get; set; } = new SolidColorBrush(Color.FromRgb(0x33, 0x33, 0x3b));
    public Brush AxisBrush   { get; set; } = new SolidColorBrush(Color.FromRgb(0x76, 0x7a, 0x83));
    public Brush CurveBrush  { get; set; } = new SolidColorBrush(Color.FromRgb(0x4c, 0xc2, 0xff));
    public Brush FillBrush   { get; set; } = new SolidColorBrush(Color.FromArgb(0x40, 0x4c, 0xc2, 0xff));
    public Brush TargetBrush { get; set; } = new SolidColorBrush(Color.FromRgb(0x5a, 0x5f, 0x6b));
    public Brush NodeFill    { get; set; } = new SolidColorBrush(Color.FromRgb(0x04, 0x22, 0x2f));
    public Brush LabelBrush  { get; set; } = new SolidColorBrush(Color.FromRgb(0x6a, 0x6f, 0x7b));

    /// <summary>Show Hz / dB axis labels drawn at their true (log/linear) positions.</summary>
    public bool ShowAxisLabels { get; set; } = true;

    private static readonly Typeface LabelTypeface =
        new(new FontFamily("Segoe UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);

    public EqGraphControl()
    {
        Focusable = true;
        Background = Brushes.Transparent;
    }

    // ---- coordinate mapping ----
    private double HzToX(double hz)
    {
        double t = (Math.Log10(hz) - Math.Log10(MinHz)) / (Math.Log10(MaxHz) - Math.Log10(MinHz));
        return t * ActualWidth;
    }
    private double XToHz(double x)
    {
        double t = Math.Clamp(x / Math.Max(1, ActualWidth), 0, 1);
        return Math.Pow(10, Math.Log10(MinHz) + t * (Math.Log10(MaxHz) - Math.Log10(MinHz)));
    }
    private double DbToY(double db)
    {
        double t = (db + _dbRange) / (2 * _dbRange);
        return (1 - t) * ActualHeight;
    }
    private double YToDb(double y)
    {
        double t = 1 - Math.Clamp(y / Math.Max(1, ActualHeight), 0, 1);
        return t * 2 * _dbRange - _dbRange;
    }

    // ---- rendering ----
    protected override void OnRender(DrawingContext dc)
    {
        double w = ActualWidth, h = ActualHeight;
        if (w <= 0 || h <= 0) return;

        dc.DrawRectangle(Background, null, new Rect(0, 0, w, h));

        var gridPen = new Pen(GridBrush, 1);
        foreach (var db in new[] { -12.0, -6, 0, 6, 12 })
        {
            double y = DbToY(db);
            dc.DrawLine(gridPen, new Point(0, y), new Point(w, y));
            if (ShowAxisLabels && db != 0)
                DrawLabel(dc, (db > 0 ? "+" : "") + db.ToString("0"), 3, y - 6);
        }
        foreach (var hz in new[] { 50.0, 100, 200, 500, 1000, 2000, 5000, 10000 })
        {
            double x = HzToX(hz);
            dc.DrawLine(gridPen, new Point(x, 0), new Point(x, h));
            if (ShowAxisLabels)
            {
                string label = hz >= 1000 ? $"{hz / 1000:0.#}k" : $"{hz:0}";
                DrawLabel(dc, label, x + 3, h - 15);
            }
        }

        if (TargetCurve is { Length: > 1 })
            DrawCurve(dc, new Pen(TargetBrush, 1.5) { DashStyle = DashStyles.Dash }, null, TargetCurve);

        // Draw curve without preamp — shows filter shape only; preamp is displayed as text label
        var shapeState = new EqState { Filters = _state.Filters, PreampDb = 0 };
        var pts = FrequencyResponse.Sample(shapeState, Math.Max(64, (int)(w / 3)), Fs);
        DrawCurve(dc, new Pen(CurveBrush, 2.5), FillBrush, pts);

        // Draw selected filter's individual response as dashed accent overlay
        if (_selectedIndex >= 0 && _selectedIndex < _state.Filters.Count)
        {
            var sel = _state.Filters[_selectedIndex];
            var selState = new EqState { PreampDb = 0, Filters = [sel.Clone()] };
            var selPts = FrequencyResponse.Sample(selState, Math.Max(64, (int)(w / 3)), Fs);
            var dashPen = new Pen(new SolidColorBrush(Color.FromArgb(0x80, 0x4C, 0xC2, 0xFF)), 1.5)
                { DashStyle = DashStyles.Dash };
            DrawCurve(dc, dashPen, null, selPts);
        }

        for (int i = 0; i < _state.Filters.Count; i++)
        {
            var f = _state.Filters[i];
            // Place the node ON the summed curve at its Fc so it never floats off the line.
            var p = new Point(HzToX(f.Fc), DbToY(ShapeDbAt(f.Fc)));

            if (i == _selectedIndex)
            {
                // Selected node: larger, bright accent fill, white outline
                dc.DrawEllipse(new SolidColorBrush(Color.FromRgb(0x4C, 0xC2, 0xFF)),
                               new Pen(Brushes.White, 2),
                               p, NodeRadius + 4, NodeRadius + 4);
            }
            else
            {
                var pen = new Pen(f.Enabled ? CurveBrush : AxisBrush, 2.5);
                dc.DrawEllipse(NodeFill, pen, p, NodeRadius, NodeRadius);
            }
        }
    }

    /// <summary>Summed EQ response (dB) at frequency hz, excluding preamp — i.e. the value of the
    /// solid curve the user sees. Nodes are placed on this so every dot sits on the curve.</summary>
    private double ShapeDbAt(double hz)
    {
        double sum = 0;
        foreach (var band in _state.Filters)
            sum += band.GainDbAt(hz, Fs);
        return sum;
    }

    private void DrawLabel(DrawingContext dc, string text, double x, double y)
    {
        var ft = new FormattedText(text, CultureInfo.InvariantCulture, FlowDirection.LeftToRight,
            LabelTypeface, 10, LabelBrush, VisualTreeHelper.GetDpi(this).PixelsPerDip);
        dc.DrawText(ft, new Point(x, y));
    }

    private void DrawCurve(DrawingContext dc, Pen pen, Brush? fill, (double Hz, double Db)[] pts)
    {
        var geo = new StreamGeometry();
        using (var ctx = geo.Open())
        {
            ctx.BeginFigure(new Point(HzToX(pts[0].Hz), DbToY(pts[0].Db)), fill != null, false);
            for (int i = 1; i < pts.Length; i++)
                ctx.LineTo(new Point(HzToX(pts[i].Hz), DbToY(pts[i].Db)), true, false);
            if (fill != null)
            {
                ctx.LineTo(new Point(HzToX(pts[^1].Hz), ActualHeight), false, false);
                ctx.LineTo(new Point(HzToX(pts[0].Hz), ActualHeight), false, false);
            }
        }
        geo.Freeze();
        dc.DrawGeometry(fill, pen, geo);
    }

    // ---- hit testing ----
    private int HitNode(Point p)
    {
        for (int i = 0; i < _state.Filters.Count; i++)
        {
            var f = _state.Filters[i];
            var np = new Point(HzToX(f.Fc), DbToY(ShapeDbAt(f.Fc)));
            if ((np - p).Length <= NodeRadius + 6) return i;
        }
        return -1;
    }

    // ---- interaction ----
    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        var pos = e.GetPosition(this);
        _dragIndex = HitNode(pos);
        if (_dragIndex >= 0)
        {
            if (_selectedIndex != _dragIndex)
                _selectedIndex = _dragIndex;
            // Relative drag: remember gain + pointer level at grab so vertical motion adds to gain.
            _dragStartGain     = _state.Filters[_dragIndex].GainDb;
            _dragStartMouseDb  = YToDb(pos.Y);
            NodeSelected?.Invoke(this, _dragIndex);
            CaptureMouse();
        }
        base.OnMouseLeftButtonDown(e);
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        if (_dragIndex >= 0 && IsMouseCaptured)
        {
            var p = e.GetPosition(this);
            var f = _state.Filters[_dragIndex];

            // Horizontal → Fc (all types).
            f.Fc = Math.Clamp(XToHz(p.X), MinHz, MaxHz);

            // Vertical → gain, but only for types that have gain. Relative so the node
            // (drawn on the summed curve) tracks the pointer without jumping.
            if (f.Type.UsesGain())
            {
                double delta = YToDb(p.Y) - _dragStartMouseDb;
                f.GainDb = Math.Clamp(_dragStartGain + delta, -_dbRange, _dbRange);
            }

            InvalidateVisual();
            NodeDragging?.Invoke(this, _dragIndex);
        }
        base.OnMouseMove(e);
    }

    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        if (_dragIndex >= 0)
        {
            _dragIndex = -1;
            ReleaseMouseCapture();
            EqChanged?.Invoke(this, EventArgs.Empty);   // apply to APO here (debounced by caller)
        }
        base.OnMouseLeftButtonUp(e);
    }

    protected override void OnMouseWheel(MouseWheelEventArgs e)
    {
        int i = HitNode(e.GetPosition(this));
        if (i >= 0)
        {
            var f = _state.Filters[i];
            double factor = e.Delta > 0 ? 1.1 : 1 / 1.1;
            f.Q = Math.Clamp(f.Q * factor, 0.3, 6.0);
            InvalidateVisual();
            EqChanged?.Invoke(this, EventArgs.Empty);
            e.Handled = true;
        }
        base.OnMouseWheel(e);
    }

    protected override void OnMouseDoubleClick(MouseButtonEventArgs e)
    {
        var p = e.GetPosition(this);
        if (HitNode(p) < 0 && _state.Filters.Count < MaxBands)
        {
            _state.Filters.Add(new Filter
            {
                Type = FilterType.Peaking,
                Fc = XToHz(p.X),
                GainDb = YToDb(p.Y),
                Q = 1.0,
                Enabled = true
            });
            InvalidateVisual();
            EqChanged?.Invoke(this, EventArgs.Empty);
        }
        base.OnMouseDoubleClick(e);
    }

    protected override void OnMouseRightButtonUp(MouseButtonEventArgs e)
    {
        int i = HitNode(e.GetPosition(this));
        if (i >= 0)
        {
            _state.Filters.RemoveAt(i);
            InvalidateVisual();
            EqChanged?.Invoke(this, EventArgs.Empty);
            e.Handled = true;
        }
        base.OnMouseRightButtonUp(e);
    }
}
