using System;
using System.IO;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;

namespace Content.Tools
{
    public sealed class Merger
    {
        public Map MapOurs { get; }
        public Map MapBased { get; }
        public Map MapOther { get; }

        public Dictionary<uint, uint> TileMapFromOtherToOurs { get; } = new Dictionary<uint, uint>();
        public Dictionary<uint, uint> TileMapFromBasedToOurs { get; } = new Dictionary<uint, uint>();
        public Dictionary<uint, uint> EntityMapFromOtherToOurs { get; } = new Dictionary<uint, uint>();
        public List<uint> EntityListDirectMerge { get; } = new List<uint>();

        private const int ExpectedChunkSize = 16 * 16 * 4;

        public Merger(Map ours, Map based, Map other)
        {
            MapOurs = ours;
            MapBased = based;
            MapOther = other;
        }

        public bool Merge()
        {
            PlanTileMapping(TileMapFromOtherToOurs, MapOther);
            PlanTileMapping(TileMapFromBasedToOurs, MapBased);
            MergeTiles();
            PlanEntityMapping();
            return MergeEntities();
        }

        // -- Tiles --

        public void PlanTileMapping(Dictionary<uint, uint> relativeOtherToOurs, Map relativeOther)
        {
            var mapping = new Dictionary<string, uint>();
            uint nextAvailable = 0;
            foreach (var kvp in MapOurs.TilemapNode)
            {
                var k = uint.Parse(kvp.Key.ToString());
                var v = kvp.Value.ToString();
                mapping[v] = k;
                if (k >= nextAvailable)
                    nextAvailable = k + 1;
            }
            foreach (var kvp in relativeOther.TilemapNode)
            {
                var k = uint.Parse(kvp.Key.ToString());
                var v = kvp.Value.ToString();
                if (mapping.ContainsKey(v))
                {
                    relativeOtherToOurs[k] = mapping[v];
                }
                else
                {
                    MapOurs.TilemapNode.Add(nextAvailable.ToString(CultureInfo.InvariantCulture), v);
                    relativeOtherToOurs[k] = nextAvailable++;
                }
            }
        }

        public void MergeTiles()
        {
            var a = MapOurs.GridsNode.Children[0];
            var b = MapBased.GridsNode.Children[0];
            var c = MapOther.GridsNode.Children[0];
            var aChunks = a["chunks"];
            var bChunks = b["chunks"];
            var cChunks = c["chunks"];
            MergeTileChunks((YamlSequenceNode) aChunks, (YamlSequenceNode) bChunks, (YamlSequenceNode) cChunks);
        }

        public void MergeTileChunks(YamlSequenceNode aChunks, YamlSequenceNode bChunks, YamlSequenceNode cChunks)
        {
            var aMap = ConvertTileChunks(aChunks);
            var bMap = ConvertTileChunks(bChunks);
            var cMap = ConvertTileChunks(cChunks);

            var xMap = new HashSet<string>();
            foreach (var kvp in aMap)
                xMap.Add(kvp.Key);
            // don't include b because that would mess with chunk deletion
            foreach (var kvp in cMap)
                xMap.Add(kvp.Key);

            foreach (var ind in xMap)
            {
                using var a = new MemoryStream(GetChunkBytes(aMap, ind));
                using var b = new MemoryStream(GetChunkBytes(bMap, ind));
                using var c = new MemoryStream(GetChunkBytes(cMap, ind));
                using var aR = new BinaryReader(a);
                using var bR = new BinaryReader(b);
                using var cR = new BinaryReader(c);

                var outB = new byte[ExpectedChunkSize];

                {
                    using (var outS = new MemoryStream(outB))
                    using (var outW = new BinaryWriter(outS))

                    for (var i = 0; i < ExpectedChunkSize; i += 4)
                    {
                        var aI = aR.ReadUInt32();
                        var bI = MapTileId(bR.ReadUInt32(), TileMapFromBasedToOurs);
                        var cI = MapTileId(cR.ReadUInt32(), TileMapFromOtherToOurs);
                        // cI needs translation.

                        uint result = aI;
                        if (aI == bI)
                        {
                            // If aI == bI then aI did not change anything, so cI always wins
                            result = cI;
                        }
                        else if (bI != cI)
                        {
                            // If bI != cI then cI definitely changed something (conflict, but overrides aI)
                            result = cI;
                            Console.WriteLine("WARNING: Tile (" + ind + ")[" + i + "] was changed by both branches.");
                        }
                        outW.Write(result);
                    }
                }

                // Actually output chunk
                if (!aMap.ContainsKey(ind))
                {
                    var res = new YamlMappingNode();
                    res.Children["ind"] = ind;
                    aMap[ind] = res;
                }
                aMap[ind].Children["tiles"] = Convert.ToBase64String(outB);
            }
        }

