using System;
using System.Numerics;
using Windows.Foundation;

namespace Sonex.Client.Controls.SceneCanvas;

public abstract class SceneCanvasObject
{
    private Rect _boundsMeters;
    private double _rotationRadians;
    private string _id = CreateGuidId();
    private int _layoutId;

    public string Id
    {
        get => _id;
        set
        {
            if (Guid.TryParse(value, out var parsed))
            {
                _id = parsed.ToString("N");
                return;
            }

            _id = CreateGuidId();
        }
    }

    public bool IsVisible { get; set; } = true;

    public int LayoutId
    {
        get => _layoutId;
        set => _layoutId = value;
    }

    public int Layer
    {
        get => _layoutId;
        set => _layoutId = value;
    }

    public int ZIndex { get; set; }

    public bool CanMove { get; set; } = true;

    public bool CanResize { get; set; }

    public bool CanRotate { get; set; }

    public double PositionZMeters { get; set; }

    public double HeightMeters { get; set; }

    public Rect BoundsMeters
    {
        get => _boundsMeters;
        set => _boundsMeters = NormalizeRect(value);
    }

    public double RotationRadians
    {
        get => _rotationRadians;
        set => _rotationRadians = double.IsFinite(value) ? value : 0d;
    }

    public virtual Rect SpatialBoundsMeters => CalculateSpatialBounds(BoundsMeters, RotationRadians);

    internal int LastQueryToken { get; set; }

    protected SceneCanvasObject()
    {
        _boundsMeters = new Rect(0, 0, 0, 0);
    }

    protected SceneCanvasObject(Rect boundsMeters)
    {
        _boundsMeters = NormalizeRect(boundsMeters);
    }

    public virtual bool HitTest(Vector2 worldPointMeters, float toleranceMeters = 0.05f)
    {
        if (Math.Abs(RotationRadians) > 0.0001d)
        {
            var center = new Vector2(
                (float)(BoundsMeters.X + (BoundsMeters.Width * 0.5d)),
                (float)(BoundsMeters.Y + (BoundsMeters.Height * 0.5d)));

            worldPointMeters = RotatePoint(worldPointMeters, center, -RotationRadians);
        }

        var left = BoundsMeters.X - toleranceMeters;
        var right = BoundsMeters.X + BoundsMeters.Width + toleranceMeters;
        var top = BoundsMeters.Y - toleranceMeters;
        var bottom = BoundsMeters.Y + BoundsMeters.Height + toleranceMeters;

        return worldPointMeters.X >= left &&
               worldPointMeters.X <= right &&
               worldPointMeters.Y >= top &&
               worldPointMeters.Y <= bottom;
    }

    // The drawing session is already transformed from world meters to screen space.
    public abstract void OnDraw(SceneCanvasDrawContext context);

    public abstract SceneCanvasObject Clone();

    protected void CopyBasePropertiesTo(SceneCanvasObject target)
    {
        if (target == null)
        {
            return;
        }

        target.IsVisible = IsVisible;
        target.LayoutId = LayoutId;
        target.ZIndex = ZIndex;
        target.CanMove = CanMove;
        target.CanResize = CanResize;
        target.CanRotate = CanRotate;
        target.PositionZMeters = PositionZMeters;
        target.HeightMeters = HeightMeters;
        target.BoundsMeters = BoundsMeters;
        target.RotationRadians = RotationRadians;
    }

    private static Rect CalculateSpatialBounds(Rect bounds, double rotationRadians)
    {
        if (Math.Abs(rotationRadians) <= 0.0001d)
        {
            return bounds;
        }

        var center = new Vector2(
            (float)(bounds.X + (bounds.Width * 0.5d)),
            (float)(bounds.Y + (bounds.Height * 0.5d)));

        var c1 = RotatePoint(new Vector2((float)bounds.X, (float)bounds.Y), center, rotationRadians);
        var c2 = RotatePoint(new Vector2((float)(bounds.X + bounds.Width), (float)bounds.Y), center, rotationRadians);
        var c3 = RotatePoint(new Vector2((float)(bounds.X + bounds.Width), (float)(bounds.Y + bounds.Height)), center, rotationRadians);
        var c4 = RotatePoint(new Vector2((float)bounds.X, (float)(bounds.Y + bounds.Height)), center, rotationRadians);

        var minX = Math.Min(Math.Min(c1.X, c2.X), Math.Min(c3.X, c4.X));
        var minY = Math.Min(Math.Min(c1.Y, c2.Y), Math.Min(c3.Y, c4.Y));
        var maxX = Math.Max(Math.Max(c1.X, c2.X), Math.Max(c3.X, c4.X));
        var maxY = Math.Max(Math.Max(c1.Y, c2.Y), Math.Max(c3.Y, c4.Y));

        return new Rect(minX, minY, Math.Max(0d, maxX - minX), Math.Max(0d, maxY - minY));
    }

    private static Vector2 RotatePoint(Vector2 point, Vector2 center, double angleRadians)
    {
        var translated = point - center;
        var sin = Math.Sin(angleRadians);
        var cos = Math.Cos(angleRadians);

        var rotated = new Vector2(
            (float)((translated.X * cos) - (translated.Y * sin)),
            (float)((translated.X * sin) + (translated.Y * cos)));

        return rotated + center;
    }

    private static Rect NormalizeRect(Rect rect)
    {
        var x = double.IsFinite(rect.X) ? rect.X : 0d;
        var y = double.IsFinite(rect.Y) ? rect.Y : 0d;
        var width = double.IsFinite(rect.Width) ? rect.Width : 0d;
        var height = double.IsFinite(rect.Height) ? rect.Height : 0d;

        if (width < 0d)
        {
            x += width;
            width = -width;
        }

        if (height < 0d)
        {
            y += height;
            height = -height;
        }

        return new Rect(x, y, width, height);
    }

    private static string CreateGuidId()
    {
        return Guid.NewGuid().ToString("N");
    }
}
