using System.Collections.Generic;
using System.IO;
using YamlDotNet.Serialization;

namespace ManifoldGardenArchipelago
{
    public readonly struct SceneItemReference(string scene_arg, int index_arg)
    {
        public readonly string scene = scene_arg;
        public readonly int index = index_arg;

        public override string ToString()
        {
            return $"({scene}, {index})";
        }
    }

    public readonly struct LocationDescription(string name_arg, Requirement requirement_arg)
    {
        public readonly string name = name_arg;
        public readonly Requirement requirement = requirement_arg;
    }

    public class SceneDescription
    {
        public readonly List<LocationDescription> locations = [];
        public readonly Dictionary<int, Requirement> doors = [];
        public readonly Dictionary<int, Requirement> smokeWalls = [];
        public readonly Dictionary<int, Requirement> worldGrows = [];
        public readonly Dictionary<int, Requirement> lasers = [];
    }

    public class GameStateListeners
    {
        public readonly Dictionary<string, Requirement> locations = [];
        public readonly Dictionary<SceneItemReference, Requirement> doors = [];
        public readonly Dictionary<SceneItemReference, Requirement> smokeWalls = [];
        public readonly Dictionary<SceneItemReference, Requirement> worldGrows = [];
        public readonly Dictionary<SceneItemReference, Requirement> lasers = [];
    }

    public static class GameData
    {
        public static readonly Dictionary<string, SceneDescription> scenes = [];

        public static readonly Dictionary<string, GameStateListeners> listenersByScene = [];
        public static readonly Dictionary<SceneItemReference, GameStateListeners> listenersByButton = [];
        public static readonly Dictionary<SceneItemReference, GameStateListeners> listenersBySocket = [];
        public static readonly Dictionary<SceneItemReference, GameStateListeners> listenersByPad = [];
        public static readonly Dictionary<SceneItemReference, GameStateListeners> listenersByWaterwheel = [];
        public static readonly Dictionary<SceneItemReference, GameStateListeners> listenersBySphere = [];
        public static readonly Dictionary<string, GameStateListeners> listenersByItem = [];

