using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using AvaloniaEdit.Editing;
using AvaloniaEdit.Rendering;
using AvaloniaEdit.TextMate;
using ClaudeDo.Ui.ViewModels.Modals;
using Seg = ClaudeDo.Ui.Views.Controls.DiffEditorSetup.Seg;

namespace ClaudeDo.Ui.Views.Controls;

/// <summary>
/// Read-only diff renderer. <see cref="IsSplit"/> false shows one editor with the unified
/// stream; true shows the old state on the left and the new state on the right, aligned by
/// <see cref="DiffAlignment"/>. Syntax highlighting comes from TextMate, keyed off the file's
/// extension — the same mechanism the 3-pane conflict resolver uses.
/// </summary>
public partial class DiffTextView : UserControl
{
    public static readonly StyledProperty<DiffFileViewModel?> FileProperty =
        AvaloniaProperty.Register<DiffTextView, DiffFileViewModel?>(nameof(File));

    public static readonly StyledProperty<bool> IsSplitProperty =
        AvaloniaProperty.Register<DiffTextView, bool>(nameof(IsSplit));

    public static readonly StyledProperty<bool> WrapLinesProperty =
        AvaloniaProperty.Register<DiffTextView, bool>(nameof(WrapLines));

    public DiffFileViewModel? File
    {
        get => GetValue(FileProperty);
        set => SetValue(FileProperty, value);
    }

    public bool IsSplit
    {
        get => GetValue(IsSplitProperty);
        set => SetValue(IsSplitProperty, value);
    }

    public bool WrapLines
    {
        get => GetValue(WrapLinesProperty);
        set => SetValue(WrapLinesProperty, value);
    }

    private TextMate.Installation? _leftTm, _rightTm;

    // Row lookup per editor, indexed by document line number (1-based). Populated on rebuild
    // and consumed by the margin and background renderers added in later tasks.
    private RowInfo?[] _leftRows = Array.Empty<RowInfo?>();
    private RowInfo?[] _rightRows = Array.Empty<RowInfo?>();

    private AlignedDiff _aligned = AlignedDiff.Empty;

    private DiffLineNumberMargin? _leftMargin, _rightMargin;

    private bool _renderersInstalled;

    // Above this many raw diff lines, DiffAlignment.Build moves to a background thread instead
    // of running inline in ReloadFile. Build is O(lines) plus one word-diff LCS per 1:1 changed
    // pair, and each LCS is up to MaxWordDiffTokens² (400² = 160k int cells, ~640KB) — a diff
    // this size can plausibly stack up enough such pairs to blow a 16ms frame, and Planning mode
    // instantiates one DiffTextView per changed file in an ItemsControl, so several can hit this
    // at once. A line-count threshold was chosen over chunked/incremental alignment because Build
    // is already pure and side-effect-free (safe to run on Task.Run as-is), whereas incremental
    // construction would need to fragment its LCS/gap-detection state across calls for no benefit
    // to the common case. Below the threshold, staying synchronous avoids a placeholder frame
    // (there is nothing to show until the first build completes) for the overwhelming majority of
    // diffs, which finish well under a frame either way.
    private const int AsyncBuildLineThreshold = 500;

    // Bumped by every ReloadFile call; a background build only applies its result if this is
    // still the generation that started it, so a fast File-to-File switch during a slow build
    // can't overwrite the newer file's rows with a stale AlignedDiff.
    private int _buildGeneration;

    public DiffTextView()
    {
        InitializeComponent();
        _leftTm = DiffEditorSetup.InstallHighlighting(LeftEditor);
        _rightTm = DiffEditorSetup.InstallHighlighting(RightEditor);
        ReloadFile();
        new VerticalScrollSync(new[] { LeftEditor, RightEditor },
                               isActive: () => IsSplit, anchorByLine: () => WrapLines);
    }

