using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Minint.Core.Models;
using Minint.ViewModels;

namespace Minint.Controls;

public class PixelCanvas : Control
{
    #region Styled Properties

    public static readonly StyledProperty<WriteableBitmap?> SourceBitmapProperty =
        AvaloniaProperty.Register<PixelCanvas, WriteableBitmap?>(nameof(SourceBitmap));

    public static readonly StyledProperty<bool> ShowGridProperty =
        AvaloniaProperty.Register<PixelCanvas, bool>(nameof(ShowGrid), defaultValue: false);

    public WriteableBitmap? SourceBitmap
    {
        get => GetValue(SourceBitmapProperty);
        set => SetValue(SourceBitmapProperty, value);
    }

    public bool ShowGrid
    {
        get => GetValue(ShowGridProperty);
        set => SetValue(ShowGridProperty, value);
    }

    #endregion

    #region Events for tool interaction

    public event Action<int, int>? ToolDown;
    public event Action<int, int>? ToolDrag;
    public event Action<(int X, int Y)?>? CursorPixelChanged;
    public Func<List<(int X, int Y)>?>? GetPreviewMask { get; set; }

    // Selection events
    public event Action<int, int>? SelectionStart;
    public event Action<int, int>? SelectionUpdate;
    public event Action<int, int>? SelectionEnd;

    // Paste events
    public event Action<int, int>? PasteMoved;
    public event Action? PasteCommitted;
    public event Action? PasteCancelled;

    /// <summary>Provides the current EditorViewModel for reading selection/paste state during render.</summary>
    public EditorViewModel? Editor { get; set; }

    #endregion

    private readonly Viewport _viewport = new();
    private bool _isPanning;
    private bool _isDrawing;
    private bool _isSelecting;
    private Point _panStart;
    private double _panStartOffsetX, _panStartOffsetY;
    private bool _viewportInitialized;
    private int _lastBitmapWidth;
    private int _lastBitmapHeight;
    private (int X, int Y)? _lastCursorPixel;
    private Point? _lastScreenPos;

    private ScrollBar? _hScrollBar;
    private ScrollBar? _vScrollBar;
    private bool _suppressScrollSync;

    private const double ScrollPixelsPerTick = 20.0;

    public Viewport Viewport => _viewport;

    static PixelCanvas()
    {
        AffectsRender<PixelCanvas>(SourceBitmapProperty, ShowGridProperty);
        FocusableProperty.OverrideDefaultValue<PixelCanvas>(true);
    }

    public PixelCanvas()
    {
        ClipToBounds = true;
    }

    public void AttachScrollBars(ScrollBar horizontal, ScrollBar vertical)
    {
        if (_hScrollBar is not null) _hScrollBar.ValueChanged -= OnHScrollChanged;
        if (_vScrollBar is not null) _vScrollBar.ValueChanged -= OnVScrollChanged;
        _hScrollBar = horizontal;
        _vScrollBar = vertical;
        _hScrollBar.ValueChanged += OnHScrollChanged;
        _vScrollBar.ValueChanged += OnVScrollChanged;
    }

    #region Rendering

    public override void Render(DrawingContext context)
    {
        base.Render(context);
        context.FillRectangle(Brushes.Transparent, new Rect(Bounds.Size));

        var bmp = SourceBitmap;
        if (bmp is null) return;

        int imgW = bmp.PixelSize.Width;
        int imgH = bmp.PixelSize.Height;

        if (!_viewportInitialized)
        {
            _viewport.FitToView(imgW, imgH, Bounds.Width, Bounds.Height);
            _viewportInitialized = true;
        }

        DrawCheckerboard(context, imgW, imgH);

        var destRect = _viewport.ImageScreenRect(imgW, imgH);
        var srcRect = new Rect(0, 0, imgW, imgH);
        RenderOptions.SetBitmapInterpolationMode(this, BitmapInterpolationMode.None);
        context.DrawImage(bmp, srcRect, destRect);

        if (ShowGrid)
            DrawPixelGrid(context, imgW, imgH);

        DrawToolPreview(context, imgW, imgH);
        DrawSelectionOverlay(context);
        DrawPastePreview(context);

        int w = imgW, h = imgH;
        Dispatcher.UIThread.Post(() => SyncScrollBars(w, h), DispatcherPriority.Render);
    }

