using System.Drawing.Drawing2D;
using RioJoy.Core.Overlay;

namespace RioJoy.Tray.Editor;

/// <summary>
/// Interactive wallpaper preview: draws the rendered cockpit wallpaper scaled to
/// fit, outlines the template's label cells, and reports clicks in template-space
/// coordinates so the host can select (and cycle through stacked) regions. The
/// wallpaper legitimately stacks cells at the same pixels — the image feeds six
/// chroma-split displays — so selection cycling is the host's job via
/// <see cref="OverlayHitTester"/>.
/// </summary>
internal sealed class WallpaperCanvas : Control
{
    // Grabbing a handle within this many control pixels counts as a hit; a press
    // that moves less than the drag threshold is treated as a click (cell cycling).
    private const float HandleGrabPx = 5f;
    private const int DragThresholdPx = 3;

    private Bitmap? _image;
    private string? _selectedRegionName;
    private bool _showOutlines = true;
    private bool _editMode;
    private OverlayRegion? _hover;

    // In-flight box drag: the grabbed handle, the mouse-down point (control px),
    // the region's rectangle at mouse-down (template px), and whether the press
    // has moved far enough to count as a drag rather than a click.
    private OverlayBoxHandle _dragHandle = OverlayBoxHandle.None;
    private Point _downControl;
    private (double X, double Y, double W, double H) _downRect;
    private bool _dragging;

    public WallpaperCanvas()
    {
        DoubleBuffered = true;
        ResizeRedraw = true;
        BackColor = Color.FromArgb(28, 28, 28);
    }

    /// <summary>The template whose cells are outlined and hit-tested.</summary>
    public OverlayTemplate? Template { get; set; }

    /// <summary>The highlighted cell (the one being edited); null = none.</summary>
    public string? SelectedRegionName
    {
        get => _selectedRegionName;
        set { _selectedRegionName = value; Invalidate(); }
    }

    /// <summary>Outline every cell (helps find unlabelled ones).</summary>
    public bool ShowOutlines
    {
        get => _showOutlines;
        set { _showOutlines = value; Invalidate(); }
    }

    /// <summary>
    /// Box-editing mode: the selected cell grows resize handles and can be dragged;
    /// clicks (without a drag) still select/cycle cells as usual.
    /// </summary>
    public bool EditMode
    {
        get => _editMode;
        set
        {
            _editMode = value;
            if (!value) Cursor = Cursors.Default;
            Invalidate();
        }
    }

    /// <summary>Raised with template-space coordinates when the wallpaper is clicked.</summary>
    public event Action<double, double>? TemplateClicked;

    /// <summary>Raised when the topmost cell under the mouse changes (null = none).</summary>
    public event Action<OverlayRegion?>? HoverChanged;

    /// <summary>Raised while a box drag mutates the selected region's geometry.</summary>
    public event Action? SelectedGeometryChanged;

    /// <summary>Raised when a box drag ends (time to re-render the labels).</summary>
    public event Action? GeometryCommitted;

    /// <summary>Swap in a freshly rendered wallpaper; the canvas owns (and disposes) it.</summary>
    public void SetImage(Bitmap? image)
    {
        _image?.Dispose();
        _image = image;
        Invalidate();
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
            _image?.Dispose();
        base.Dispose(disposing);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        if (_image is null || Fit() is not { } fit)
            return;

        Graphics g = e.Graphics;
        g.InterpolationMode = InterpolationMode.HighQualityBilinear;
        g.PixelOffsetMode = PixelOffsetMode.Half; // keep the scaled image and outlines aligned
        g.DrawImage(_image, new RectangleF(fit.OffsetX, fit.OffsetY, _image.Width * fit.Scale, _image.Height * fit.Scale));

        if (Template is null)
            return;

        if (_showOutlines)
        {
            using var pen = new Pen(Color.FromArgb(90, 200, 200, 200));
            foreach (OverlayRegion r in Template.Regions)
            {
                RectangleF rc = ToControl(r, fit);
                g.DrawRectangle(pen, rc.X, rc.Y, rc.Width, rc.Height);
            }
        }

        if (_selectedRegionName is { } name && Template.FindRegion(name) is { } selected)
        {
            using var pen = new Pen(Color.Gold, 2f);
            RectangleF rc = ToControl(selected, fit);
            g.DrawRectangle(pen, rc.X, rc.Y, rc.Width, rc.Height);

            if (_editMode)
            {
                using var fill = new SolidBrush(Color.Gold);
                foreach (PointF p in HandlePoints(rc))
                    g.FillRectangle(fill, p.X - 3f, p.Y - 3f, 6f, 6f);
            }
        }
    }

    // Corner + edge-midpoint handle positions for the selected box.
    private static PointF[] HandlePoints(RectangleF rc)
    {
        float mx = rc.X + rc.Width / 2f, my = rc.Y + rc.Height / 2f;
        return new[]
        {
            new PointF(rc.Left, rc.Top), new PointF(mx, rc.Top), new PointF(rc.Right, rc.Top),
            new PointF(rc.Left, my), new PointF(rc.Right, my),
            new PointF(rc.Left, rc.Bottom), new PointF(mx, rc.Bottom), new PointF(rc.Right, rc.Bottom),
        };
    }