    /// Brushes and the mono typeface only resolve once the control is in the visual tree —
    /// a detached control has no resource parent to walk up to, and its styles haven't been
    /// applied yet. Installing the renderers in the constructor would freeze them on their
    /// hardcoded fallbacks for good, since they capture their brushes once.
    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
    {
        base.OnAttachedToVisualTree(e);
        if (_renderersInstalled) return;
        _renderersInstalled = true;
        InstallRenderers();
        RebuildMargins();
        InvalidateRenderers();
    }

    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        base.OnPropertyChanged(change);
        if (change.Property == FileProperty)
            ReloadFile();
        else if (change.Property == IsSplitProperty)
            RefreshLayout();
        else if (change.Property == WrapLinesProperty)
            ApplyWrap();
    }

    /// Recomputes the alignment (word-diff LCS) for the current <see cref="File"/> and then
    /// refreshes everything downstream of it. Only ever needed when the file actually changes.
    /// Small diffs build inline; large ones move to a background thread (see
    /// <see cref="AsyncBuildLineThreshold"/>) and keep showing the previous file's rows until
    /// the new build lands, rather than flashing an empty frame.
    private void ReloadFile()
    {
        var lines = File?.Lines;
        var generation = ++_buildGeneration;

        if (lines is null || lines.Count <= AsyncBuildLineThreshold)
        {
            _aligned = DiffAlignment.Build(lines);
            RefreshLayout();
            return;
        }

        _ = BuildAsync(lines, generation);
    }

    private async Task BuildAsync(IReadOnlyList<DiffLineViewModel> lines, int generation)
    {
        var aligned = await Task.Run(() => DiffAlignment.Build(lines));
        if (generation != _buildGeneration) return; // superseded by a newer File in the meantime.
        _aligned = aligned;
        RefreshLayout();
    }

    /// Re-applies the current <see cref="_aligned"/> to the editors for the current layout
    /// (split vs unified). A single <see cref="AlignedDiff"/> already carries both row sets,
    /// so toggling <see cref="IsSplit"/> never needs to re-run the alignment.
    private void RefreshLayout()
    {
        var split = IsSplit;
        RightEditor.IsVisible = split;
        PaneDivider.IsVisible = split;
        PaneHeader.IsVisible = split;
        Grid.SetColumnSpan(LeftEditor, split ? 1 : 3);

        if (split)
        {
            LeftEditor.Text = _aligned.LeftText;
            RightEditor.Text = _aligned.RightText;
            _leftRows = BuildRows(_aligned.SplitRows.Count,
                i => new RowInfo(_aligned.SplitRows[i].LeftKind, _aligned.SplitRows[i].OldNo, null,
                                 _aligned.SplitRows[i].LeftSpans));
            _rightRows = BuildRows(_aligned.SplitRows.Count,
                i => new RowInfo(_aligned.SplitRows[i].RightKind, null, _aligned.SplitRows[i].NewNo,
                                 _aligned.SplitRows[i].RightSpans));
        }
        else
        {
            LeftEditor.Text = _aligned.UnifiedText;
            RightEditor.Text = "";
            _leftRows = BuildRows(_aligned.UnifiedRows.Count,
                i => new RowInfo(_aligned.UnifiedRows[i].Kind, _aligned.UnifiedRows[i].OldNo,
                                 _aligned.UnifiedRows[i].NewNo, _aligned.UnifiedRows[i].Spans));
            _rightRows = Array.Empty<RowInfo?>();
        }

        RebuildMargins();
        ApplyWrap();
        DiffEditorSetup.ApplyGrammar(File?.Path, _leftTm, _rightTm);
        InvalidateRenderers();
    }

    /// Row index i is document line i + 1, so slot 0 stays null and lookups can pass the
    /// line number straight through.
    private static RowInfo?[] BuildRows(int count, Func<int, RowInfo> project)
    {
        var rows = new RowInfo?[count + 1];
        for (var i = 0; i < count; i++) rows[i + 1] = project(i);
        return rows;
    }

    private RowInfo? LeftRow(int line) =>
        line > 0 && line < _leftRows.Length ? _leftRows[line] : null;

    private RowInfo? RightRow(int line) =>
        line > 0 && line < _rightRows.Length ? _rightRows[line] : null;

    private void ApplyWrap()
    {
        LeftEditor.WordWrap = WrapLines;
        RightEditor.WordWrap = WrapLines;
    }

    /// The margin's column layout depends on split vs unified, so it is rebuilt rather than
    /// reconfigured whenever the layout changes.
    private void RebuildMargins()
    {
        if (_leftMargin is not null) LeftEditor.TextArea.LeftMargins.Remove(_leftMargin);
        if (_rightMargin is not null) RightEditor.TextArea.LeftMargins.Remove(_rightMargin);

        var foreground = Brush("TextFaintBrush", Color.Parse("#80FFFFFF"));
        var typeface = new Typeface(LeftEditor.FontFamily);

        _leftMargin = new DiffLineNumberMargin(LeftRow, showOld: true, showNew: !IsSplit,
                                               foreground, typeface, LeftEditor.FontSize);
        LeftEditor.TextArea.LeftMargins.Insert(0, _leftMargin);

        _rightMargin = new DiffLineNumberMargin(RightRow, showOld: false, showNew: true,
                                                foreground, typeface, RightEditor.FontSize);
        RightEditor.TextArea.LeftMargins.Insert(0, _rightMargin);
    }

    private IBrush Brush(string key, Color fallback) => DiffEditorSetup.Brush(this, key, fallback);

    private void InstallRenderers()
    {
        var add    = Brush("RunningTintBrush",  Color.Parse("#1F7C9166"));
        var del    = Brush("ErrorTintBrush",    Color.Parse("#1FC87060"));
        var filler = Brush("DiffFillerBrush",   Color.Parse("#0AFFFFFF"));
        var gap    = Brush("DiffGapBrush",      Color.Parse("#14FFFFFF"));
        var wordAdd = Brush("DiffWordAddBrush", Color.Parse("#556FA86B"));
        var wordDel = Brush("DiffWordDelBrush", Color.Parse("#55C87060"));

        LeftEditor.TextArea.TextView.BackgroundRenderers.Add(new DiffLineRenderer(LeftRow, add, del, filler, gap));
        LeftEditor.TextArea.TextView.BackgroundRenderers.Add(new WordDiffRenderer(LeftRow, wordAdd, wordDel));
        RightEditor.TextArea.TextView.BackgroundRenderers.Add(new DiffLineRenderer(RightRow, add, del, filler, gap));
        RightEditor.TextArea.TextView.BackgroundRenderers.Add(new WordDiffRenderer(RightRow, wordAdd, wordDel));
    }

    private void InvalidateRenderers()
    {
        LeftEditor.TextArea.TextView.InvalidateVisual();
        RightEditor.TextArea.TextView.InvalidateVisual();
    }

    /// <summary>What one document line represents, for the margin and the background renderers.</summary>
    internal sealed record RowInfo(
        AlignedSide Kind, int? OldNo, int? NewNo, IReadOnlyList<TextSpan> Spans);

    /// <summary>Draws one or two gutter columns of diff line numbers from the row lookup.</summary>
    private sealed class DiffLineNumberMargin : AbstractMargin
    {
        private const double ColumnWidth = 44;
        private const double Gap = 6;

        private readonly Func<int, RowInfo?> _rows;
        private readonly bool _showOld;
        private readonly bool _showNew;
        private readonly IBrush _foreground;
        private readonly Typeface _typeface;
        private readonly double _fontSize;

        public DiffLineNumberMargin(Func<int, RowInfo?> rows, bool showOld, bool showNew,
                                    IBrush foreground, Typeface typeface, double fontSize)
        {
            _rows = rows;
            _showOld = showOld;
            _showNew = showNew;
            _foreground = foreground;
            _typeface = typeface;
            _fontSize = fontSize;
        }

        private double Columns => (_showOld ? 1 : 0) + (_showNew ? 1 : 0);

        protected override Size MeasureOverride(Size availableSize) =>
            new(Columns * ColumnWidth + Gap, 0);

        protected override void OnTextViewChanged(TextView? oldTextView, TextView? newTextView)
        {
            if (oldTextView is not null) oldTextView.VisualLinesChanged -= OnVisualLinesChanged;
            base.OnTextViewChanged(oldTextView, newTextView);
            if (newTextView is not null) newTextView.VisualLinesChanged += OnVisualLinesChanged;
            InvalidateVisual();
        }

        private void OnVisualLinesChanged(object? sender, EventArgs e) => InvalidateVisual();

        public override void Render(DrawingContext context)
        {
            var textView = TextView;
            if (textView is null || !textView.VisualLinesValid) return;

            foreach (var visualLine in textView.VisualLines)
            {
                var lineNumber = visualLine.FirstDocumentLine.LineNumber;
                if (_rows(lineNumber) is not { } row) continue;

                var y = visualLine.VisualTop - textView.ScrollOffset.Y;
                var column = 0;
                if (_showOld) DrawNumber(context, row.OldNo, column++, y);
                if (_showNew) DrawNumber(context, row.NewNo, column, y);
            }
        }

        private void DrawNumber(DrawingContext context, int? value, int column, double y)
        {
            if (value is null) return;
            var text = new FormattedText(value.Value.ToString(CultureInfo.InvariantCulture),
                CultureInfo.InvariantCulture, FlowDirection.LeftToRight, _typeface, _fontSize, _foreground);
            // Right-align inside the column so the digits line up across rows.
            var x = (column + 1) * ColumnWidth - text.Width - Gap;
            context.DrawText(text, new Point(x, y));
        }
    }

    /// <summary>Tints whole rows by their diff role.</summary>
    private sealed class DiffLineRenderer : IBackgroundRenderer
    {
        private readonly Func<int, RowInfo?> _rows;
        private readonly IBrush _add, _del, _filler, _gap;

        public DiffLineRenderer(Func<int, RowInfo?> rows, IBrush add, IBrush del, IBrush filler, IBrush gap)
        {
            _rows = rows; _add = add; _del = del; _filler = filler; _gap = gap;
        }

        public KnownLayer Layer => KnownLayer.Background;

        public void Draw(TextView textView, DrawingContext drawingContext)
        {
            if (!textView.VisualLinesValid) return;
            foreach (var visualLine in textView.VisualLines)
            {
                var row = _rows(visualLine.FirstDocumentLine.LineNumber);
                var brush = row?.Kind switch
                {
                    AlignedSide.Add    => _add,
                    AlignedSide.Del    => _del,
                    AlignedSide.Filler => _filler,
                    AlignedSide.Gap    => _gap,
                    _                  => null,
                };
                if (brush is null) continue;

                var top = visualLine.VisualTop - textView.ScrollOffset.Y;
                drawingContext.FillRectangle(brush, new Rect(0, top, textView.Bounds.Width, visualLine.Height));
            }
        }
    }

    /// <summary>Tints the changed character ranges inside a row, on top of the row tint.</summary>
    private sealed class WordDiffRenderer : IBackgroundRenderer
    {
        private readonly Func<int, RowInfo?> _rows;
        private readonly IBrush _add, _del;

        public WordDiffRenderer(Func<int, RowInfo?> rows, IBrush add, IBrush del)
        {
            _rows = rows; _add = add; _del = del;
        }

        // Above the line tint but still behind the text.
        public KnownLayer Layer => KnownLayer.Selection;

        public void Draw(TextView textView, DrawingContext drawingContext)
        {
            if (!textView.VisualLinesValid) return;
            foreach (var visualLine in textView.VisualLines)
            {
                var documentLine = visualLine.FirstDocumentLine;
                var row = _rows(documentLine.LineNumber);
                if (row is null || row.Spans.Count == 0) continue;

                var brush = row.Kind == AlignedSide.Add ? _add : _del;
                foreach (var span in row.Spans)
                {
                    var offset = documentLine.Offset + span.Start;
                    // Spans are computed against the row text; clamp in case the document
                    // and the row lookup ever disagree rather than drawing past the line.
                    if (offset < documentLine.Offset || offset + span.Length > documentLine.EndOffset) continue;

                    var builder = new BackgroundGeometryBuilder { AlignToWholePixels = true, CornerRadius = 2 };
                    builder.AddSegment(textView, new Seg(offset, span.Length));
                    if (builder.CreateGeometry() is { } geometry)
                        drawingContext.DrawGeometry(brush, null, geometry);
                }
            }
        }
    }
}