    private void DrawCheckerboard(DrawingContext context, int imgW, int imgH)
    {
        var rect = _viewport.ImageScreenRect(imgW, imgH);
        var clip = new Rect(0, 0, Bounds.Width, Bounds.Height);
        var visible = rect.Intersect(clip);
        if (visible.Width <= 0 || visible.Height <= 0) return;

        const int checkerSize = 8;
        var light = new SolidColorBrush(Color.FromRgb(204, 204, 204));
        var dark = new SolidColorBrush(Color.FromRgb(170, 170, 170));

        using (context.PushClip(visible))
        {
            context.FillRectangle(light, visible);
            double startX = visible.X - ((visible.X - rect.X) % (checkerSize * 2));
            double startY = visible.Y - ((visible.Y - rect.Y) % (checkerSize * 2));
            for (double y = startY; y < visible.Bottom; y += checkerSize)
            {
                for (double x = startX; x < visible.Right; x += checkerSize)
                {
                    int col = (int)((x - rect.X) / checkerSize);
                    int row = (int)((y - rect.Y) / checkerSize);
                    if ((col + row) % 2 == 1)
                        context.FillRectangle(dark, new Rect(x, y, checkerSize, checkerSize));
                }
            }
        }
    }

    private void DrawPixelGrid(DrawingContext context, int imgW, int imgH)
    {
        double zoom = _viewport.Zoom;
        if (zoom < 4) return;

        var pen = new Pen(Brushes.Black, 1);

        var clip = new Rect(0, 0, Bounds.Width, Bounds.Height);
        var imgRect = _viewport.ImageScreenRect(imgW, imgH);
        var visible = imgRect.Intersect(clip);
        if (visible.Width <= 0 || visible.Height <= 0) return;

        var (startPx, startPy) = _viewport.ScreenToPixel(visible.X, visible.Y);
        var (endPx, endPy) = _viewport.ScreenToPixel(visible.Right, visible.Bottom);
        startPx = Math.Max(0, startPx);
        startPy = Math.Max(0, startPy);
        endPx = Math.Min(imgW, endPx + 1);
        endPy = Math.Min(imgH, endPy + 1);

        using (context.PushClip(visible))
        {
            for (int px = startPx; px <= endPx; px++)
            {
                var (sx, _) = _viewport.PixelToScreen(px, 0);
                double x = Math.Floor(sx) + 0.5;
                context.DrawLine(pen, new Point(x, visible.Top), new Point(x, visible.Bottom));
            }
            for (int py = startPy; py <= endPy; py++)
            {
                var (_, sy) = _viewport.PixelToScreen(0, py);
                double y = Math.Floor(sy) + 0.5;
                context.DrawLine(pen, new Point(visible.Left, y), new Point(visible.Right, y));
            }
        }
    }

    private void DrawToolPreview(DrawingContext context, int imgW, int imgH)
    {
        var mask = GetPreviewMask?.Invoke();
        if (mask is null || mask.Count == 0) return;

        double zoom = _viewport.Zoom;
        var previewBrush = new SolidColorBrush(Color.FromArgb(80, 255, 255, 255));
        var outlinePen = new Pen(new SolidColorBrush(Color.FromArgb(160, 255, 255, 255)), 1);

        var clip = new Rect(0, 0, Bounds.Width, Bounds.Height);
        using (context.PushClip(clip))
        {
            foreach (var (px, py) in mask)
            {
                var (sx, sy) = _viewport.PixelToScreen(px, py);
                context.FillRectangle(previewBrush, new Rect(sx, sy, zoom, zoom));
            }

            if (mask.Count > 0)
            {
                int minX = mask[0].X, maxX = mask[0].X;
                int minY = mask[0].Y, maxY = mask[0].Y;
                foreach (var (px, py) in mask)
                {
                    if (px < minX) minX = px;
                    if (px > maxX) maxX = px;
                    if (py < minY) minY = py;
                    if (py > maxY) maxY = py;
                }
                var (ox, oy) = _viewport.PixelToScreen(minX, minY);
                context.DrawRectangle(outlinePen, new Rect(ox, oy, (maxX - minX + 1) * zoom, (maxY - minY + 1) * zoom));
            }
        }
    }

