using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using Avalonia.Media;
using TraceCad.App.Tools;
using TraceCad.Core.Commands;
using TraceCad.Core.Geometry;
using TraceCad.Core.Model;

namespace TraceCad.App;

public sealed class SketchCanvas : Control
{
    private readonly SolidColorBrush _canvasBackground = new(Color.FromRgb(8, 13, 24));
    private readonly Pen _gridPen = new(new SolidColorBrush(Color.FromRgb(30, 41, 59)), 1.0);
    private readonly Pen _axisPen = new(new SolidColorBrush(Color.FromRgb(71, 85, 105)), 1.3);
    private readonly Pen _entityPen = new(new SolidColorBrush(Color.FromRgb(226, 232, 240)), 1.7);
    private readonly Pen _selectedPen = new(new SolidColorBrush(Color.FromRgb(96, 165, 250)), 2.6);
    private readonly ViewTransform _transform = new();
    private readonly SelectTool _selectTool = new();
    private bool _isPanning;
    private Point _lastPanPoint;
    private Point2? _hoverPoint;
    private SnapResult? _hoverSnap;
    private Entity? _selectedEntity;
    private ISketchTool _activeTool;
    private Bitmap? _referenceBitmap;
    private KeyModifiers _keyModifiers;

    public SketchCanvas()
    {
        Focusable = true;
        ClipToBounds = true;
        Document = SketchDocument.CreateDefault();
        Commands = new CommandManager();
        _activeTool = _selectTool;
        StatusText = _activeTool.Prompt;
    }

    public event EventHandler? Changed;

    public event EventHandler? StatusChanged;

    public SketchDocument Document { get; private set; }

    public CommandManager Commands { get; }

    public string StatusText { get; private set; }

    public string ActiveToolName => _activeTool.Name;

    public Entity? SelectedEntity => _selectedEntity;