        public static Requirement ParseRequirement(string scene, Dictionary<object, object> yamlReq)
        {
            List<Requirement> reqs = [];

            if (yamlReq.ContainsKey("entry"))
            {
                if (yamlReq["entry"] is string sceneName)
                {
                    reqs.Add(new EntryRequirement(sceneName));
                }
                else if (yamlReq["entry"] is List<string> scenes)
                {
                    foreach (var sceneName2 in scenes)
                    {
                        reqs.Add(new EntryRequirement(sceneName2));
                    }
                }
            }

            if (yamlReq.ContainsKey("item"))
            {
                if (yamlReq["item"] is string itemName)
                {
                    reqs.Add(new ItemRequirement(itemName));
                }
                else if (yamlReq["item"] is List<string> items)
                {
                    foreach (var item in items)
                    {
                        reqs.Add(new ItemRequirement(item));
                    }
                }
            }

            if (yamlReq.ContainsKey("button"))
            {
                if (yamlReq["button"] is string index)
                {
                    reqs.Add(new ButtonRequirement(new(scene, int.Parse(index))));
                }
                else if (yamlReq["button"] is Dictionary<object, object> details)
                {
                    reqs.Add(new ButtonRequirement(new((string)details["scene"], int.Parse((string)details["index"]))));
                }
                else if (yamlReq["button"] is List<object> buttons)
                {
                    foreach (var button in buttons)
                    {
                        if (button is string index2)
                        {
                            reqs.Add(new ButtonRequirement(new(scene, int.Parse(index2))));
                        }
                        else if (button is Dictionary<object, object> details2)
                        {
                            reqs.Add(new ButtonRequirement(new((string)details2["scene"], int.Parse((string)details2["index"]))));
                        }
                    }
                }
            }

            if (yamlReq.ContainsKey("socket"))
            {
                if (yamlReq["socket"] is string index)
                {
                    reqs.Add(new SocketRequirement(new(scene, int.Parse(index))));
                }
                else if (yamlReq["socket"] is Dictionary<object, object> details)
                {
                    reqs.Add(new SocketRequirement(new((string)details["scene"], int.Parse((string)details["index"]))));
                }
                else if (yamlReq["socket"] is List<object> buttons)
                {
                    foreach (var button in buttons)
                    {
                        if (button is string index2)
                        {
                            reqs.Add(new SocketRequirement(new(scene, int.Parse(index2))));
                        }
                        else if (button is Dictionary<object, object> details2)
                        {
                            reqs.Add(new SocketRequirement(new((string)details2["scene"], int.Parse((string)details2["index"]))));
                        }
                    }
                }
            }

            if (yamlReq.ContainsKey("pad"))
            {
                if (yamlReq["pad"] is string index)
                {
                    reqs.Add(new PadRequirement(new(scene, int.Parse(index))));
                }
                else if (yamlReq["pad"] is Dictionary<object, object> details)
                {
                    reqs.Add(new PadRequirement(new((string)details["scene"], int.Parse((string)details["index"]))));
                }
                else if (yamlReq["pad"] is List<object> buttons)
                {
                    foreach (var button in buttons)
                    {
                        if (button is string index2)
                        {
                            reqs.Add(new PadRequirement(new(scene, int.Parse(index2))));
                        }
                        else if (button is Dictionary<object, object> details2)
                        {
                            reqs.Add(new PadRequirement(new((string)details2["scene"], int.Parse((string)details2["index"]))));
                        }
                    }
                }
            }

            if (yamlReq.ContainsKey("waterwheel"))
            {
                if (yamlReq["waterwheel"] is string index)
                {
                    reqs.Add(new WaterwheelRequirement(new(scene, int.Parse(index))));
                }
                else if (yamlReq["waterwheel"] is Dictionary<object, object> details)
                {
                    reqs.Add(new WaterwheelRequirement(new((string)details["scene"], int.Parse((string)details["index"]))));
                }
                else if (yamlReq["waterwheel"] is List<object> buttons)
                {
                    foreach (var button in buttons)
                    {
                        if (button is string index2)
                        {
                            reqs.Add(new WaterwheelRequirement(new(scene, int.Parse(index2))));
                        }
                        else if (button is Dictionary<object, object> details2)
                        {
                            reqs.Add(new WaterwheelRequirement(new((string)details2["scene"], int.Parse((string)details2["index"]))));
                        }
                    }
                }
            }

            if (yamlReq.ContainsKey("sphere"))
            {
                if (yamlReq["sphere"] is string index)
                {
                    reqs.Add(new SphereRequirement(new(scene, int.Parse(index))));
                }
                else if (yamlReq["sphere"] is Dictionary<object, object> details)
                {
                    reqs.Add(new SphereRequirement(new((string)details["scene"], int.Parse((string)details["index"]))));
                }
                else if (yamlReq["sphere"] is List<object> buttons)
                {
                    foreach (var button in buttons)
                    {
                        if (button is string index2)
                        {
                            reqs.Add(new SphereRequirement(new(scene, int.Parse(index2))));
                        }
                        else if (button is Dictionary<object, object> details2)
                        {
                            reqs.Add(new SphereRequirement(new((string)details2["scene"], int.Parse((string)details2["index"]))));
                        }
                    }
                }
            }

            if (yamlReq.ContainsKey("full_garden") && (string)yamlReq["full_garden"] == "true")
            {
                reqs.Add(new FullGardenRequirement());
            }

            if (yamlReq.ContainsKey("or"))
            {
                List<Requirement> unionReq = [];

                foreach (var reqObj in (List<object>)yamlReq["or"])
                {
                    unionReq.Add(ParseRequirement(scene, (Dictionary<object, object>)reqObj));
                }

                reqs.Add(new OrRequirement(unionReq));
            }

            if (yamlReq.ContainsKey("inverted"))
            {
                reqs.Add(new InvertedRequirement(ParseRequirement(scene, (Dictionary<object, object>)yamlReq["inverted"])));
            }

            if (reqs.Count == 1)
            {
                return reqs[0];
            }
            else
            {
                return new AndRequirement(reqs);
            }
        }

        private static GameStateListeners GetOrAddListeners<T>(Dictionary<T, GameStateListeners> kvp, T key)
        {
            if (!kvp.TryGetValue(key, out GameStateListeners listeners))
            {
                listeners = new();
                kvp[key] = listeners;
            }

            return listeners;
        }