    private void DrawSelectionOverlay(DrawingContext context)
    {
        var sel = Editor?.SelectionRectNormalized;
        if (sel is null) return;

        var (sx, sy, sw, sh) = sel.Value;
        double zoom = _viewport.Zoom;
        var (screenX, screenY) = _viewport.PixelToScreen(sx, sy);
        var rect = new Rect(screenX, screenY, sw * zoom, sh * zoom);

        var fillBrush = new SolidColorBrush(Color.FromArgb(40, 100, 150, 255));
        context.FillRectangle(fillBrush, rect);

        var borderPen = new Pen(new SolidColorBrush(Color.FromArgb(200, 100, 150, 255)), 1,
            new DashStyle([4, 4], 0));
        context.DrawRectangle(borderPen, rect);
    }

    private void DrawPastePreview(DrawingContext context)
    {
        if (Editor is null || !Editor.IsPasting || Editor.Clipboard is null)
            return;

        var pos = Editor.PastePosition!.Value;
        var frag = Editor.Clipboard;
        double zoom = _viewport.Zoom;

        var clip = new Rect(0, 0, Bounds.Width, Bounds.Height);
        using (context.PushClip(clip))
        {
            for (int fy = 0; fy < frag.Height; fy++)
            {
                for (int fx = 0; fx < frag.Width; fx++)
                {
                    var color = frag.Pixels[fy * frag.Width + fx];
                    if (color.A == 0) continue;

                    var (sx, sy) = _viewport.PixelToScreen(pos.X + fx, pos.Y + fy);
                    byte dispA = (byte)(color.A * 180 / 255); // semi-transparent preview
                    var brush = new SolidColorBrush(Color.FromArgb(dispA, color.R, color.G, color.B));
                    context.FillRectangle(brush, new Rect(sx, sy, zoom, zoom));
                }
            }

            // Border around the floating fragment
            var (ox, oy) = _viewport.PixelToScreen(pos.X, pos.Y);
            var borderPen = new Pen(new SolidColorBrush(Color.FromArgb(200, 255, 200, 50)), 1,
                new DashStyle([3, 3], 0));
            context.DrawRectangle(borderPen, new Rect(ox, oy, frag.Width * zoom, frag.Height * zoom));
        }
    }

    #endregion

    #region Scrollbar Sync

    private void SyncScrollBars(int imgW, int imgH)
    {
        if (_hScrollBar is null || _vScrollBar is null) return;
        _suppressScrollSync = true;

        var (hMin, hMax, hVal, hView) = _viewport.GetScrollInfo(imgW, Bounds.Width, _viewport.OffsetX);
        _hScrollBar.Minimum = -hMax;
        _hScrollBar.Maximum = -hMin;
        _hScrollBar.Value = -hVal;
        _hScrollBar.ViewportSize = hView;

        var (vMin, vMax, vVal, vView) = _viewport.GetScrollInfo(imgH, Bounds.Height, _viewport.OffsetY);
        _vScrollBar.Minimum = -vMax;
        _vScrollBar.Maximum = -vMin;
        _vScrollBar.Value = -vVal;
        _vScrollBar.ViewportSize = vView;

        _suppressScrollSync = false;
    }

