using System.ComponentModel;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.VisualTree;
using Ehwrj.App.Models;
using Ehwrj.Core.Geometry;
using Ehwrj.Core.Models;

namespace Ehwrj.App.Rendering;

public class MapCanvas : Control
{
    private MapSettings? _subscribedMapSettings;
    private OverlaySettings? _subscribedOverlaySettings;

    public static readonly StyledProperty<LiveSnapshot> SnapshotProperty = AvaloniaProperty.Register<MapCanvas, LiveSnapshot>(
        nameof(Snapshot),
        LiveSnapshot.Empty);

    public static readonly StyledProperty<OverlaySettings> OverlaySettingsProperty = AvaloniaProperty.Register<MapCanvas, OverlaySettings>(
        nameof(OverlaySettings),
        new OverlaySettings());

    public static readonly StyledProperty<MapSettings> MapSettingsProperty = AvaloniaProperty.Register<MapCanvas, MapSettings>(
        nameof(MapSettings),
        new MapSettings());

    public static readonly StyledProperty<UiText> UiProperty = AvaloniaProperty.Register<MapCanvas, UiText>(
        nameof(Ui),
        UiText.For("en-US"));

    public LiveSnapshot Snapshot
    {
        get => GetValue(SnapshotProperty);
        set => SetValue(SnapshotProperty, value);
    }

    public OverlaySettings OverlaySettings
    {
        get => GetValue(OverlaySettingsProperty);
        set => SetValue(OverlaySettingsProperty, value);
    }

    public MapSettings MapSettings
    {
        get => GetValue(MapSettingsProperty);
        set => SetValue(MapSettingsProperty, value);
    }

    public UiText Ui
    {
        get => GetValue(UiProperty);
        set => SetValue(UiProperty, value);
    }

    static MapCanvas()
    {
        AffectsRender<MapCanvas>(SnapshotProperty, OverlaySettingsProperty, MapSettingsProperty, UiProperty);
    }

    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        base.OnPropertyChanged(change);

