Code/Core/PanelShape.cs
using System;
using Sandbox;
using Sandbox.UI;

namespace Goo;

public enum PanelShapeKind
{
    None,
    Circle,
    Polygon,
}

public readonly struct PanelShapePoint : IEquatable<PanelShapePoint>
{
    public Length X { get; }
    public Length Y { get; }

    public PanelShapePoint(Length x, Length y)
    {
        UiCssSerializer.ValidateLength(x, nameof(x));
        UiCssSerializer.ValidateLength(y, nameof(y));
        X = x;
        Y = y;
    }

    public static PanelShapePoint Percent(float x, float y)
        => new(
            new Length { Value = x, Unit = LengthUnit.Percentage },
            new Length { Value = y, Unit = LengthUnit.Percentage });

    public static PanelShapePoint Pixels(float x, float y)
        => new(
            new Length { Value = x, Unit = LengthUnit.Pixels },
            new Length { Value = y, Unit = LengthUnit.Pixels });

    public static implicit operator PanelShapePoint(Vector2 unitPoint)
        => Percent(unitPoint.x * 100f, unitPoint.y * 100f);

    public bool Equals(PanelShapePoint other) => X == other.X && Y == other.Y;
    public override bool Equals(object? obj) => obj is PanelShapePoint other && Equals(other);
    public override int GetHashCode() => HashCode.Combine(X, Y);
    public static bool operator ==(PanelShapePoint left, PanelShapePoint right) => left.Equals(right);
    public static bool operator !=(PanelShapePoint left, PanelShapePoint right) => !left.Equals(right);
}

public readonly struct PanelShape : IEquatable<PanelShape>
{
    const int MaxPoints = 8;

    readonly PanelShapePoint[]? _points;

    public static readonly PanelShape None = default;

    public PanelShapeKind Kind { get; }
    public Length? CircleRadius { get; }
    public Length CircleCenterX { get; }
    public Length CircleCenterY { get; }
    public int Count => _points?.Length ?? 0;
    public PanelShapePoint this[int index] => _points![index];

    PanelShape(
        PanelShapeKind kind,
        Length? circleRadius,
        Length circleCenterX,
        Length circleCenterY,
        PanelShapePoint[]? points)
    {
        Kind = kind;
        CircleRadius = circleRadius;
        CircleCenterX = circleCenterX;
        CircleCenterY = circleCenterY;
        _points = points;
    }

    public static PanelShape Circle(Length? radius = null, Length? centerX = null, Length? centerY = null)
    {
        if (radius.HasValue)
        {
            UiCssSerializer.ValidateLength(radius.Value, nameof(radius));
            if (radius.Value.Value < 0f)
                throw new ArgumentOutOfRangeException(nameof(radius));
        }

        var center = new Length { Value = 50f, Unit = LengthUnit.Percentage };
        var x = centerX ?? center;
        var y = centerY ?? center;
        UiCssSerializer.ValidateLength(x, nameof(centerX));
        UiCssSerializer.ValidateLength(y, nameof(centerY));
        return new PanelShape(PanelShapeKind.Circle, radius, x, y, null);
    }

    public static PanelShape Polygon(params Vector2[] points)
    {
        if (points is null)
            throw new ArgumentNullException(nameof(points));
        var typed = new PanelShapePoint[points.Length];
        for (int i = 0; i < points.Length; i++) typed[i] = points[i];
        return Polygon(typed);
    }

    public static PanelShape Polygon(params PanelShapePoint[] points)
    {
        if (points is null || points.Length < 3 || points.Length > MaxPoints)
            throw new ArgumentException($"Panel polygons require 3 to {MaxPoints} points.", nameof(points));
        var copy = new PanelShapePoint[points.Length];
        for (int i = 0; i < points.Length; i++)
        {
            UiCssSerializer.ValidateLength(points[i].X, nameof(points));
            UiCssSerializer.ValidateLength(points[i].Y, nameof(points));
            copy[i] = points[i];
        }
        return new PanelShape(PanelShapeKind.Polygon, null, default, default, copy);
    }

    internal string ToCss()
    {
        if (Kind == PanelShapeKind.None) return "none";
        if (Kind == PanelShapeKind.Circle)
        {
            var radius = CircleRadius.HasValue ? UiCssSerializer.Length(CircleRadius.Value) + " " : "";
            return $"circle({radius}at {UiCssSerializer.Length(CircleCenterX)} {UiCssSerializer.Length(CircleCenterY)})";
        }

        var css = new System.Text.StringBuilder("polygon(");
        for (int i = 0; i < Count; i++)
        {
            if (i > 0) css.Append(", ");
            css.Append(UiCssSerializer.Length(this[i].X)).Append(' ').Append(UiCssSerializer.Length(this[i].Y));
        }
        return css.Append(')').ToString();
    }

    public bool Equals(PanelShape other)
    {
        if (Kind != other.Kind) return false;
        if (Kind == PanelShapeKind.Circle)
            return CircleRadius == other.CircleRadius
                && CircleCenterX == other.CircleCenterX
                && CircleCenterY == other.CircleCenterY;
        if (Count != other.Count) return false;
        for (int i = 0; i < Count; i++)
            if (this[i] != other[i]) return false;
        return true;
    }

    public override bool Equals(object? obj) => obj is PanelShape other && Equals(other);

    public override int GetHashCode()
    {
        var hash = new HashCode();
        hash.Add(Kind);
        if (Kind == PanelShapeKind.Circle)
        {
            hash.Add(CircleRadius);
            hash.Add(CircleCenterX);
            hash.Add(CircleCenterY);
        }
        else
        {
            for (int i = 0; i < Count; i++) hash.Add(this[i]);
        }
        return hash.ToHashCode();
    }

    public static bool operator ==(PanelShape left, PanelShape right) => left.Equals(right);
    public static bool operator !=(PanelShape left, PanelShape right) => !left.Equals(right);
}