    private void OnHScrollChanged(object? sender, RangeBaseValueChangedEventArgs e)
    {
        if (_suppressScrollSync) return;
        var (imgW, imgH) = GetImageSize();
        _viewport.SetOffset(-e.NewValue, _viewport.OffsetY, imgW, imgH, Bounds.Width, Bounds.Height);
        RecalcCursorPixel();
        InvalidateVisual();
    }

    private void OnVScrollChanged(object? sender, RangeBaseValueChangedEventArgs e)
    {
        if (_suppressScrollSync) return;
        var (imgW, imgH) = GetImageSize();
        _viewport.SetOffset(_viewport.OffsetX, -e.NewValue, imgW, imgH, Bounds.Width, Bounds.Height);
        RecalcCursorPixel();
        InvalidateVisual();
    }

    #endregion

    #region Mouse Input

    private (int W, int H) GetImageSize()
    {
        var bmp = SourceBitmap;
        return bmp is not null ? (bmp.PixelSize.Width, bmp.PixelSize.Height) : (0, 0);
    }

    private (int X, int Y)? ScreenToPixelClamped(Point pos)
    {
        var (imgW, imgH) = GetImageSize();
        if (imgW == 0) return null;
        var (px, py) = _viewport.ScreenToPixel(pos.X, pos.Y);
        if (px < 0 || px >= imgW || py < 0 || py >= imgH)
            return null;
        return (px, py);
    }

    private void RecalcCursorPixel()
    {
        if (_lastScreenPos is null) return;
        var pixel = ScreenToPixelClamped(_lastScreenPos.Value);
        if (pixel != _lastCursorPixel)
        {
            _lastCursorPixel = pixel;
            CursorPixelChanged?.Invoke(pixel);
        }
    }

    protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
    {
        base.OnPointerWheelChanged(e);
        var (imgW, imgH) = GetImageSize();
        if (imgW == 0) return;

        bool ctrl = (e.KeyModifiers & KeyModifiers.Control) != 0;
        bool shift = (e.KeyModifiers & KeyModifiers.Shift) != 0;

        if (ctrl)
        {
            var pos = e.GetPosition(this);
            _viewport.ZoomAtPoint(pos.X, pos.Y, e.Delta.Y, imgW, imgH, Bounds.Width, Bounds.Height);
        }
        else
        {
            double dx = e.Delta.X * ScrollPixelsPerTick;
            double dy = e.Delta.Y * ScrollPixelsPerTick;
            if (shift && Math.Abs(e.Delta.X) < 0.001)
            {
                dx = dy;
                dy = 0;
            }
            _viewport.Pan(dx, dy, imgW, imgH, Bounds.Width, Bounds.Height);
        }

        RecalcCursorPixel();
        InvalidateVisual();
        e.Handled = true;
    }

    protected override void OnPointerPressed(PointerPressedEventArgs e)
    {
        base.OnPointerPressed(e);
        var props = e.GetCurrentPoint(this).Properties;

        if (props.IsMiddleButtonPressed)
        {
            _isPanning = true;
            _panStart = e.GetPosition(this);
            _panStartOffsetX = _viewport.OffsetX;
            _panStartOffsetY = _viewport.OffsetY;
            e.Handled = true;
            return;
        }

        if (!props.IsLeftButtonPressed || _isPanning) return;

        var pixel = ScreenToPixelClamped(e.GetPosition(this));
        if (pixel is null) return;

        // Paste mode: left-click commits
        if (Editor is not null && Editor.IsPasting)
        {
            PasteCommitted?.Invoke();
            e.Handled = true;
            return;
        }

        // Select tool: begin rubber-band
        if (Editor is not null && Editor.ActiveTool == ToolType.Select)
        {
            _isSelecting = true;
            SelectionStart?.Invoke(pixel.Value.X, pixel.Value.Y);
            e.Handled = true;
            return;
        }

        // Regular drawing tools
        _isDrawing = true;
        ToolDown?.Invoke(pixel.Value.X, pixel.Value.Y);
        e.Handled = true;
    }