        if (change.Property == OverlaySettingsProperty)
        {
            SubscribeOverlaySettings(change.NewValue as OverlaySettings);
        }
        else if (change.Property == MapSettingsProperty)
        {
            SubscribeMapSettings(change.NewValue as MapSettings);
        }
    }

    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
    {
        SubscribeMapSettings(null);
        SubscribeOverlaySettings(null);
        base.OnDetachedFromVisualTree(e);
    }

    public override void Render(DrawingContext context)
    {
        base.Render(context);

        var bounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
        context.FillRectangle(new SolidColorBrush(Color.FromRgb(7, 16, 20)), bounds);

        var view = CreateView(bounds);
        DrawRotatedMap(context, view);
        DrawRangeRings(context, view);
        DrawDeliveryTracker(context, view);
        DrawObjects(context, view, detailed: true);
        DrawStatus(context, bounds);
    }

    private ViewState CreateView(Rect bounds)
    {
        var imageSize = Snapshot.MapImage?.Size ?? new Size(1, 1);
        var rect = Fit(bounds, imageSize.Width, imageSize.Height, 0.96);
        var player = Snapshot.Player;

        if (MapSettings.FollowPlayer && player is not null)
        {
            var projected = ProjectPointRaw(player, Snapshot, rect);
            if (projected is not null)
            {
                const double scale = 1.45;
                var width = rect.Width * scale;
                var height = rect.Height * scale;
                var normalizedX = (projected.Value.X - rect.Left) / Math.Max(1, rect.Width);
                var normalizedY = (projected.Value.Y - rect.Top) / Math.Max(1, rect.Height);
                rect = new Rect(
                    bounds.Center.X - normalizedX * width,
                    bounds.Center.Y - normalizedY * height,
                    width,
                    height);
            }
        }

        var rotation = MapSettings.RotateWithPlayer && player is not null
            ? ObjectHeading(player) ?? 0
            : 0;

        return new ViewState(rect, bounds.Center, rotation);
    }

    private void DrawRotatedMap(DrawingContext context, ViewState view)
    {
        if (Math.Abs(view.RotationRadians) < 0.0001)
        {
            DrawMap(context, view.Rect);
            return;
        }

        using (context.PushTransform(Matrix.CreateRotation(-view.RotationRadians, view.Center)))
        {
            DrawMap(context, view.Rect);
        }
    }

    private void DrawMap(DrawingContext context, Rect rect)
    {
        if (Snapshot.MapImage is not null)
        {
            context.DrawImage(Snapshot.MapImage, new Rect(Snapshot.MapImage.Size), rect);
        }
        else
        {
            context.FillRectangle(new SolidColorBrush(Color.FromRgb(10, 26, 32)), rect);
            DrawCenteredText(context, Ui.WaitingForMapImage, rect, 18, Colors.White);
        }

        var gridPen = new Pen(new SolidColorBrush(Color.FromArgb(60, 230, 237, 243)), 1);
        for (var i = 1; i < 6; i++)
        {
            var x = rect.Left + rect.Width * i / 6;
            var y = rect.Top + rect.Height * i / 6;
            context.DrawLine(gridPen, new Point(x, rect.Top), new Point(x, rect.Bottom));
            context.DrawLine(gridPen, new Point(rect.Left, y), new Point(rect.Right, y));
        }

        context.DrawRectangle(null, new Pen(new SolidColorBrush(Color.FromArgb(110, 230, 237, 243)), 1), rect);
    }

    protected void DrawObjects(DrawingContext context, ViewState view, bool detailed)
    {
        foreach (var obj in Snapshot.Objects)
        {
            var point = ProjectPoint(obj, Snapshot, view);
            if (point is null) continue;

            var brush = new SolidColorBrush(ObjectColor(obj));

            var radius = obj.IsPlayer ? 7 : obj.IsAircraft ? 5 : 4;
            context.DrawEllipse(brush, new Pen(Brushes.Black, 1), point.Value, radius, radius);

            if (detailed && MapSettings.ShowAircraftMach && obj.Mach.HasValue && obj.IsAircraft)
            {
                DrawText(context, $"M {obj.Mach.Value:0.0}", new Point(point.Value.X + 9, point.Value.Y - 10), 12, Colors.White);
            }

            if (detailed && MapSettings.ShowLabels)
            {
                var label = obj.Name ?? obj.Icon ?? obj.Type;
                if (!string.IsNullOrWhiteSpace(label))
                {
                    DrawText(context, label, new Point(point.Value.X + 9, point.Value.Y + 4), 12, Color.FromRgb(220, 229, 238));
                }
            }
        }
    }

    private void DrawRangeRings(DrawingContext context, ViewState view)
    {
        if (!MapSettings.ShowRangeRings || Snapshot.Player is null)
        {
            return;
        }

        var playerPoint = ProjectPoint(Snapshot.Player, Snapshot, view);
        if (playerPoint is null)
        {
            return;
        }

        var pen = new Pen(new SolidColorBrush(Color.FromArgb(75, 230, 237, 243)), 1);
        var radiusUnit = Math.Min(view.Rect.Width, view.Rect.Height) * 0.1;
        for (var i = 1; i <= 3; i++)
        {
            var radius = radiusUnit * i;
            context.DrawEllipse(null, pen, playerPoint.Value, radius, radius);
            DrawText(context, $"{i * 10} km", new Point(playerPoint.Value.X + radius + 4, playerPoint.Value.Y - 8), 11, Color.FromArgb(160, 230, 237, 243));
        }
    }

    private void DrawDeliveryTracker(DrawingContext context, ViewState view)
    {
        if (!MapSettings.ShowDeliveryTracker)
        {
            return;
        }

        var friendlyAircraft = Snapshot.Objects.Where(static o =>
            o.IsAircraft && o.Kind is MapObjectKind.Ally or MapObjectKind.Squad or MapObjectKind.Player);
        var targets = Snapshot.Objects.Where(static o =>
            o.IsEnemyBombingPoint || o.Kind == MapObjectKind.Enemy && (o.IsObjective || !o.IsAircraft));
        var pen = new Pen(new SolidColorBrush(Color.FromArgb(170, 255, 212, 94)), 2);

        foreach (var aircraft in friendlyAircraft)
        {
            var start = ProjectPoint(aircraft, Snapshot, view);
            var forward = HeadingVector(aircraft);
            if (start is null || forward is null)
            {
                continue;
            }

            foreach (var target in targets)
            {
                var end = ProjectPoint(target, Snapshot, view);
                if (end is null)
                {
                    continue;
                }

                var dx = end.Value.X - start.Value.X;
                var dy = end.Value.Y - start.Value.Y;
                var distance = Math.Sqrt(dx * dx + dy * dy);
                if (distance < 1)
                {
                    continue;
                }

                var dot = (dx * forward.Value.X + dy * forward.Value.Y) / distance;
                if (dot < MapSettings.DeliveryTrackerAngle)
                {
                    continue;
                }

                context.DrawLine(pen, start.Value, end.Value);
            }
        }
    }

    private void DrawStatus(DrawingContext context, Rect bounds)
    {
        var status = Snapshot.IsGameRunning
            ? Ui.FormatStatus(Snapshot.Status)
            : Ui.StartWarThunderToEnableLiveData;
        DrawText(context, status, new Point(18, bounds.Bottom - 32), 13, Color.FromRgb(147, 164, 179));
    }

    protected static Rect Fit(Rect bounds, double imageWidth, double imageHeight, double fill)
    {
        var targetWidth = bounds.Width * fill;
        var targetHeight = bounds.Height * fill;
        var scale = Math.Min(targetWidth / Math.Max(1, imageWidth), targetHeight / Math.Max(1, imageHeight));
        var width = imageWidth * scale;
        var height = imageHeight * scale;
        return new Rect(bounds.Left + (bounds.Width - width) / 2, bounds.Top + (bounds.Height - height) / 2, width, height);
    }

    protected static Point? ProjectPoint(MapObject obj, LiveSnapshot snapshot, ViewState view)
    {
        var point = ProjectPointRaw(obj, snapshot, view.Rect);
        return point.HasValue ? RotateAround(point.Value, view.Center, -view.RotationRadians) : null;
    }

    protected static Point? ProjectPointRaw(MapObject obj, LiveSnapshot snapshot, Rect rect)
    {
        var viewport = new MapViewport(rect.Left, rect.Top, rect.Width, rect.Height);
        var projected = CoordinateProjector.Project(obj, snapshot.MapInfo, viewport);
        return projected.HasValue ? new Point(projected.Value.X, projected.Value.Y) : null;
    }

    protected static void DrawCenteredText(DrawingContext context, string text, Rect rect, double size, Color color)
    {
        var formatted = CreateText(text, size, color);
        context.DrawText(formatted, new Point(rect.Left + (rect.Width - formatted.Width) / 2, rect.Top + (rect.Height - formatted.Height) / 2));
    }

    protected static void DrawText(DrawingContext context, string text, Point point, double size, Color color)
    {
        context.DrawText(CreateText(text, size, color), point);
    }

    protected static void DrawOutlinedText(
        DrawingContext context,
        string text,
        Point point,
        double size,
        Color color,
        double outlineWidth,
        double opacityPercent)
    {
        var foreground = WithOpacity(color, opacityPercent);
        if (outlineWidth > 0)
        {
            var outline = WithOpacity(Colors.Black, Math.Min(100, opacityPercent + 20));
            var offsets = new[]
            {
                new Point(-outlineWidth, 0),
                new Point(outlineWidth, 0),
                new Point(0, -outlineWidth),
                new Point(0, outlineWidth),
                new Point(-outlineWidth, -outlineWidth),
                new Point(outlineWidth, outlineWidth)
            };

            foreach (var offset in offsets)
            {
                DrawText(context, text, new Point(point.X + offset.X, point.Y + offset.Y), size, outline);
            }
        }

        DrawText(context, text, point, size, foreground);
    }

    protected static Color ParseColor(string? value, Color fallback)
    {
        return Color.TryParse(value, out var color) ? color : fallback;
    }

    protected static Color ObjectColor(MapObject obj)
    {
        return obj.Kind switch
        {
            MapObjectKind.Player => Color.FromRgb(246, 200, 95),
            MapObjectKind.Ally => Color.FromRgb(76, 201, 167),
            MapObjectKind.Squad => Color.FromRgb(120, 156, 255),
            MapObjectKind.Enemy => Color.FromRgb(255, 82, 82),
            MapObjectKind.Objective => Color.FromRgb(222, 202, 132),
            _ => obj.IsAircraft ? Color.FromRgb(255, 82, 82) : Color.FromRgb(222, 202, 132)
        };
    }

    protected static double? ObjectHeading(MapObject obj)
    {
        if (obj.DirectionX.HasValue && obj.DirectionY.HasValue)
        {
            return Math.Atan2(obj.DirectionY.Value, obj.DirectionX.Value);
        }

        return obj.HeadingRadians;
    }

    protected static Point? HeadingVector(MapObject obj)
    {
        var heading = ObjectHeading(obj);
        if (!heading.HasValue)
        {
            return null;
        }

        return new Point(Math.Cos(heading.Value), Math.Sin(heading.Value));
    }

    private static Point RotateAround(Point point, Point center, double radians)
    {
        if (Math.Abs(radians) < 0.0001)
        {
            return point;
        }

        var cos = Math.Cos(radians);
        var sin = Math.Sin(radians);
        var dx = point.X - center.X;
        var dy = point.Y - center.Y;
        return new Point(
            center.X + dx * cos - dy * sin,
            center.Y + dx * sin + dy * cos);
    }

    protected static FormattedText CreateText(string text, double size, Color color)
    {
        return new FormattedText(
            text,
            CultureInfo.InvariantCulture,
            FlowDirection.LeftToRight,
            new Typeface("Inter"),
            size,
            new SolidColorBrush(color));
    }

    private static Color WithOpacity(Color color, double opacityPercent)
    {
        var alpha = (byte)Math.Clamp(opacityPercent / 100.0 * 255, 0, 255);
        return Color.FromArgb(alpha, color.R, color.G, color.B);
    }

    private void SubscribeOverlaySettings(OverlaySettings? settings)
    {
        if (ReferenceEquals(_subscribedOverlaySettings, settings))
        {
            return;
        }

        if (_subscribedOverlaySettings is not null)
        {
            _subscribedOverlaySettings.PropertyChanged -= OnOverlaySettingsChanged;
        }

        _subscribedOverlaySettings = settings;
        if (_subscribedOverlaySettings is not null)
        {
            _subscribedOverlaySettings.PropertyChanged += OnOverlaySettingsChanged;
        }
    }

    private void SubscribeMapSettings(MapSettings? settings)
    {
        if (ReferenceEquals(_subscribedMapSettings, settings))
        {
            return;
        }

        if (_subscribedMapSettings is not null)
        {
            _subscribedMapSettings.PropertyChanged -= OnMapSettingsChanged;
        }

        _subscribedMapSettings = settings;
        if (_subscribedMapSettings is not null)
        {
            _subscribedMapSettings.PropertyChanged += OnMapSettingsChanged;
        }
    }

    private void OnMapSettingsChanged(object? sender, PropertyChangedEventArgs e)
    {
        InvalidateVisual();
    }

    private void OnOverlaySettingsChanged(object? sender, PropertyChangedEventArgs e)
    {
        InvalidateVisual();
    }
}

public readonly record struct ViewState(Rect Rect, Point Center, double RotationRadians);
