using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Minint.Core.Models;
using Minint.Core.Services;
using Minint.Core.Services.Impl;

namespace Minint.ViewModels;

/// <summary>
/// Palette-independent clipboard fragment: stores resolved RGBA pixels.
/// </summary>
public sealed record ClipboardFragment(int Width, int Height, RgbaColor[] Pixels);

public partial class EditorViewModel : ViewModelBase
{
    private readonly ICompositor _compositor = new Compositor();
    private readonly IPaletteService _paletteService = new PaletteService();
    private readonly IDrawingService _drawingService = new DrawingService();
    private readonly IFloodFillService _floodFillService = new FloodFillService();
    private readonly IFragmentService _fragmentService = new FragmentService();

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(HasContainer))]
    [NotifyPropertyChangedFor(nameof(Title))]
    private MinintContainer? _container;

    [ObservableProperty]
    private MinintDocument? _activeDocument;

    [ObservableProperty]
    private MinintLayer? _activeLayer;

    private bool _suppressDocumentSync;

    [ObservableProperty]
    private WriteableBitmap? _canvasBitmap;

    [ObservableProperty]
    private bool _showGrid;

    // Tool state
    [ObservableProperty]
    private ToolType _activeTool = ToolType.Brush;

    [ObservableProperty]
    private int _brushRadius = 1;

    [ObservableProperty]
    private (int X, int Y)? _previewCenter;

    private Avalonia.Media.Color _previewColor = Avalonia.Media.Color.FromArgb(255, 0, 0, 0);

    public Avalonia.Media.Color PreviewColor
    {
        get => _previewColor;
        set
        {
            if (_previewColor == value) return;
            _previewColor = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(SelectedColor));
        }
    }

    public RgbaColor SelectedColor => new(_previewColor.R, _previewColor.G, _previewColor.B, _previewColor.A);

    // Selection state (Select tool rubber-band)
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(HasSelection))]
    private (int X, int Y, int W, int H)? _selectionRect;

    public bool HasSelection => SelectionRect is not null;

    // Clipboard
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(HasClipboard))]
    private ClipboardFragment? _clipboard;

    public bool HasClipboard => Clipboard is not null;

    // Paste mode
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsPasting))]
    private (int X, int Y)? _pastePosition;

    public bool IsPasting => PastePosition is not null;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(Title))]
    private string? _filePath;

    public bool HasContainer => Container is not null;

    public string Title => FilePath is not null
        ? $"Minint — {System.IO.Path.GetFileName(FilePath)}"
        : Container is not null
            ? "Minint — Untitled"
            : "Minint";

    public ObservableCollection<MinintDocument> Documents { get; } = [];
    public ObservableCollection<MinintLayer> Layers { get; } = [];

    #region Container / Document management

    public void NewContainer(int width, int height)
    {
        var c = new MinintContainer(width, height);
        c.AddNewDocument("Document 1");
        LoadContainer(c, null);
    }

    public void LoadContainer(MinintContainer container, string? path)
    {
        Container = container;
        FilePath = path;

        SyncDocumentsList();
        SelectDocument(container.Documents.Count > 0 ? container.Documents[0] : null);
    }

    partial void OnActiveDocumentChanged(MinintDocument? value)
    {
        if (_suppressDocumentSync) return;
        SyncLayersAndCanvas(value);
    }

    public void SelectDocument(MinintDocument? doc)
    {
        _suppressDocumentSync = true;
        ActiveDocument = doc;
        _suppressDocumentSync = false;
        SyncLayersAndCanvas(doc);
    }

    public void SyncAfterExternalChange() => SyncDocumentsList();

    private void SyncDocumentsList()
    {
        Documents.Clear();
        if (Container is null) return;
        foreach (var doc in Container.Documents)
            Documents.Add(doc);
    }

    private void SyncLayersAndCanvas(MinintDocument? doc)
    {
        UnsubscribeLayerVisibility();
        Layers.Clear();
        if (doc is not null)
        {
            foreach (var layer in doc.Layers)
                Layers.Add(layer);
            ActiveLayer = doc.Layers.Count > 0 ? doc.Layers[0] : null;
        }
        else
        {
            ActiveLayer = null;
        }
        SubscribeLayerVisibility();
        RefreshCanvas();
    }

    #endregion

    #region Layer visibility change tracking

    private void SubscribeLayerVisibility()
    {
        foreach (var layer in Layers)
        {
            if (layer is INotifyPropertyChanged npc)
                npc.PropertyChanged += OnLayerPropertyChanged;
        }
    }

    private void UnsubscribeLayerVisibility()
    {
        foreach (var layer in Layers)
        {
            if (layer is INotifyPropertyChanged npc)
                npc.PropertyChanged -= OnLayerPropertyChanged;
        }
    }

    private void OnLayerPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName is nameof(MinintLayer.IsVisible) or nameof(MinintLayer.Opacity))
            RefreshCanvas();
    }

    #endregion

    #region Document commands

    [RelayCommand]
    private void AddDocument()
    {
        if (Container is null) return;
        int num = Container.Documents.Count + 1;
        var doc = Container.AddNewDocument($"Document {num}");
        Documents.Add(doc);
        SelectDocument(doc);
    }

    [RelayCommand]
    private void RemoveDocument()
    {
        if (Container is null || ActiveDocument is null) return;
        if (Container.Documents.Count <= 1) return;

        var doc = ActiveDocument;
        int idx = Container.Documents.IndexOf(doc);
        Container.Documents.Remove(doc);
        Documents.Remove(doc);

        int newIdx = Math.Min(idx, Container.Documents.Count - 1);
        SelectDocument(newIdx >= 0 ? Container.Documents[newIdx] : null);
    }

    [RelayCommand]
    private void RenameDocument() { }

    [RelayCommand]
    private void MoveDocumentUp()
    {
        if (Container is null || ActiveDocument is null) return;
        var doc = ActiveDocument;
        int idx = Container.Documents.IndexOf(doc);
        if (idx <= 0) return;
        (Container.Documents[idx], Container.Documents[idx - 1]) = (Container.Documents[idx - 1], Container.Documents[idx]);
        SyncDocumentsList();
        ReselectDocument(doc);
    }

    [RelayCommand]
    private void MoveDocumentDown()
    {
        if (Container is null || ActiveDocument is null) return;
        var doc = ActiveDocument;
        int idx = Container.Documents.IndexOf(doc);
        if (idx < 0 || idx >= Container.Documents.Count - 1) return;
        (Container.Documents[idx], Container.Documents[idx + 1]) = (Container.Documents[idx + 1], Container.Documents[idx]);
        SyncDocumentsList();
        ReselectDocument(doc);
    }

    private void ReselectDocument(MinintDocument doc)
    {
        // Force SelectedItem rebinding after list rebuild even when the selected object is the same reference.
        _suppressDocumentSync = true;
        ActiveDocument = null;
        ActiveDocument = doc;
        _suppressDocumentSync = false;
        SyncLayersAndCanvas(doc);
    }

    #endregion

    #region Layer commands

    [RelayCommand]
    private void AddLayer()
    {
        if (Container is null || ActiveDocument is null) return;
        int num = ActiveDocument.Layers.Count + 1;
        var layer = new MinintLayer($"Layer {num}", Container.PixelCount);
        ActiveDocument.Layers.Add(layer);
        Layers.Add(layer);
        ActiveLayer = layer;
        SubscribeLayerVisibility();
    }

    [RelayCommand]
    private void RemoveLayer()
    {
        if (ActiveDocument is null || ActiveLayer is null) return;
        if (ActiveDocument.Layers.Count <= 1) return;

        UnsubscribeLayerVisibility();
        var layer = ActiveLayer;
        int idx = ActiveDocument.Layers.IndexOf(layer);
        ActiveDocument.Layers.Remove(layer);
        Layers.Remove(layer);

        int newIdx = Math.Min(idx, ActiveDocument.Layers.Count - 1);
        ActiveLayer = newIdx >= 0 ? ActiveDocument.Layers[newIdx] : null;
        SubscribeLayerVisibility();
        RefreshCanvas();
    }

    [RelayCommand]
    private void MoveLayerUp()
    {
        if (ActiveDocument is null || ActiveLayer is null) return;
        int idx = ActiveDocument.Layers.IndexOf(ActiveLayer);
        if (idx <= 0) return;

        UnsubscribeLayerVisibility();
        var layer = ActiveLayer;
        (ActiveDocument.Layers[idx], ActiveDocument.Layers[idx - 1]) = (ActiveDocument.Layers[idx - 1], ActiveDocument.Layers[idx]);
        Layers.Move(idx, idx - 1);
        ActiveLayer = layer;
        SubscribeLayerVisibility();
        RefreshCanvas();
    }

    [RelayCommand]
    private void MoveLayerDown()
    {
        if (ActiveDocument is null || ActiveLayer is null) return;
        int idx = ActiveDocument.Layers.IndexOf(ActiveLayer);
        if (idx < 0 || idx >= ActiveDocument.Layers.Count - 1) return;

        UnsubscribeLayerVisibility();
        var layer = ActiveLayer;
        (ActiveDocument.Layers[idx], ActiveDocument.Layers[idx + 1]) = (ActiveDocument.Layers[idx + 1], ActiveDocument.Layers[idx]);
        Layers.Move(idx, idx + 1);
        ActiveLayer = layer;
        SubscribeLayerVisibility();
        RefreshCanvas();
    }

    [RelayCommand]
    private void DuplicateLayer()
    {
        if (Container is null || ActiveDocument is null || ActiveLayer is null) return;
        var src = ActiveLayer;
        var dup = new MinintLayer(src.Name + " copy", src.IsVisible, src.Opacity, (int[])src.Pixels.Clone());
        int idx = ActiveDocument.Layers.IndexOf(src) + 1;
        ActiveDocument.Layers.Insert(idx, dup);

        UnsubscribeLayerVisibility();
        Layers.Insert(idx, dup);
        ActiveLayer = dup;
        SubscribeLayerVisibility();
        RefreshCanvas();
    }

    #endregion

    #region Drawing

    public void OnToolDown(int px, int py)
    {
        if (IsPlaying) return;
        if (Container is null || ActiveDocument is null || ActiveLayer is null)
            return;

        int w = Container.Width, h = Container.Height;
        if (px < 0 || px >= w || py < 0 || py >= h)
            return;

        if (ActiveTool == ToolType.Select) return; // handled separately

        switch (ActiveTool)
        {
            case ToolType.Brush:
            {
                int colorIdx = ActiveDocument.EnsureColorCached(SelectedColor);
                _drawingService.ApplyBrush(ActiveLayer, px, py, BrushRadius, colorIdx, w, h);
                break;
            }
            case ToolType.Eraser:
                _drawingService.ApplyEraser(ActiveLayer, px, py, BrushRadius, w, h);
                break;
            case ToolType.Fill:
            {
                int colorIdx = ActiveDocument.EnsureColorCached(SelectedColor);
                _floodFillService.Fill(ActiveLayer, px, py, colorIdx, w, h);
                break;
            }
        }

        RefreshCanvas();
    }

    public void OnToolDrag(int px, int py)
    {
        if (IsPlaying) return;
        if (ActiveTool is ToolType.Fill or ToolType.Select) return;
        OnToolDown(px, py);
    }

    public List<(int X, int Y)>? GetPreviewMask()
    {
        if (PreviewCenter is null || Container is null)
            return null;
        if (ActiveTool is ToolType.Fill or ToolType.Select)
            return null;

        var (cx, cy) = PreviewCenter.Value;
        return _drawingService.GetBrushMask(cx, cy, BrushRadius, Container.Width, Container.Height);
    }

    [RelayCommand]
    private void SelectBrush() { CancelPasteMode(); ActiveTool = ToolType.Brush; }

    [RelayCommand]
    private void SelectEraser() { CancelPasteMode(); ActiveTool = ToolType.Eraser; }

    [RelayCommand]
    private void SelectFill() { CancelPasteMode(); ActiveTool = ToolType.Fill; }

    [RelayCommand]
    private void SelectSelectTool() { CancelPasteMode(); ActiveTool = ToolType.Select; }

    [RelayCommand]
    private void ToggleGrid() => ShowGrid = !ShowGrid;

    #endregion

    #region Selection + Copy/Paste (A4)

    /// <summary>Called by PixelCanvas when selection drag starts.</summary>
    public void BeginSelection(int px, int py)
    {
        if (IsPlaying) return;
        SelectionRect = (px, py, 0, 0);
    }

    /// <summary>Called by PixelCanvas as the user drags.</summary>
    public void UpdateSelection(int px, int py)
    {
        if (SelectionRect is null) return;
        var s = SelectionRect.Value;
        int x = Math.Min(s.X, px);
        int y = Math.Min(s.Y, py);
        int w = Math.Abs(px - s.X) + 1;
        int h = Math.Abs(py - s.Y) + 1;
        // Store normalized rect but keep original anchor in _selAnchor
        _selectionRectNormalized = (x, y, w, h);
    }

    /// <summary>Called by PixelCanvas when mouse is released.</summary>
    public void FinishSelection(int px, int py)
    {
        if (SelectionRect is null) return;
        var s = SelectionRect.Value;
        int x0 = Math.Min(s.X, px);
        int y0 = Math.Min(s.Y, py);
        int rw = Math.Abs(px - s.X) + 1;
        int rh = Math.Abs(py - s.Y) + 1;
        if (Container is not null)
        {
            x0 = Math.Max(0, x0);
            y0 = Math.Max(0, y0);
            rw = Math.Min(rw, Container.Width - x0);
            rh = Math.Min(rh, Container.Height - y0);
        }
        if (rw <= 0 || rh <= 0)
        {
            SelectionRect = null;
            _selectionRectNormalized = null;
            return;
        }
        SelectionRect = (x0, y0, rw, rh);
        _selectionRectNormalized = SelectionRect;
    }

    private (int X, int Y, int W, int H)? _selectionRectNormalized;

    /// <summary>The normalized (positive W/H, clamped) selection rectangle for rendering.</summary>
    public (int X, int Y, int W, int H)? SelectionRectNormalized => _selectionRectNormalized;

    [RelayCommand]
    private void CopySelection()
    {
        if (SelectionRect is null || ActiveDocument is null || ActiveLayer is null || Container is null)
            return;

        var (sx, sy, sw, sh) = SelectionRect.Value;
        int cw = Container.Width;
        var palette = ActiveDocument.Palette;
        var srcPixels = ActiveLayer.Pixels;
        var buf = new RgbaColor[sw * sh];

        for (int dy = 0; dy < sh; dy++)
        {
            int srcRow = sy + dy;
            for (int dx = 0; dx < sw; dx++)
            {
                int srcCol = sx + dx;
                int idx = srcPixels[srcRow * cw + srcCol];
                buf[dy * sw + dx] = idx < palette.Count ? palette[idx] : RgbaColor.Transparent;
            }
        }

        Clipboard = new ClipboardFragment(sw, sh, buf);
    }

    [RelayCommand]
    private void PasteClipboard()
    {
        if (Clipboard is null) return;
        PastePosition = (0, 0);
    }

    public void MovePaste(int px, int py)
    {
        if (IsPlaying || !IsPasting) return;
        PastePosition = (px, py);
    }

    [RelayCommand]
    public void CommitPaste()
    {
        if (IsPlaying) return;
        if (!IsPasting || Clipboard is null || ActiveDocument is null || ActiveLayer is null || Container is null)
            return;

        var (px, py) = PastePosition!.Value;
        int cw = Container.Width, ch = Container.Height;
        var frag = Clipboard;
        var dstPixels = ActiveLayer.Pixels;

        for (int fy = 0; fy < frag.Height; fy++)
        {
            int dy = py + fy;
            if (dy < 0 || dy >= ch) continue;
            for (int fx = 0; fx < frag.Width; fx++)
            {
                int dx = px + fx;
                if (dx < 0 || dx >= cw) continue;
                var color = frag.Pixels[fy * frag.Width + fx];
                if (color.A == 0) continue; // skip transparent
                int colorIdx = ActiveDocument.EnsureColorCached(color);
                dstPixels[dy * cw + dx] = colorIdx;
            }
        }

        PastePosition = null;
        SelectionRect = null;
        _selectionRectNormalized = null;
        RefreshCanvas();
    }

    [RelayCommand]
    public void CancelPaste()
    {
        PastePosition = null;
    }

    public void ClearSelection()
    {
        SelectionRect = null;
        _selectionRectNormalized = null;
    }

    private void CancelPasteMode()
    {
        PastePosition = null;
        SelectionRect = null;
        _selectionRectNormalized = null;
    }

    #endregion

    #region Canvas rendering

    public void RefreshCanvas()
    {
        if (Container is null || ActiveDocument is null)
        {
            CanvasBitmap = null;
            return;
        }

        int w = Container.Width;
        int h = Container.Height;
        uint[] argb = _compositor.Composite(ActiveDocument, w, h);

        var bmp = new WriteableBitmap(
            new PixelSize(w, h),
            new Vector(96, 96),
            Avalonia.Platform.PixelFormat.Bgra8888);

        using (var fb = bmp.Lock())
        {
            unsafe
            {
                var dst = new Span<uint>((void*)fb.Address, w * h);
                for (int i = 0; i < argb.Length; i++)
                {
                    uint px = argb[i];
                    byte a = (byte)(px >> 24);
                    byte r = (byte)((px >> 16) & 0xFF);
                    byte g = (byte)((px >> 8) & 0xFF);
                    byte b = (byte)(px & 0xFF);

                    if (a == 255)
                    {
                        dst[i] = px;
                    }
                    else if (a == 0)
                    {
                        dst[i] = 0;
                    }
                    else
                    {
                        r = (byte)(r * a / 255);
                        g = (byte)(g * a / 255);
                        b = (byte)(b * a / 255);
                        dst[i] = (uint)(b | (g << 8) | (r << 16) | (a << 24));
                    }
                }
            }
        }

        CanvasBitmap = bmp;
    }

    #endregion

    #region Animation playback

    private DispatcherTimer? _animationTimer;
    private int _animationFrameIndex;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsNotPlaying))]
    private bool _isPlaying;

    public bool IsNotPlaying => !IsPlaying;

    [RelayCommand]
    private void PlayAnimation()
    {
        if (Container is null || Container.Documents.Count < 2) return;
        if (IsPlaying) return;

        IsPlaying = true;
        _animationFrameIndex = ActiveDocument is not null
            ? Container.Documents.IndexOf(ActiveDocument)
            : 0;
        if (_animationFrameIndex < 0) _animationFrameIndex = 0;

        AdvanceAnimationFrame();
    }

    [RelayCommand]
    private void StopAnimation()
    {
        _animationTimer?.Stop();
        _animationTimer = null;
        IsPlaying = false;
        if (ActiveDocument is not null)
            SyncLayersAndCanvas(ActiveDocument);
    }

    private void AdvanceAnimationFrame()
    {
        if (Container is null || !IsPlaying)
        {
            StopAnimation();
            return;
        }

        var docs = Container.Documents;
        if (docs.Count == 0)
        {
            StopAnimation();
            return;
        }

        _animationFrameIndex %= docs.Count;
        var doc = docs[_animationFrameIndex];

        _suppressDocumentSync = true;
        ActiveDocument = doc;
        _suppressDocumentSync = false;
        RefreshCanvasFor(doc);

        uint delay = doc.FrameDelayMs;
        if (delay < 10) delay = 10;

        _animationTimer?.Stop();
        _animationTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(delay) };
        _animationTimer.Tick += (_, _) =>
        {
            _animationTimer?.Stop();
            _animationFrameIndex++;
            AdvanceAnimationFrame();
        };
        _animationTimer.Start();
    }

    private void RefreshCanvasFor(MinintDocument doc)
    {
        if (Container is null)
        {
            CanvasBitmap = null;
            return;
        }

        int w = Container.Width;
        int h = Container.Height;
        uint[] argb = _compositor.Composite(doc, w, h);

        var bmp = new WriteableBitmap(
            new PixelSize(w, h),
            new Vector(96, 96),
            Avalonia.Platform.PixelFormat.Bgra8888);

        using (var fb = bmp.Lock())
        {
            unsafe
            {
                var dst = new Span<uint>((void*)fb.Address, w * h);
                for (int i = 0; i < argb.Length; i++)
                {
                    uint px = argb[i];
                    byte a2 = (byte)(px >> 24);
                    byte r2 = (byte)((px >> 16) & 0xFF);
                    byte g2 = (byte)((px >> 8) & 0xFF);
                    byte b2 = (byte)(px & 0xFF);

                    if (a2 == 255) { dst[i] = px; }
                    else if (a2 == 0) { dst[i] = 0; }
                    else
                    {
                        r2 = (byte)(r2 * a2 / 255);
                        g2 = (byte)(g2 * a2 / 255);
                        b2 = (byte)(b2 * a2 / 255);
                        dst[i] = (uint)(b2 | (g2 << 8) | (r2 << 16) | (a2 << 24));
                    }
                }
            }
        }

        CanvasBitmap = bmp;
    }

    #endregion

    public ICompositor Compositor => _compositor;
    public IPaletteService PaletteService => _paletteService;
    public IDrawingService DrawingService => _drawingService;
    public IFragmentService FragmentService => _fragmentService;
}