    protected override void OnPointerMoved(PointerEventArgs e)
    {
        base.OnPointerMoved(e);
        var pos = e.GetPosition(this);
        _lastScreenPos = pos;

        if (_isPanning)
        {
            var (imgW, imgH) = GetImageSize();
            _viewport.SetOffset(
                _panStartOffsetX + (pos.X - _panStart.X),
                _panStartOffsetY + (pos.Y - _panStart.Y),
                imgW, imgH, Bounds.Width, Bounds.Height);
            RecalcCursorPixel();
            InvalidateVisual();
            e.Handled = true;
            return;
        }

        var pixel = ScreenToPixelClamped(pos);
        if (pixel != _lastCursorPixel)
        {
            _lastCursorPixel = pixel;
            CursorPixelChanged?.Invoke(pixel);
        }

        // Paste mode: floating fragment follows cursor
        if (Editor is not null && Editor.IsPasting && pixel is not null)
        {
            PasteMoved?.Invoke(pixel.Value.X, pixel.Value.Y);
            InvalidateVisual();
            e.Handled = true;
            return;
        }

        // Selection rubber-band drag
        if (_isSelecting && pixel is not null)
        {
            SelectionUpdate?.Invoke(pixel.Value.X, pixel.Value.Y);
            InvalidateVisual();
            e.Handled = true;
            return;
        }

        if (_isDrawing && pixel is not null)
        {
            ToolDrag?.Invoke(pixel.Value.X, pixel.Value.Y);
            e.Handled = true;
        }
        else
        {
            InvalidateVisual();
        }
    }

    protected override void OnPointerReleased(PointerReleasedEventArgs e)
    {
        base.OnPointerReleased(e);

        if (_isPanning && e.InitialPressMouseButton == MouseButton.Middle)
        {
            _isPanning = false;
            e.Handled = true;
        }
        else if (_isSelecting && e.InitialPressMouseButton == MouseButton.Left)
        {
            _isSelecting = false;
            var pixel = ScreenToPixelClamped(e.GetPosition(this));
            if (pixel is not null)
                SelectionEnd?.Invoke(pixel.Value.X, pixel.Value.Y);
            InvalidateVisual();
            e.Handled = true;
        }
        else if (_isDrawing && e.InitialPressMouseButton == MouseButton.Left)
        {
            _isDrawing = false;
            e.Handled = true;
        }
    }

    protected override void OnPointerExited(PointerEventArgs e)
    {
        base.OnPointerExited(e);
        _lastScreenPos = null;
        if (_lastCursorPixel is not null)
        {
            _lastCursorPixel = null;
            CursorPixelChanged?.Invoke(null);
            InvalidateVisual();
        }
    }

    protected override void OnKeyDown(KeyEventArgs e)
    {
        base.OnKeyDown(e);

        if (e.Key == Key.Escape)
        {
            if (Editor is not null && Editor.IsPasting)
            {
                PasteCancelled?.Invoke();
                InvalidateVisual();
                e.Handled = true;
            }
            else if (Editor is not null && Editor.HasSelection)
            {
                Editor.ClearSelection();
                InvalidateVisual();
                e.Handled = true;
            }
        }
        else if (e.Key == Key.Enter && Editor is not null && Editor.IsPasting)
        {
            PasteCommitted?.Invoke();
            InvalidateVisual();
            e.Handled = true;
        }
    }

    #endregion

    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        base.OnPropertyChanged(change);
        if (change.Property == SourceBitmapProperty)
        {
            var bmp = change.GetNewValue<WriteableBitmap?>();
            int w = bmp?.PixelSize.Width ?? 0;
            int h = bmp?.PixelSize.Height ?? 0;
            if (w != _lastBitmapWidth || h != _lastBitmapHeight)
            {
                _lastBitmapWidth = w;
                _lastBitmapHeight = h;
                _viewportInitialized = false;
            }
        }
    }
}
