using System.Collections.Generic;
using Intrepid.Gameplay.Journey;
using UnityEngine;

namespace Editor.GON.WorldEditor
{
    public static class WorldLayoutValidator
    {
        public static List<WorldValidationMessage> Validate(WorldLayoutSO layout)
        {
            var messages = new List<WorldValidationMessage>();
            if (layout is null)
            {
                messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error, "No WorldLayoutSO selected."));
                return messages;
            }

            layout.EnsureDefaults();
            ValidateStages(layout, messages);
            ValidateEndpoints(layout, messages);
            ValidateConnections(layout, messages);
            return messages;
        }

        public static bool HasErrors(List<WorldValidationMessage> messages)
        {
            if (messages is null)
            {
                return false;
            }

            foreach (var message in messages)
            {
                if (message != null && message.severity == WorldValidationSeverity.Error)
                {
                    return true;
                }
            }

            return false;
        }

        private static void ValidateStages(WorldLayoutSO layout, List<WorldValidationMessage> messages)
        {
            var occupied = new Dictionary<string, string>();
            foreach (var stage in layout.stages)
            {
                if (stage is null)
                {
                    continue;
                }

                stage.EnsureDefaults();
                ValidateStageDefinitionSync(layout, stage, messages);

                var endpointCount = 0;
                foreach (var endpoint in layout.endpoints)
                {
                    if (endpoint != null && endpoint.stageId == stage.id)
                    {
                        endpointCount++;
                    }
                }

                if (endpointCount == 0)
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Warning,
                        "Stage has no Journey endpoints.", stage.id));
                }

                if (stage.stageKind == WorldStageKind.SavePoint && stage.occupiedTiles.Count != 1)
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                        "Save Point stages must occupy exactly one WorldTile.", stage.id));
                }

                foreach (var tile in stage.occupiedTiles)
                {
                    var key = TileKey(stage.worldLayer, tile);
                    if (occupied.TryGetValue(key, out var otherStageId) && otherStageId != stage.id)
                    {
                        messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                            "WorldTile is occupied by more than one Stage in the same WorldLayer.", stage.id));
                    }
                    else
                    {
                        occupied[key] = stage.id;
                    }
                }
            }
        }

        private static void ValidateStageDefinitionSync(
            WorldLayoutSO layout,
            WorldStageAuthoring stage,
            List<WorldValidationMessage> messages)
        {
            if (stage.stageDefinition is null)
            {
                messages.Add(new WorldValidationMessage(WorldValidationSeverity.Warning,
                    "Stage has no StageDefinition. Use Create StageDefinition from the stage inspector.",
                    stage.id));
                return;
            }

            stage.stageDefinition.EnsureDefaults();
            var sync = stage.stageDefinition.worldSync;
            if (sync is null || !sync.HasSync)
            {
                messages.Add(new WorldValidationMessage(WorldValidationSeverity.Warning,
                    "StageDefinition is not synced. Use Sync StageDefinition from the stage inspector.",
                    stage.id));
                return;
            }

            if (!string.IsNullOrEmpty(sync.worldStageId) && sync.worldStageId != stage.id)
            {
                messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                    "StageDefinition is synced to another world stage id. Reassign or recreate the StageDefinition.",
                    stage.id));
                return;
            }

            var expectedHash = StageDefinitionSyncer.ComputeExpectedContractHash(layout, stage);
            if (sync.contractHash != expectedHash)
            {
                messages.Add(new WorldValidationMessage(WorldValidationSeverity.Warning,
                    "StageDefinition sync contract is outdated. Use Sync StageDefinition from the stage inspector.",
                    stage.id));
            }
        }

        private static void ValidateEndpoints(WorldLayoutSO layout, List<WorldValidationMessage> messages)
        {
            var nonSocketByTile = new Dictionary<string, JEndpointKind>();
            var socketByFace = new HashSet<string>();

            foreach (var endpoint in layout.endpoints)
            {
                if (endpoint is null)
                {
                    continue;
                }

                var stage = layout.FindStage(endpoint.stageId);
                if (stage is null)
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                        "JEndpoint references a missing Stage.", endpoint.id));
                    continue;
                }

                if (!stage.ContainsTile(endpoint.worldTile))
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                        "JEndpoint is placed on a WorldTile not occupied by its Stage.", endpoint.id));
                }

                if (stage.stageKind == WorldStageKind.SavePoint &&
                    endpoint.kind is JEndpointKind.Anchor or JEndpointKind.Lift)
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                        "Save Point stages cannot contain JEndpointAnchor or JEndpointLift.", endpoint.id));
                }

                if (endpoint.kind == JEndpointKind.Socket)
                {
                    var socketKey = endpoint.stageId + ":" + endpoint.worldTile.x + ":" + endpoint.worldTile.y + ":" +
                                    endpoint.socketFace;
                    if (!socketByFace.Add(socketKey))
                    {
                        messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                            "More than one JEndpointSocket exists on the same tile face.", endpoint.id));
                    }

                    var adjacent = Adjacent(endpoint.worldTile, endpoint.socketFace);
                    if (stage.ContainsTile(adjacent))
                    {
                        messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                            "JEndpointSocket is placed on an internal Stage face.", endpoint.id));
                    }
                }
                else
                {
                    var key = TileKey(stage.worldLayer, endpoint.worldTile);
                    if (nonSocketByTile.TryGetValue(key, out var existingKind) && existingKind != endpoint.kind)
                    {
                        messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                            "JEndpointAnchor and JEndpointLift cannot coexist on the same WorldTile.", endpoint.id));
                    }
                    else
                    {
                        nonSocketByTile[key] = endpoint.kind;
                    }
                }
            }
        }

        private static void ValidateConnections(WorldLayoutSO layout, List<WorldValidationMessage> messages)
        {
            var endpointConnectionCounts = new Dictionary<string, int>();
            foreach (var connection in layout.connections)
            {
                if (connection is null)
                {
                    continue;
                }

                var endpointA = layout.FindEndpoint(connection.endpointAId);
                var endpointB = layout.FindEndpoint(connection.endpointBId);

                if (endpointA is null || endpointB is null)
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                        "JConnection references a missing endpoint.", connection.id));
                    continue;
                }

                if (endpointA.id == endpointB.id)
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                        "JConnection links an endpoint to itself.", connection.id));
                }

                if (endpointA.kind != endpointB.kind)
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                        "JConnection links incompatible endpoint types.", connection.id));
                }

                Increment(endpointConnectionCounts, endpointA.id);
                Increment(endpointConnectionCounts, endpointB.id);

                ValidateConnectionGeometry(layout, connection, endpointA, endpointB, messages);
            }

            foreach (var endpoint in layout.endpoints)
            {
                if (endpoint is null)
                {
                    continue;
                }

                endpointConnectionCounts.TryGetValue(endpoint.id, out var count);
                switch (endpoint.kind)
                {
                    case JEndpointKind.Socket when count > 1:
                        messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                            "JEndpointSocket has more than one JConnection.", endpoint.id));
                        break;
                    case JEndpointKind.Lift when count > 2:
                        messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                            "JEndpointLift has more than two JConnections.", endpoint.id));
                        break;
                }

                if (count == 0)
                {
                    messages.Add(new WorldValidationMessage(endpoint.required
                            ? WorldValidationSeverity.Error
                            : WorldValidationSeverity.Warning,
                        endpoint.required
                            ? "Required JEndpoint has no JConnection."
                            : "Optional JEndpoint has no JConnection.",
                        endpoint.id));
                }
            }
        }

        private static void ValidateConnectionGeometry(
            WorldLayoutSO layout,
            WorldConnectionAuthoring connection,
            WorldEndpointAuthoring endpointA,
            WorldEndpointAuthoring endpointB,
            List<WorldValidationMessage> messages)
        {
            if (endpointA.kind == JEndpointKind.Socket && endpointB.kind == JEndpointKind.Socket)
            {
                if (!AreOpposite(endpointA.socketFace, endpointB.socketFace) ||
                    !IsAdjacentForFaces(endpointA.worldTile, endpointA.socketFace, endpointB.worldTile))
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                        "JEndpointSocket connections must use opposite faces on adjacent WorldTiles.", connection.id));
                }
            }

            if (endpointA.kind == JEndpointKind.Lift && endpointB.kind == JEndpointKind.Lift)
            {
                var stageA = layout.FindStage(endpointA.stageId);
                var stageB = layout.FindStage(endpointB.stageId);
                if (endpointA.worldTile.x != endpointB.worldTile.x || endpointA.worldTile.y != endpointB.worldTile.y)
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                        "JEndpointLift connections must be aligned on the same 2D WorldTile coordinate.",
                        connection.id));
                }

                if (stageA != null && stageB != null &&
                    Mathf.Abs(stageA.worldLayer - stageB.worldLayer) != 1)
                {
                    messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
                        "JEndpointLift connections must connect adjacent WorldLayers.", connection.id));
                }
            }
        }

        private static void Increment(Dictionary<string, int> counts, string key)
        {
            if (string.IsNullOrEmpty(key))
            {
                return;
            }

            counts.TryGetValue(key, out var count);
            counts[key] = count + 1;
        }

        private static string TileKey(int layer, JWorldTile tile)
        {
            return layer + ":" + tile.x + ":" + tile.y;
        }

        private static JWorldTile Adjacent(JWorldTile tile, JSocketFace face)
        {
            return face switch
            {
                JSocketFace.North => new JWorldTile(tile.x, tile.y - 1),
                JSocketFace.South => new JWorldTile(tile.x, tile.y + 1),
                JSocketFace.East => new JWorldTile(tile.x + 1, tile.y),
                _ => new JWorldTile(tile.x - 1, tile.y)
            };
        }

        private static bool AreOpposite(JSocketFace a, JSocketFace b)
        {
            return (a == JSocketFace.North && b == JSocketFace.South) ||
                   (a == JSocketFace.South && b == JSocketFace.North) ||
                   (a == JSocketFace.East && b == JSocketFace.West) ||
                   (a == JSocketFace.West && b == JSocketFace.East);
        }

        private static bool IsAdjacentForFaces(JWorldTile a, JSocketFace faceA, JWorldTile b)
        {
            var expected = Adjacent(a, faceA);
            return expected.x == b.x && expected.y == b.y;
        }
    }
}