        public static void Load()
        {
            Plugin.Logger.LogInfo(Path.Combine(BepInEx.Paths.BepInExRootPath, "plugins", "game_data.yaml"));
            var yamlText = File.OpenText(Path.Combine(BepInEx.Paths.BepInExRootPath, "plugins", "game_data.yaml"));

            var deserializer = new DeserializerBuilder().Build();
            var yamlObject = deserializer.Deserialize<Dictionary<string, object>>(yamlText);

            foreach (var scenePair in yamlObject)
            {
                Plugin.Logger.LogInfo($"Scene: {scenePair.Key}");
                SceneDescription sceneDescription = new();

                var sceneDetails = (Dictionary<object, object>)scenePair.Value;

                if (sceneDetails.ContainsKey("locations"))
                {
                    var sceneLocations = (Dictionary<object, object>)sceneDetails["locations"];

                    foreach (var locationPair in sceneLocations)
                    {
                        string locationName = (string)locationPair.Key;
                        Requirement req = ParseRequirement(scenePair.Key, (Dictionary<object, object>)locationPair.Value);
                        sceneDescription.locations.Add(new(locationName, req));

                        Plugin.Logger.LogInfo($"{locationName} requirements: {req}");

                        foreach (var reqScene in req.references.scenes)
                        {
                            GetOrAddListeners(listenersByScene, reqScene).locations[locationName] = req;
                        }

                        foreach (var button in req.references.buttons)
                        {
                            GetOrAddListeners(listenersByButton, button).locations[locationName] = req;
                        }

                        foreach (var socket in req.references.sockets)
                        {
                            GetOrAddListeners(listenersBySocket, socket).locations[locationName] = req;
                        }

                        foreach (var pad in req.references.pads)
                        {
                            GetOrAddListeners(listenersByPad, pad).locations[locationName] = req;
                        }

                        foreach (var waterwheel in req.references.waterwheels)
                        {
                            GetOrAddListeners(listenersByWaterwheel, waterwheel).locations[locationName] = req;
                        }

                        foreach (var sphere in req.references.spheres)
                        {
                            GetOrAddListeners(listenersBySphere, sphere).locations[locationName] = req;
                        }

                        foreach (var item in req.references.items)
                        {
                            GetOrAddListeners(listenersByItem, item).locations[locationName] = req;
                        }
                    }
                }
                
                if (sceneDetails.ContainsKey("doors"))
                {
                    var sceneDoors = (Dictionary<object, object>)sceneDetails["doors"];

                    foreach (var doorPair in sceneDoors)
                    {
                        SceneItemReference sir = new(scenePair.Key, int.Parse((string)doorPair.Key));
                        Requirement req = ParseRequirement(scenePair.Key, (Dictionary<object, object>)doorPair.Value);
                        sceneDescription.doors[sir.index] = req;

                        Plugin.Logger.LogInfo($"Door {sir} requirements: {req}");

                        foreach (var reqScene in req.references.scenes)
                        {
                            GetOrAddListeners(listenersByScene, reqScene).doors[sir] = req;
                        }

                        foreach (var button in req.references.buttons)
                        {
                            GetOrAddListeners(listenersByButton, button).doors[sir] = req;
                        }

                        foreach (var socket in req.references.sockets)
                        {
                            GetOrAddListeners(listenersBySocket, socket).doors[sir] = req;
                        }

                        foreach (var pad in req.references.pads)
                        {
                            GetOrAddListeners(listenersByPad, pad).doors[sir] = req;
                        }

                        foreach (var waterwheel in req.references.waterwheels)
                        {
                            GetOrAddListeners(listenersByWaterwheel, waterwheel).doors[sir] = req;
                        }

                        foreach (var sphere in req.references.spheres)
                        {
                            GetOrAddListeners(listenersBySphere, sphere).doors[sir] = req;
                        }

                        foreach (var item in req.references.items)
                        {
                            GetOrAddListeners(listenersByItem, item).doors[sir] = req;
                        }
                    }
                }

                if (sceneDetails.ContainsKey("smoke"))
                {
                    foreach (var smokePair in (Dictionary<object, object>)sceneDetails["smoke"])
                    {
                        SceneItemReference sir = new(scenePair.Key, int.Parse((string)smokePair.Key));
                        Requirement req = ParseRequirement(scenePair.Key, (Dictionary<object, object>)smokePair.Value);
                        sceneDescription.smokeWalls[sir.index] = req;

                        Plugin.Logger.LogInfo($"Smoke {sir} requirements: {req}");

                        foreach (var reqScene in req.references.scenes)
                        {
                            GetOrAddListeners(listenersByScene, reqScene).smokeWalls[sir] = req;
                        }

                        foreach (var button in req.references.buttons)
                        {
                            GetOrAddListeners(listenersByButton, button).smokeWalls[sir] = req;
                        }

                        foreach (var socket in req.references.sockets)
                        {
                            GetOrAddListeners(listenersBySocket, socket).smokeWalls[sir] = req;
                        }

                        foreach (var pad in req.references.pads)
                        {
                            GetOrAddListeners(listenersByPad, pad).smokeWalls[sir] = req;
                        }

                        foreach (var waterwheel in req.references.waterwheels)
                        {
                            GetOrAddListeners(listenersByWaterwheel, waterwheel).smokeWalls[sir] = req;
                        }

                        foreach (var sphere in req.references.spheres)
                        {
                            GetOrAddListeners(listenersBySphere, sphere).smokeWalls[sir] = req;
                        }

                        foreach (var item in req.references.items)
                        {
                            GetOrAddListeners(listenersByItem, item).smokeWalls[sir] = req;
                        }
                    }
                }

                if (sceneDetails.ContainsKey("world_grow"))
                {
                    foreach (var worldPair in (Dictionary<object, object>)sceneDetails["world_grow"])
                    {
                        SceneItemReference sir = new(scenePair.Key, int.Parse((string)worldPair.Key));
                        Requirement req = ParseRequirement(scenePair.Key, (Dictionary<object, object>)worldPair.Value);
                        sceneDescription.worldGrows[sir.index] = req;

                        Plugin.Logger.LogInfo($"World {sir} requirements: {req}");

                        foreach (var reqScene in req.references.scenes)
                        {
                            GetOrAddListeners(listenersByScene, reqScene).worldGrows[sir] = req;
                        }

                        foreach (var button in req.references.buttons)
                        {
                            GetOrAddListeners(listenersByButton, button).worldGrows[sir] = req;
                        }

                        foreach (var socket in req.references.sockets)
                        {
                            GetOrAddListeners(listenersBySocket, socket).worldGrows[sir] = req;
                        }

                        foreach (var pad in req.references.pads)
                        {
                            GetOrAddListeners(listenersByPad, pad).worldGrows[sir] = req;
                        }

                        foreach (var waterwheel in req.references.waterwheels)
                        {
                            GetOrAddListeners(listenersByWaterwheel, waterwheel).worldGrows[sir] = req;
                        }

                        foreach (var sphere in req.references.spheres)
                        {
                            GetOrAddListeners(listenersBySphere, sphere).worldGrows[sir] = req;
                        }

                        foreach (var item in req.references.items)
                        {
                            GetOrAddListeners(listenersByItem, item).worldGrows[sir] = req;
                        }
                    }
                }

                if (sceneDetails.ContainsKey("lasers"))
                {
                    foreach (var laserPair in (Dictionary<object, object>)sceneDetails["lasers"])
                    {
                        SceneItemReference sir = new(scenePair.Key, int.Parse((string)laserPair.Key));
                        Requirement req = ParseRequirement(scenePair.Key, (Dictionary<object, object>)laserPair.Value);
                        sceneDescription.lasers[sir.index] = req;

                        Plugin.Logger.LogInfo($"Laser {sir} requirements: {req}");

                        foreach (var reqScene in req.references.scenes)
                        {
                            GetOrAddListeners(listenersByScene, reqScene).lasers[sir] = req;
                        }

                        foreach (var button in req.references.buttons)
                        {
                            GetOrAddListeners(listenersByButton, button).lasers[sir] = req;
                        }

                        foreach (var socket in req.references.sockets)
                        {
                            GetOrAddListeners(listenersBySocket, socket).lasers[sir] = req;
                        }

                        foreach (var pad in req.references.pads)
                        {
                            GetOrAddListeners(listenersByPad, pad).lasers[sir] = req;
                        }

                        foreach (var waterwheel in req.references.waterwheels)
                        {
                            GetOrAddListeners(listenersByWaterwheel, waterwheel).lasers[sir] = req;
                        }

                        foreach (var sphere in req.references.spheres)
                        {
                            GetOrAddListeners(listenersBySphere, sphere).lasers[sir] = req;
                        }

                        foreach (var item in req.references.items)
                        {
                            GetOrAddListeners(listenersByItem, item).lasers[sir] = req;
                        }
                    }
                }

                scenes[scenePair.Key] = sceneDescription;
            }
        }
    }
}