using System.Globalization;
using System.Text.Json;

namespace Ehwrj.Core.Models;

public sealed class MapObject
{
    public required string Id { get; init; }
    public string? Name { get; init; }
    public string? Type { get; init; }
    public string? Icon { get; init; }
    public string? Team { get; init; }
    public string? Color { get; init; }
    public double? X { get; init; }
    public double? Y { get; init; }
    public double? DirectionX { get; init; }
    public double? DirectionY { get; init; }
    public double? HeadingRadians { get; init; }
    public double? Mach { get; init; }
    public double? ClosureSpeed { get; init; }
    public double? ClimbAngleDegrees { get; init; }
    public MapObjectKind Kind { get; init; }
    public bool IsPlayer { get; init; }
    public bool IsAircraft { get; init; }
    public bool IsObjective { get; init; }
    public bool IsEnemyBombingPoint { get; init; }
    public string RawJson { get; init; } = "{}";

    public static IReadOnlyList<MapObject> FromJson(string json, ObjectTracker tracker, FlightState? flightState = null)
    {
        using var document = JsonDocument.Parse(json);
        var root = document.RootElement;
        var source = root.ValueKind == JsonValueKind.Array
            ? root.EnumerateArray()
            : root.ValueKind == JsonValueKind.Object && root.TryGetProperty("objects", out var objects) && objects.ValueKind == JsonValueKind.Array
                ? objects.EnumerateArray()
                : Array.Empty<JsonElement>().AsEnumerable();

        return source.Select((item, index) => FromElement(item, index, tracker, flightState ?? FlightState.Empty)).ToArray();
    }

    private static MapObject FromElement(JsonElement element, int index, ObjectTracker tracker, FlightState flightState)
    {
        var name = ReadString(element, "name", "title", "label");
        var type = ReadString(element, "type", "object_type");
        var icon = ReadString(element, "icon", "icon_type");
        var team = ReadString(element, "team", "army", "side");
        var color = ReadString(element, "color", "colour");
        var id = ReadString(element, "id", "uid") ?? StableId(name, type, icon, index);
        var x = ReadNumber(element, "x", "sx", "lon");
        var y = ReadNumber(element, "y", "sy", "lat");
        ReadVector(element, ref x, ref y, "pos", "position", "coord", "coords", "location");

        var dx = ReadNumber(element, "dx", "dir_x", "vx");
        var dy = ReadNumber(element, "dy", "dir_y", "vy");
        ReadVector(element, ref dx, ref dy, "dir", "direction", "velocity", "vector");
        var heading = NormalizeHeading(ReadNumber(element, "angle", "rotation", "dir", "heading"));

        var classifier = $"{name} {type} {icon} {team}".ToLowerInvariant();
        var kind = Classify(classifier, color);
        var isPlayer = kind == MapObjectKind.Player;
        var isAircraft = classifier.Contains("air", StringComparison.Ordinal) ||
                         classifier.Contains("plane", StringComparison.Ordinal) ||
                         classifier.Contains("fighter", StringComparison.Ordinal) ||
                         classifier.Contains("bomber", StringComparison.Ordinal) ||
                         classifier.Contains("attacker", StringComparison.Ordinal) ||
                         classifier.Contains("interceptor", StringComparison.Ordinal) ||
                         classifier.Contains("player", StringComparison.Ordinal);
        var isEnemyBombingPoint = classifier.Contains("bombing_point", StringComparison.Ordinal) &&
                                  kind == MapObjectKind.Enemy;
        var isObjective = kind == MapObjectKind.Objective ||
                          isEnemyBombingPoint ||
                          classifier.Contains("capture", StringComparison.Ordinal) ||
                          classifier.Contains("zone", StringComparison.Ordinal) ||
                          classifier.Contains("objective", StringComparison.Ordinal);

        var motion = tracker.Update(id, x, y);
        var mach = isPlayer && flightState.Mach.HasValue
            ? flightState.Mach
            : motion.Mach;
        var climbAngle = isPlayer ? flightState.ClimbAngleDegrees : null;

        return new MapObject
        {
            Id = id,
            Name = name,
            Type = type,
            Icon = icon,
            Team = team,
            Color = color,
            X = x,
            Y = y,
            DirectionX = dx,
            DirectionY = dy,
            HeadingRadians = heading,
            Mach = mach,
            ClosureSpeed = motion.ClosureSpeed,
            ClimbAngleDegrees = climbAngle,
            Kind = kind,
            IsPlayer = isPlayer,
            IsAircraft = isAircraft,
            IsObjective = isObjective,
            IsEnemyBombingPoint = isEnemyBombingPoint,
            RawJson = element.GetRawText()
        };
    }