        public uint MapTileId(uint src, Dictionary<uint, uint> mapping)
        {
            return (src & 0xFFFF0000) | mapping[src & 0xFFFF];
        }

        public Dictionary<string, YamlMappingNode> ConvertTileChunks(YamlSequenceNode chunks)
        {
            var map = new Dictionary<string, YamlMappingNode>();
            foreach (var chunk in chunks)
                map[chunk["ind"].ToString()] = (YamlMappingNode) chunk;
            return map;
        }

        public byte[] GetChunkBytes(Dictionary<string, YamlMappingNode> chunks, string ind)
        {
            if (!chunks.ContainsKey(ind))
                return new byte[ExpectedChunkSize];
            return Convert.FromBase64String(chunks[ind]["tiles"].ToString());
        }

        // -- Entities --

        public void PlanEntityMapping()
        {
            // Ok, so here's how it works:
            // 1. Entities that do not exist in "based" are additions.
            // 2. Entities that exist in "based" but do not exist in the one map or the other are removals.

            // Find modifications and deletions
            foreach (var kvp in MapBased.Entities)
            {
                var deletedByOurs = !MapOurs.Entities.ContainsKey(kvp.Key);
                var deletedByOther = !MapOther.Entities.ContainsKey(kvp.Key);
                if (deletedByOther && !deletedByOurs)
                {
                    // Delete
                    MapOurs.Entities.Remove(kvp.Key);
                }
                else if (!(deletedByOurs || deletedByOther))
                {
                    // Modify
                    EntityMapFromOtherToOurs[kvp.Key] = kvp.Key;
                }
            }

            // Find additions
            foreach (var kvp in MapOther.Entities)
            {
                if (!MapBased.Entities.ContainsKey(kvp.Key))
                {
                    // New
                    var newId = MapOurs.NextAvailableEntityId++;
                    EntityMapFromOtherToOurs[kvp.Key] = newId;
                }
            }
        }

        public bool MergeEntities()
        {
            bool success = true;
            foreach (var kvp in EntityMapFromOtherToOurs)
            {
                // For debug use.
                // Console.WriteLine("Entity C/" + kvp.Key + " -> A/" + kvp.Value);
                YamlMappingNode oursEnt;
                if (MapOurs.Entities.ContainsKey(kvp.Value))
                {
                    oursEnt = MapOurs.Entities[kvp.Value];
                    if (!MapBased.Entities.TryGetValue(kvp.Value, out var basedEnt))
                    {
                        basedEnt = oursEnt;
                    }

                    if (!MergeEntityNodes(oursEnt, basedEnt, MapOther.Entities[kvp.Key]))
                    {
                        Console.WriteLine("Unable to successfully merge entity C/" + kvp.Key);
                        success = false;
                    }
                }
                else
                {
                    oursEnt = (YamlMappingNode) YamlTools.CopyYamlNodes(MapOther.Entities[kvp.Key]);
                    if (!MapEntity(oursEnt)) {
                        Console.WriteLine("Unable to successfully import entity C/" + kvp.Key);
                        success = false;
                    } else {
                        MapOurs.Entities[kvp.Value] = oursEnt;
                    }
                }
                oursEnt.Children["uid"] = kvp.Value.ToString(CultureInfo.InvariantCulture);
            }
            return success;
        }