    public void SetDocument(SketchDocument document)
    {
        Document = document;
        SketchConstraintSolver.Solve(Document);
        Commands.Clear();
        _selectedEntity = null;
        _transform.Reset();
        LoadReferenceBitmap();
        SetStatus(_activeTool.Prompt);
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void ImportReferenceImage(string imagePath)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(imagePath);

        _referenceBitmap?.Dispose();
        _referenceBitmap = new Bitmap(imagePath);
        var bitmapSize = _referenceBitmap.Size;
        var defaultScale = bitmapSize.Width > 0.0 ? Document.Width / bitmapSize.Width : 1.0;

        Document.Reference = new ReferenceImage(
            imagePath,
            0.55,
            Locked: true,
            new ReferenceTransform(
                OriginX: 0.0,
                OriginY: 0.0,
                ScaleX: defaultScale,
                ScaleY: defaultScale,
                RotationDeg: 0.0));

        SetStatus($"Reference imported: {Path.GetFileName(imagePath)}");
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void SetReferenceImage(ReferenceImage reference)
    {
        ArgumentNullException.ThrowIfNull(reference);

        Document.Reference = reference;
        LoadReferenceBitmap();
        SetStatus($"Reference loaded: {Path.GetFileName(reference.ImagePath)}");
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void SetReferenceOpacity(double opacity)
    {
        if (Document.Reference is null)
        {
            return;
        }

        Document.Reference = Document.Reference with { Opacity = Math.Clamp(opacity, 0.0, 1.0) };
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void SetReferenceLocked(bool locked)
    {
        if (Document.Reference is null)
        {
            return;
        }

        Document.Reference = Document.Reference with { Locked = locked };
        SetStatus(locked ? "Reference locked" : "Reference unlocked");
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void SetReferenceTransform(ReferenceTransform transform)
    {
        if (Document.Reference is null || Document.Reference.Locked)
        {
            return;
        }

        Document.Reference = Document.Reference with
        {
            Transform = transform with
            {
                ScaleX = Math.Max(0.0001, transform.ScaleX),
                ScaleY = Math.Max(0.0001, transform.ScaleY)
            }
        };
        SetStatus("Reference transform updated");
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void ClearReference()
    {
        Document.Reference = null;
        _referenceBitmap?.Dispose();
        _referenceBitmap = null;
        SetStatus("Reference cleared");
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void SetTool(ISketchTool tool)
    {
        _activeTool.Cancel(CreateToolContext());
        _activeTool = tool;
        SetStatus(tool.Prompt);
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void SelectTool() => SetTool(_selectTool);

    public void DeleteSelected()
    {
        if (_selectedEntity is null)
        {
            return;
        }

        Commands.Execute(Document, new DeleteEntityCommand(_selectedEntity));
        _selectedEntity = null;
        SetStatus(_activeTool.Prompt);
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void Undo()
    {
        Commands.Undo(Document);
        _selectedEntity = null;
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void Redo()
    {
        Commands.Redo(Document);
        _selectedEntity = null;
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    public void AlignSelectedLineHorizontal() => AlignSelectedLine(horizontal: true);

    public void AlignSelectedLineVertical() => AlignSelectedLine(horizontal: false);

    public override void Render(DrawingContext context)
    {
        base.Render(context);
        context.DrawRectangle(_canvasBackground, null, Bounds);
        DrawReferenceImage(context);
        DrawGrid(context);

        foreach (var entity in Document.Entities)
        {
            DrawEntity(context, entity, entity.Id == _selectedEntity?.Id);
        }

        _activeTool.RenderOverlay(context, new ToolRenderContext(
            _transform,
            _hoverPoint,
            _hoverSnap,
            _transform.Scale,
            _keyModifiers.HasFlag(KeyModifiers.Shift)));
        if (_hoverSnap is not null)
        {
            ToolDrawing.DrawSnapMarker(context, _transform, _hoverSnap);
        }
    }

    protected override void OnPointerPressed(PointerPressedEventArgs e)
    {
        base.OnPointerPressed(e);
        Focus();

        var point = e.GetPosition(this);
        _keyModifiers = e.KeyModifiers;
        var properties = e.GetCurrentPoint(this).Properties;
        if (properties.IsMiddleButtonPressed || properties.IsRightButtonPressed)
        {
            BeginPan(e, point);
            return;
        }

        if (properties.IsLeftButtonPressed)
        {
            _activeTool.PointerDown(CreateToolContext(), _transform.ScreenToModel(point));
            Changed?.Invoke(this, EventArgs.Empty);
        }
    }

    protected override void OnPointerMoved(PointerEventArgs e)
    {
        base.OnPointerMoved(e);

        var point = e.GetPosition(this);
        _keyModifiers = e.KeyModifiers;
        if (_isPanning)
        {
            _transform.Pan(point - _lastPanPoint);
            _lastPanPoint = point;
            InvalidateVisual();
            return;
        }

        _hoverSnap = SnapPoint(_transform.ScreenToModel(point));
        _hoverPoint = _hoverSnap.Point;
        _activeTool.PointerMove(CreateToolContext(), _transform.ScreenToModel(point));
    }

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

        if (_isPanning)
        {
            _isPanning = false;
            e.Pointer.Capture(null);
            return;
        }

        _activeTool.PointerUp(CreateToolContext(), _transform.ScreenToModel(e.GetPosition(this)));
    }

    protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
    {
        base.OnPointerWheelChanged(e);
        var factor = e.Delta.Y > 0 ? 1.15 : 1.0 / 1.15;
        _transform.ZoomAt(e.GetPosition(this), factor);
        InvalidateVisual();
    }

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

        if (e.Key == Key.Escape)
        {
            _activeTool.Cancel(CreateToolContext());
            e.Handled = true;
            return;
        }

        if (e.Key == Key.Delete)
        {
            DeleteSelected();
            e.Handled = true;
            return;
        }

        if (e.KeyModifiers.HasFlag(KeyModifiers.Control) &&
            (e.Key == Key.Y || (e.KeyModifiers.HasFlag(KeyModifiers.Shift) && e.Key == Key.Z)))
        {
            Redo();
            e.Handled = true;
            return;
        }

        if (e.KeyModifiers.HasFlag(KeyModifiers.Control) && e.Key == Key.Z)
        {
            Undo();
            e.Handled = true;
            return;
        }

        _activeTool.KeyDown(CreateToolContext(), e.Key);
    }

    private void DrawGrid(DrawingContext context)
    {
        var bounds = Bounds;
        var topLeft = _transform.ScreenToModel(new Point(0.0, 0.0));
        var bottomRight = _transform.ScreenToModel(new Point(bounds.Width, bounds.Height));
        var spacing = GridSpacingMm();
        var startX = Math.Floor(topLeft.X / spacing) * spacing;
        var endX = Math.Ceiling(bottomRight.X / spacing) * spacing;
        var startY = Math.Floor(topLeft.Y / spacing) * spacing;
        var endY = Math.Ceiling(bottomRight.Y / spacing) * spacing;

        for (var x = startX; x <= endX; x += spacing)
        {
            var p1 = _transform.ModelToScreen(new Point2(x, topLeft.Y));
            var p2 = _transform.ModelToScreen(new Point2(x, bottomRight.Y));
            context.DrawLine(Math.Abs(x) < GeometryConstants.Epsilon ? _axisPen : _gridPen, p1, p2);
        }

        for (var y = startY; y <= endY; y += spacing)
        {
            var p1 = _transform.ModelToScreen(new Point2(topLeft.X, y));
            var p2 = _transform.ModelToScreen(new Point2(bottomRight.X, y));
            context.DrawLine(Math.Abs(y) < GeometryConstants.Epsilon ? _axisPen : _gridPen, p1, p2);
        }
    }

    private void AlignSelectedLine(bool horizontal)
    {
        if (_selectedEntity is not LineEntity line)
        {
            SetStatus(horizontal
                ? "Align Horizontal: select a line first"
                : "Align Vertical: select a line first");
            return;
        }

        var vector = line.End - line.Start;
        if (vector.Length <= GeometryConstants.Epsilon)
        {
            SetStatus("Align: selected line has zero length");
            return;
        }

        var currentAngle = Math.Atan2(vector.Y, vector.X) * 180.0 / Math.PI;
        var targetAngle = horizontal ? 0.0 : 90.0;
        var angle = targetAngle - currentAngle;
        var pivot = new Point2((line.Start.X + line.End.X) / 2.0, (line.Start.Y + line.End.Y) / 2.0);
        Commands.Execute(Document, new RotateDocumentCommand(angle, pivot));
        _selectedEntity = Document.Entities.FirstOrDefault(entity => entity.Id == line.Id);
        SetStatus(horizontal
            ? $"Aligned selected line horizontal; rotated drawing {angle:0.###} deg"
            : $"Aligned selected line vertical; rotated drawing {angle:0.###} deg");
        Changed?.Invoke(this, EventArgs.Empty);
        InvalidateVisual();
    }

    private void DrawReferenceImage(DrawingContext context)
    {
        if (_referenceBitmap is null || Document.Reference is null)
        {
            return;
        }

        var reference = Document.Reference;
        var transform = reference.Transform;
        var origin = _transform.ModelToScreen(new Point2(transform.OriginX, transform.OriginY));
        var width = _referenceBitmap.Size.Width * transform.ScaleX * _transform.Scale;
        var height = _referenceBitmap.Size.Height * transform.ScaleY * _transform.Scale;
        if (width <= 0.0 || height <= 0.0)
        {
            return;
        }

        using var opacity = context.PushOpacity(reference.Opacity);
        using var translate = context.PushTransform(Matrix.CreateTranslation(origin.X, origin.Y));
        using var rotate = context.PushTransform(Matrix.CreateRotation(transform.RotationDeg * Math.PI / 180.0));

        context.DrawImage(_referenceBitmap, new Rect(0.0, 0.0, width, height));
    }

    private double GridSpacingMm()
    {
        var spacing = 5.0;
        while (spacing * _transform.Scale < 18.0)
        {
            spacing *= 2.0;
        }

        return spacing;
    }

    private void DrawEntity(DrawingContext context, Entity entity, bool selected)
    {
        var pen = selected ? _selectedPen : _entityPen;

        switch (entity)
        {
            case LineEntity line:
                context.DrawLine(pen, _transform.ModelToScreen(line.Start), _transform.ModelToScreen(line.End));
                break;
            case CircleEntity circle:
                ToolDrawing.DrawCircle(context, _transform, circle.Center, circle.Radius, pen);
                break;
            case ArcEntity arc:
                ToolDrawing.DrawArc(context, _transform, arc.Center, arc.Radius, arc.StartAngleDeg, arc.EndAngleDeg, arc.IsClockwise, pen);
                break;
        }
    }

    private SnapResult SnapPoint(Point2 point)
    {
        var toleranceMm = 14.0 / _transform.Scale;
        SnapResult? best = null;
        var bestDistance = double.MaxValue;

        foreach (var snap in Document.Entities.SelectMany(GetSnapCandidates))
        {
            var distance = point.DistanceTo(snap.Point);
            if (distance < bestDistance && distance <= toleranceMm)
            {
                best = snap;
                bestDistance = distance;
            }
        }

        return best ?? SnapResult.None(point);
    }

    private static IEnumerable<SnapResult> GetSnapCandidates(Entity entity)
    {
        switch (entity)
        {
            case LineEntity line:
                yield return new SnapResult(
                    line.Start,
                    SnapKind.Endpoint,
                    line,
                    (line.Start - line.End).Normalized());
                yield return new SnapResult(
                    line.End,
                    SnapKind.Endpoint,
                    line,
                    (line.End - line.Start).Normalized());
                break;
            case ArcEntity arc:
                yield return new SnapResult(
                    arc.StartPoint,
                    SnapKind.Endpoint,
                    arc,
                    GeometryHelpers.TangentAtAngle(arc.StartAngleDeg, arc.IsClockwise) * -1.0);
                yield return new SnapResult(
                    arc.EndPoint,
                    SnapKind.Endpoint,
                    arc,
                    GeometryHelpers.TangentAtAngle(arc.EndAngleDeg, arc.IsClockwise));
                yield return new SnapResult(arc.Center, SnapKind.Center, arc);
                break;
            case CircleEntity circle:
                yield return new SnapResult(circle.Center, SnapKind.Center, circle);
                break;
        }
    }

    private Entity? HitTest(Point2 point, double tolerancePixels)
    {
        var toleranceMm = tolerancePixels / _transform.Scale;
        return Document.Entities.LastOrDefault(entity => DistanceToEntity(point, entity) <= toleranceMm);
    }

    private static double DistanceToEntity(Point2 point, Entity entity)
    {
        return entity switch
        {
            LineEntity line => DistanceToSegment(point, line.Start, line.End),
            CircleEntity circle => Math.Abs(point.DistanceTo(circle.Center) - circle.Radius),
            ArcEntity arc => DistanceToArc(point, arc),
            _ => double.MaxValue
        };
    }

    private static double DistanceToSegment(Point2 point, Point2 start, Point2 end)
    {
        var segment = end - start;
        var lengthSquared = segment.Dot(segment);
        if (lengthSquared <= GeometryConstants.Epsilon)
        {
            return point.DistanceTo(start);
        }

        var t = Math.Clamp((point - start).Dot(segment) / lengthSquared, 0.0, 1.0);
        var projected = start + (segment * t);
        return point.DistanceTo(projected);
    }

    private static double DistanceToArc(Point2 point, ArcEntity arc)
    {
        var angle = GeometryHelpers.AngleDeg(arc.Center, point);
        if (!GeometryHelpers.AngleIsOnSweep(arc.StartAngleDeg, arc.EndAngleDeg, angle, arc.IsClockwise))
        {
            return Math.Min(point.DistanceTo(arc.StartPoint), point.DistanceTo(arc.EndPoint));
        }

        return Math.Abs(point.DistanceTo(arc.Center) - arc.Radius);
    }

    private ToolContext CreateToolContext()
    {
        return new ToolContext(
            Document,
            Commands,
            SnapPoint,
            HitTest,
            () => _selectedEntity,
            entity =>
            {
                _selectedEntity = entity;
                Changed?.Invoke(this, EventArgs.Empty);
            },
            SetStatus,
            InvalidateVisual,
            _keyModifiers.HasFlag(KeyModifiers.Shift));
    }

    private void SetStatus(string status)
    {
        StatusText = status;
        StatusChanged?.Invoke(this, EventArgs.Empty);
    }

    private void BeginPan(PointerPressedEventArgs e, Point point)
    {
        _isPanning = true;
        _lastPanPoint = point;
        e.Pointer.Capture(this);
        SetStatus("Pan: drag to move canvas");
    }

    private void LoadReferenceBitmap()
    {
        _referenceBitmap?.Dispose();
        _referenceBitmap = null;

        if (Document.Reference is null)
        {
            return;
        }

        try
        {
            _referenceBitmap = new Bitmap(Document.Reference.ImagePath);
        }
        catch
        {
            SetStatus($"Reference image not found: {Document.Reference.ImagePath}");
        }
    }
}