    private static MapObjectKind Classify(string classifier, string? color)
    {
        if (classifier.Contains("player", StringComparison.Ordinal) ||
            classifier.Contains("own", StringComparison.Ordinal) ||
            classifier.Contains("self", StringComparison.Ordinal))
        {
            return MapObjectKind.Player;
        }

        if (classifier.Contains("squad", StringComparison.Ordinal))
        {
            return MapObjectKind.Squad;
        }

        if (classifier.Contains("enemy", StringComparison.Ordinal) ||
            classifier.Contains("hostile", StringComparison.Ordinal) ||
            classifier.Contains("bombing_point", StringComparison.Ordinal))
        {
            return MapObjectKind.Enemy;
        }

        if (classifier.Contains("ally", StringComparison.Ordinal) ||
            classifier.Contains("friendly", StringComparison.Ordinal) ||
            classifier.Contains("defending_point", StringComparison.Ordinal))
        {
            return MapObjectKind.Ally;
        }

        if (classifier.Contains("capture", StringComparison.Ordinal) ||
            classifier.Contains("objective", StringComparison.Ordinal) ||
            classifier.Contains("zone", StringComparison.Ordinal))
        {
            return MapObjectKind.Objective;
        }

        var colorKind = ClassifyColor(color);
        if (colorKind != MapObjectKind.Unknown)
        {
            return colorKind;
        }

        return MapObjectKind.Unknown;
    }

    private static MapObjectKind ClassifyColor(string? color)
    {
        if (string.IsNullOrWhiteSpace(color))
        {
            return MapObjectKind.Unknown;
        }

        var text = color.Trim();
        if (text.StartsWith('#') && text.Length == 7)
        {
            var r = Convert.ToInt32(text[1..3], 16);
            var g = Convert.ToInt32(text[3..5], 16);
            var b = Convert.ToInt32(text[5..7], 16);
            return ClassifyRgb(r, g, b);
        }

        return MapObjectKind.Unknown;
    }

    private static MapObjectKind ClassifyRgb(int r, int g, int b)
    {
        if (r > 210 && g > 210 && b > 210)
        {
            return MapObjectKind.Player;
        }

        if (r > 160 && r > g * 1.25 && r > b * 1.25)
        {
            return MapObjectKind.Enemy;
        }

        if (g > 120 && b > 120 && r < 120)
        {
            return MapObjectKind.Ally;
        }

        if (r > 185 && g > 145 && b < 130)
        {
            return MapObjectKind.Player;
        }

        return MapObjectKind.Unknown;
    }

    private static double? NormalizeHeading(double? heading)
    {
        if (!heading.HasValue || !double.IsFinite(heading.Value))
        {
            return null;
        }

        var value = heading.Value;
        if (Math.Abs(value) > Math.Tau)
        {
            value *= Math.PI / 180.0;
        }

        while (value < -Math.PI) value += Math.Tau;
        while (value > Math.PI) value -= Math.Tau;
        return value;
    }

    private static string StableId(string? name, string? type, string? icon, int index)
    {
        return $"{name ?? "object"}:{type ?? "unknown"}:{icon ?? "none"}:{index}";
    }

    private static string? ReadString(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (!element.TryGetProperty(name, out var value)) continue;
            if (value.ValueKind == JsonValueKind.String) return value.GetString();
            if (value.ValueKind is JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False) return value.ToString();
        }

        return null;
    }

    private static double? ReadNumber(JsonElement element, params string[] names)
    {
        foreach (var name in names)
        {
            if (!element.TryGetProperty(name, out var value)) continue;
            if (value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var number)) return number;
            if (value.ValueKind == JsonValueKind.String)
            {
                var parsed = ParseNumber(value.GetString());
                if (parsed.HasValue) return parsed;
            }
        }

        return null;
    }

    private static void ReadVector(JsonElement element, ref double? x, ref double? y, params string[] names)
    {
        if (x.HasValue && y.HasValue)
        {
            return;
        }

        foreach (var name in names)
        {
            if (!element.TryGetProperty(name, out var value) ||
                value.ValueKind != JsonValueKind.Array ||
                value.GetArrayLength() < 2)
            {
                continue;
            }

            x ??= ReadNumberValue(value[0]);
            y ??= ReadNumberValue(value[1]);
            return;
        }
    }

    private static double? ReadNumberValue(JsonElement value)
    {
        return value.ValueKind switch
        {
            JsonValueKind.Number when value.TryGetDouble(out var number) => number,
            JsonValueKind.String => ParseNumber(value.GetString()),
            _ => null
        };
    }

    private static double? ParseNumber(string? text)
    {
        if (string.IsNullOrWhiteSpace(text))
        {
            return null;
        }

        if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
        {
            return value;
        }

        var normalized = text.Replace(',', '.');
        return double.TryParse(normalized, NumberStyles.Float, CultureInfo.InvariantCulture, out value)
            ? value
            : null;
    }
}