    protected override void OnMouseDown(MouseEventArgs e)
    {
        base.OnMouseDown(e);
        if (e.Button != MouseButtons.Left || ToTemplate(e.Location) is not { } pt)
            return;

        // In edit mode a press on the selected box starts a (potential) drag; the
        // click-vs-drag call is made on mouse-up so stacked-cell cycling still works.
        if (_editMode && SelectedRegion() is { } region && Fit() is { } fit)
        {
            OverlayBoxHandle handle = OverlayRegionEdit.HitHandle(region, pt.X, pt.Y, HandleGrabPx / fit.Scale);
            if (handle != OverlayBoxHandle.None)
            {
                _dragHandle = handle;
                _downControl = e.Location;
                _downRect = (region.X, region.Y, region.Width, region.Height);
                _dragging = false;
                Capture = true;
                return;
            }
        }

        TemplateClicked?.Invoke(pt.X, pt.Y);
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);

        if (_dragHandle != OverlayBoxHandle.None && Fit() is { } fit)
        {
            if (!_dragging &&
                Math.Abs(e.X - _downControl.X) < DragThresholdPx &&
                Math.Abs(e.Y - _downControl.Y) < DragThresholdPx)
                return;
            _dragging = true;

            if (SelectedRegion() is { } region)
            {
                (region.X, region.Y, region.Width, region.Height) = OverlayRegionEdit.Apply(
                    _downRect.X, _downRect.Y, _downRect.W, _downRect.H,
                    _dragHandle,
                    (e.X - _downControl.X) / fit.Scale,
                    (e.Y - _downControl.Y) / fit.Scale);
                SelectedGeometryChanged?.Invoke();
                Invalidate();
            }
            return;
        }

        UpdateEditCursor(e.Location);

        OverlayRegion? hover = null;
        if (Template is not null && ToTemplate(e.Location) is { } pt)
            hover = OverlayHitTester.RegionsAt(Template, pt.X, pt.Y).FirstOrDefault();

        if (!ReferenceEquals(hover, _hover))
        {
            _hover = hover;
            HoverChanged?.Invoke(hover);
        }
    }

    protected override void OnMouseUp(MouseEventArgs e)
    {
        base.OnMouseUp(e);
        if (_dragHandle == OverlayBoxHandle.None)
            return;

        bool dragged = _dragging;
        _dragHandle = OverlayBoxHandle.None;
        _dragging = false;
        Capture = false;

        if (dragged)
            GeometryCommitted?.Invoke();
        else if (ToTemplate(e.Location) is { } pt)
            TemplateClicked?.Invoke(pt.X, pt.Y); // a plain click: select/cycle as usual
    }

    private OverlayRegion? SelectedRegion() =>
        _selectedRegionName is { } name ? Template?.FindRegion(name) : null;

    // Resize/move cursor feedback while hovering the selected box in edit mode.
    private void UpdateEditCursor(Point location)
    {
        if (!_editMode)
            return;

        OverlayBoxHandle handle = OverlayBoxHandle.None;
        if (SelectedRegion() is { } region && Fit() is { } fit && ToTemplate(location) is { } pt)
            handle = OverlayRegionEdit.HitHandle(region, pt.X, pt.Y, HandleGrabPx / fit.Scale);

        Cursor = handle switch
        {
            OverlayBoxHandle.Move => Cursors.SizeAll,
            OverlayBoxHandle.North or OverlayBoxHandle.South => Cursors.SizeNS,
            OverlayBoxHandle.East or OverlayBoxHandle.West => Cursors.SizeWE,
            OverlayBoxHandle.NorthWest or OverlayBoxHandle.SouthEast => Cursors.SizeNWSE,
            OverlayBoxHandle.NorthEast or OverlayBoxHandle.SouthWest => Cursors.SizeNESW,
            _ => Cursors.Default,
        };
    }

    // Scale-to-fit mapping between template pixels and control pixels (letterboxed).
    private (float Scale, float OffsetX, float OffsetY)? Fit()
    {
        int iw = _image?.Width ?? Template?.Width ?? 0;
        int ih = _image?.Height ?? Template?.Height ?? 0;
        if (iw <= 0 || ih <= 0 || Width <= 0 || Height <= 0)
            return null;

        float scale = Math.Min((float)Width / iw, (float)Height / ih);
        return (scale, (Width - iw * scale) / 2f, (Height - ih * scale) / 2f);
    }

    private static RectangleF ToControl(OverlayRegion r, (float Scale, float OffsetX, float OffsetY) fit) =>
        new(
            (float)(fit.OffsetX + r.X * fit.Scale),
            (float)(fit.OffsetY + r.Y * fit.Scale),
            (float)(r.Width * fit.Scale),
            (float)(r.Height * fit.Scale));

    private (double X, double Y)? ToTemplate(Point p)
    {
        if (Fit() is not { } fit)
            return null;

        double x = (p.X - fit.OffsetX) / fit.Scale;
        double y = (p.Y - fit.OffsetY) / fit.Scale;
        int iw = _image?.Width ?? Template?.Width ?? 0;
        int ih = _image?.Height ?? Template?.Height ?? 0;
        return x >= 0 && x < iw && y >= 0 && y < ih ? (x, y) : null;
    }
}