        public bool MergeEntityNodes(YamlMappingNode ours, YamlMappingNode based, YamlMappingNode other)
        {
            // Copy to intermediate
            var otherMapped = (YamlMappingNode) YamlTools.CopyYamlNodes(other);
            if (!MapEntity(otherMapped))
                return false;
            // Merge stuff that isn't components
            var path = "Entity" + (other["uid"].ToString());
            YamlTools.MergeYamlMappings(ours, based, otherMapped, path, new string[] {"components"});
            // Components are special
            var ourComponents = new Dictionary<string, YamlMappingNode>();
            var basedComponents = new Dictionary<string, YamlMappingNode>();
            var ourComponentsNode = (YamlSequenceNode) ours["components"];
            var basedComponentsNode = (YamlSequenceNode) based["components"];
            var otherComponentsNode = (YamlSequenceNode) otherMapped["components"];
            foreach (var component in ourComponentsNode)
            {
                var name = component["type"].ToString();
                ourComponents[name] = (YamlMappingNode) component;
            }
            foreach (var component in basedComponentsNode)
            {
                var name = component["type"].ToString();
                basedComponents[name] = (YamlMappingNode) component;
            }
            foreach (var otherComponent in otherComponentsNode)
            {
                var name = otherComponent["type"].ToString();
                if (ourComponents.ContainsKey(name))
                {
                    var ourComponent = ourComponents[name];
                    if (!basedComponents.TryGetValue(name, out var basedComponent))
                        basedComponent = new YamlMappingNode();

                    YamlTools.MergeYamlNodes(ourComponent, basedComponent, otherComponent, path + "/components/" + name);
                }
                else
                {
                    ourComponentsNode.Add(otherComponent);
                }
            }
            return true;
        }

        public bool MapEntity(YamlMappingNode other)
        {
            var path = "Entity" + (other["uid"].ToString());
            if (other.Children.ContainsKey("components"))
            {
                var components = (YamlSequenceNode) other["components"];
                foreach (var component in components)
                {
                    var type = component["type"].ToString();
                    if (type == "Transform")
                    {
                        if (!MapEntityProperty((YamlMappingNode) component, "parent", path))
                            return false;
                    }
                    else if (type == "ContainerContainer")
                    {
                        MapEntityRecursiveAndBadly(component, path);
                    }
                }
            }
            return true;
        }

        public bool MapEntityProperty(YamlMappingNode node, string property, string path)
        {
            if (node.Children.ContainsKey(property)) {
                var prop = node[property];
                if (prop is YamlScalarNode)
                    return MapEntityProperty((YamlScalarNode) prop, path + "/" + property);
            }
            return true;
        }

        public bool MapEntityProperty(YamlScalarNode node, string path)
        {
            if (uint.TryParse(node.ToString(), out var uid))
            {
                if (EntityMapFromOtherToOurs.ContainsKey(uid))
                {
                    node.Value = EntityMapFromOtherToOurs[uid].ToString(CultureInfo.InvariantCulture);
                }
                else
                {
                    Console.WriteLine($"Error finding UID in MapEntityRecursiveAndBadly {path}. To fix this, the merge driver needs to be improved.");
                    return false;
                }
            }
            return true;
        }

        public bool MapEntityRecursiveAndBadly(YamlNode node, string path)
        {
            switch (node)
            {
                case YamlSequenceNode subSequence:
                    var idx = 0;
                    foreach (var val in subSequence)
                        if (!MapEntityRecursiveAndBadly(val, path + "/" + (idx++)))
                            return false;
                    return true;
                case YamlMappingNode subMapping:
                    foreach (var kvp in subMapping)
                        if (!MapEntityRecursiveAndBadly(kvp.Key, path))
                            return false;
                    foreach (var kvp in subMapping)
                        if (!MapEntityRecursiveAndBadly(kvp.Value, path + "/" + kvp.Key.ToString()))
                            return false;
                    return true;
                case YamlScalarNode subScalar:
                    return MapEntityProperty(subScalar, path);
                default:
                    throw new ArgumentException($"Unrecognized YAML node type: {node.GetType()} at {path}");
            }
        }
    }
}
