﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Oni.Imaging;

namespace Oni.Akira
{
    internal class AkiraDaeWriter
    {
        #region Private data
        private readonly PolygonMesh source;
        private DaeSceneBuilder world;
        private DaeSceneBuilder worldMarkers;
        private DaeSceneBuilder[] objects;
        private DaeSceneBuilder rooms;
        private Dictionary<int, DaeSceneBuilder> scripts;

        private static readonly string[] objectTypeNames = new[] {
            "",
            "char",
            "patr",
            "door",
            "flag",
            "furn",
            "",
            "",
            "part",
            "pwru",
            "sndg",
            "trgv",
            "weap",
            "trig",
            "turr",
            "cons",
            "cmbt",
            "mele",
            "neut"
        };

        #endregion

        #region private class DaePolygon

        private class DaePolygon
        {
            private readonly Polygon source;
            private readonly Material material;
            private readonly int[] pointIndices;
            private readonly int[] texCoordIndices;
            private readonly int[] colorIndices;

            public DaePolygon(Polygon source, int[] pointIndices, int[] texCoordIndices, int[] colorIndices)
            {
                this.source = source;
                material = source.Material;
                this.pointIndices = pointIndices;
                this.texCoordIndices = texCoordIndices;
                this.colorIndices = colorIndices;
            }

            public DaePolygon(Material material, int[] pointIndices, int[] texCoordIndices)
            {
                this.material = material;
                this.pointIndices = pointIndices;
                this.texCoordIndices = texCoordIndices;
            }

            public Polygon Source => source;
            public Material Material => material;
            public int[] PointIndices => pointIndices;
            public int[] TexCoordIndices => texCoordIndices;
            public int[] ColorIndices => colorIndices;
        }

        #endregion
        #region private class DaeMeshBuilder

        private class DaeMeshBuilder
        {
            private readonly List<DaePolygon> polygons = new List<DaePolygon>();
            private readonly List<Vector3> points = new List<Vector3>();
            private readonly Dictionary<Vector3, int> uniquePoints = new Dictionary<Vector3, int>();
            private readonly List<Vector2> texCoords = new List<Vector2>();
            private readonly Dictionary<Vector2, int> uniqueTexCoords = new Dictionary<Vector2, int>();
            private readonly List<Color> colors = new List<Color>();
            private readonly Dictionary<Color, int> uniqueColors = new Dictionary<Color, int>();
            private string name;
            private Vector3 translation;
            private Dae.Geometry geometry;

            public DaeMeshBuilder(string name)
            {
                this.name = name;
            }

            public string Name
            {
                get { return name; }
                set { name = value; }
            }

            public Vector3 Translation => translation;

            public void ResetTransform()
            {
                //
                // Attempt to un-bake the translation of the furniture
                //

                var center = BoundingSphere.CreateFromPoints(points).Center;
                center.Y = BoundingBox.CreateFromPoints(points).Min.Y;

                translation = center;

                for (int i = 0; i < points.Count; i++)
                    points[i] -= center;
            }

            public void AddPolygon(Material material, Vector3[] polygonPoints, Vector2[] polygonTexCoords)
            {
                polygons.Add(new DaePolygon(
                    material,
                    Remap(polygonPoints, points, uniquePoints),
                    Remap(polygonTexCoords, texCoords, uniqueTexCoords)));
            }

            public void AddPolygon(Polygon polygon)
            {
                polygons.Add(new DaePolygon(
                    polygon,
                    Remap(polygon.Mesh.Points, polygon.PointIndices, points, uniquePoints),
                    Remap(polygon.Mesh.TexCoords, polygon.TexCoordIndices, texCoords, uniqueTexCoords),
                    Remap(polygon.Colors, colors, uniqueColors)));
            }

            public IEnumerable<Polygon> Polygons
            {
                get
                {
                    return from p in polygons
                           where p.Source != null
                           select p.Source;
                }
            }

            private static int[] Remap<T>(IList<T> values, int[] indices, List<T> list, Dictionary<T, int> unique)
                where T : struct
            {
                var result = new int[indices.Length];

                for (int i = 0; i < indices.Length; i++)
                    result[i] = AddUnique(list, unique, values[indices[i]]);

                return result;
            }

            private static int[] Remap<T>(IList<T> values, List<T> list, Dictionary<T, int> unique)
                where T : struct
            {
                var result = new int[values.Count];

                for (int i = 0; i < values.Count; i++)
                    result[i] = AddUnique(list, unique, values[i]);

                return result;
            }

            private static int AddUnique<T>(List<T> list, Dictionary<T, int> unique, T value)
                where T : struct
            {
                int index;

                if (!unique.TryGetValue(value, out index))
                {
                    index = list.Count;
                    unique.Add(value, index);
                    list.Add(value);
                }

                return index;
            }

            public void Build()
            {
                geometry = new Dae.Geometry();
                geometry.Name = Name + "_geo";

                var positionSource = new Dae.Source(points);
                var texCoordSource = new Dae.Source(texCoords);
                var colorSource = new Dae.Source(ColorArrayToFloatArray(colors), 4);

                geometry.Vertices.Add(new Dae.Input(Dae.Semantic.Position, positionSource));

                Dae.IndexedInput posInput = null;
                Dae.IndexedInput texCoordInput = null;
                Dae.IndexedInput colorInput = null;

                var materialPrimitives = new Dictionary<Material, Dae.MeshPrimitives>();

                polygons.Sort((x, y) => string.Compare(x.Material.Name, y.Material.Name));

                foreach (var poly in polygons)
                {
                    Dae.MeshPrimitives primitives;

                    if (!materialPrimitives.TryGetValue(poly.Material, out primitives))
                    {
                        primitives = new Dae.MeshPrimitives(Dae.MeshPrimitiveType.Polygons);
                        materialPrimitives.Add(poly.Material, primitives);

                        posInput = new Dae.IndexedInput(Dae.Semantic.Position, positionSource);
                        primitives.Inputs.Add(posInput);

                        texCoordInput = new Dae.IndexedInput(Dae.Semantic.TexCoord, texCoordSource);
                        primitives.Inputs.Add(texCoordInput);

                        if (poly.ColorIndices != null /*&& !poly.Material.Image.HasAlpha*/)
                        {
                            colorInput = new Dae.IndexedInput(Dae.Semantic.Color, colorSource);
                            primitives.Inputs.Add(colorInput);
                        }

                        primitives.MaterialSymbol = poly.Material.Name;
                        geometry.Primitives.Add(primitives);
                    }

                    primitives.VertexCounts.Add(poly.PointIndices.Length);

                    posInput.Indices.AddRange(poly.PointIndices);
                    texCoordInput.Indices.AddRange(poly.TexCoordIndices);

                    if (colorInput != null)
                    {
                        //
                        // If the first polygon had color indices then the rest better have them too...
                        //

                        colorInput.Indices.AddRange(poly.ColorIndices);
                    }
                }
            }

            public Dae.Geometry Geometry => geometry;

            public void InstantiateMaterials(Dae.GeometryInstance inst, DaeSceneBuilder sceneBuilder)
            {
                var matInstances = new Dictionary<Material, Dae.MaterialInstance>();

                foreach (var poly in polygons)
                {
                    if (matInstances.ContainsKey(poly.Material))
                        continue;

                    string matSymbol = poly.Material.Name;

                    var matInstance = new Dae.MaterialInstance(
                        matSymbol,
                        sceneBuilder.GetMaterial(poly.Material));

                    matInstances.Add(poly.Material, matInstance);

                    var primitives = geometry.Primitives.FirstOrDefault(p => p.MaterialSymbol == matSymbol);

                    if (primitives == null)
                        continue;

                    var texCoordInput = primitives.Inputs.Find(i => i.Semantic == Dae.Semantic.TexCoord);

                    if (texCoordInput == null)
                        continue;

                    matInstance.Bindings.Add(new Dae.MaterialBinding("diffuse_TEXCOORD", texCoordInput));
                    inst.Materials.Add(matInstance);
                }
            }

            private static float[] ColorArrayToFloatArray(IList<Color> array)
            {
                var result = new float[array.Count * 4];

                for (int i = 0; i < array.Count; i++)
                {
                    var color = array[i].ToVector3();

                    result[i * 4 + 0] = color.X;
                    result[i * 4 + 1] = color.Y;
                    result[i * 4 + 2] = color.Z;
                }

                return result;
            }
        }

        #endregion
        #region private class DaeSceneBuilder

        private class DaeSceneBuilder
        {
            private readonly Dae.Scene scene;
            private readonly Dictionary<string, DaeMeshBuilder> nameMeshBuilder;
            private readonly List<DaeMeshBuilder> meshBuilders;
            private readonly Dictionary<Material, Dae.Material> materials;
            private string imagesFolder = "images";

            public DaeSceneBuilder()
            {
                scene = new Dae.Scene();
                nameMeshBuilder = new Dictionary<string, DaeMeshBuilder>(StringComparer.Ordinal);
                meshBuilders = new List<DaeMeshBuilder>();
                materials = new Dictionary<Material, Dae.Material>();
            }

            public string ImagesFolder
            {
                get { return imagesFolder; }
                set { imagesFolder = value; }
            }

            public DaeMeshBuilder GetMeshBuilder(string name)
            {
                DaeMeshBuilder result;

                if (!nameMeshBuilder.TryGetValue(name, out result))
                {
                    result = new DaeMeshBuilder(name);
                    nameMeshBuilder.Add(name, result);
                    meshBuilders.Add(result);
                }

                return result;
            }

            public IEnumerable<DaeMeshBuilder> MeshBuilders => meshBuilders;

            public Dae.Material GetMaterial(Material material)
            {
                Dae.Material result;

                if (!materials.TryGetValue(material, out result))
                {
                    result = new Dae.Material();
                    materials.Add(material, result);
                }

                return result;
            }

            public void Build()
            {
                BuildNodes();
                BuildMaterials();
            }

            private void BuildNodes()
            {
                foreach (var meshBuilder in meshBuilders)
                {
                    meshBuilder.Build();

                    var instance = new Dae.GeometryInstance(meshBuilder.Geometry);

                    meshBuilder.InstantiateMaterials(instance, this);

                    var node = new Dae.Node();
                    node.Name = meshBuilder.Name;
                    node.Instances.Add(instance);

                    if (meshBuilder.Translation != Vector3.Zero)
                        node.Transforms.Add(new Dae.TransformTranslate(meshBuilder.Translation));

                    scene.Nodes.Add(node);
                }
            }

            private void BuildMaterials()
            {
                foreach (var pair in materials)
                {
                    var material = pair.Key;
                    var daeMaterial = pair.Value;

                    string imageFileName = GetImageFileName(material);

                    var image = new Dae.Image
                    {
                        FilePath = "./" + imageFileName.Replace('\\', '/'),
                        Name = material.Name + "_img"
                    };

                    var effectSurface = new Dae.EffectSurface(image);

                    var effectSampler = new Dae.EffectSampler(effectSurface)
                    {
                        //    WrapS = texture.WrapU ? Dae.EffectSamplerWrap.Wrap : Dae.EffectSamplerWrap.None,
                        //    WrapT = texture.WrapV ? Dae.EffectSamplerWrap.Wrap : Dae.EffectSamplerWrap.None
                    };

                    var effectTexture = new Dae.EffectTexture(effectSampler, "diffuse_TEXCOORD");

                    var effect = new Dae.Effect
                    {
                        Name = material.Name + "_fx",
                        AmbientValue = Vector4.One,
                        SpecularValue = Vector4.Zero,
                        DiffuseValue = effectTexture,
                        TransparentValue = material.Image.HasAlpha ? effectTexture : null,
                        Parameters = {
                            new Dae.EffectParameter("surface", effectSurface),
                            new Dae.EffectParameter("sampler", effectSampler)
                        }
                    };

                    daeMaterial.Name = material.Name;
                    daeMaterial.Effect = effect;
                }
            }

            private string GetImageFileName(Material material)
            {
                string fileName = material.Name + ".tga";

                if (material.IsMarker)
                    return Path.Combine("markers", fileName);

                return Path.Combine(imagesFolder, fileName);
            }

            public void Write(string filePath)
            {
                string outputDirPath = Path.GetDirectoryName(filePath);

                foreach (var material in materials.Keys)
                    TgaWriter.Write(material.Image, Path.Combine(outputDirPath, GetImageFileName(material)));

                Dae.Writer.WriteFile(filePath, scene);
            }
        }

        #endregion

        public static void WriteRooms(PolygonMesh mesh, string name, string outputDirPath)
        {
            var writer = new AkiraDaeWriter(mesh);
            writer.WriteRooms();
            writer.rooms.Write(Path.Combine(outputDirPath, name + "_bnv.dae"));
        }

        public static void WriteRooms(PolygonMesh mesh, string filePath)
        {
            var writer = new AkiraDaeWriter(mesh);
            writer.WriteRooms();
            writer.rooms.Write(filePath);
        }

        public static void Write(PolygonMesh mesh, string name, string outputDirPath, string fileType)
        {
            var writer = new AkiraDaeWriter(mesh);

            writer.WriteGeometry();
            writer.WriteRooms();

            writer.world.Write(Path.Combine(outputDirPath, name + "_env." + fileType));
            writer.worldMarkers.Write(Path.Combine(outputDirPath, name + "_env_markers." + fileType));
            writer.rooms.Write(Path.Combine(outputDirPath, name + "_bnv." + fileType));

            for (int i = 0; i < writer.objects.Length; i++)
            {
                var builder = writer.objects[i];

                if (builder != null)
                    builder.Write(Path.Combine(outputDirPath, string.Format("{0}_{1}." + fileType, name, objectTypeNames[i])));
            }

            foreach (var pair in writer.scripts)
            {
                var scriptId = pair.Key;
                var builder = pair.Value;

                builder.Write(Path.Combine(outputDirPath, string.Format("{0}_script_{1}." + fileType, name, scriptId)));
            }
        }

        private AkiraDaeWriter(PolygonMesh source)
        {
            this.source = source;
        }

        private void WriteGeometry()
        {
            world = new DaeSceneBuilder();
            worldMarkers = new DaeSceneBuilder();
            objects = new DaeSceneBuilder[objectTypeNames.Length];
            scripts = new Dictionary<int, DaeSceneBuilder>();

            foreach (var polygon in source.Polygons)
            {
                if (polygon.Material == null)
                    continue;

                int objectType = polygon.ObjectType;
                int scriptId = polygon.ScriptId;

                if (scriptId != 0)
                {
                    string name = string.Format(CultureInfo.InvariantCulture, "script_{0}", scriptId);

                    DaeSceneBuilder sceneBuilder;

                    if (!scripts.TryGetValue(scriptId, out sceneBuilder))
                    {
                        sceneBuilder = new DaeSceneBuilder();
                        scripts.Add(scriptId, sceneBuilder);
                    }

                    var meshBuilder = sceneBuilder.GetMeshBuilder(name);

                    meshBuilder.AddPolygon(polygon);
                }
                else if (objectType == -1)
                {
                    //
                    // If it doesn't have an object type then it's probably an environment polygon.
                    //

                    string name;

                    if (source.HasDebugInfo)
                        name = polygon.FileName;
                    else
                        name = "world";

                    DaeMeshBuilder meshBuilder;

                    if (polygon.Material.IsMarker)
                        meshBuilder = worldMarkers.GetMeshBuilder(name);
                    else
                        meshBuilder = world.GetMeshBuilder(name);

                    meshBuilder.AddPolygon(polygon);
                }
                else
                {
                    //
                    // This polygon belongs to a object. Export it to one of the object files.
                    //

                    string name = string.Format(CultureInfo.InvariantCulture,
                        "{0}_{1}", objectTypeNames[objectType], polygon.ObjectId);

                    var sceneBuilder = objects[objectType];

                    if (sceneBuilder == null)
                    {
                        sceneBuilder = new DaeSceneBuilder();
                        objects[objectType] = sceneBuilder;
                    }

                    var meshBuilder = sceneBuilder.GetMeshBuilder(name);

                    meshBuilder.AddPolygon(polygon);
                }
            }

            foreach (var sceneBuilder in objects)
            {
                if (sceneBuilder == null)
                    continue;

                foreach (var meshBuilder in sceneBuilder.MeshBuilders)
                {
                    meshBuilder.ResetTransform();

                    if (source.HasDebugInfo)
                    {
                        //
                        // Polygons that belong to an object have useful object names in the debug info.
                        // Try to use them. 
                        //

                        var names = new List<string>();
                        int objectId = 0;

                        foreach (var polygon in meshBuilder.Polygons)
                        {
                            objectId = polygon.ObjectId;
                            names.Add(polygon.ObjectName);
                        }

                        string name = Utils.CommonPrefix(names);

                        if (!string.IsNullOrEmpty(name) && name.Length > 3)
                        {
                            if (!name.EndsWith("_", StringComparison.Ordinal))
                                name += "_";

                            meshBuilder.Name = string.Format(CultureInfo.InvariantCulture,
                                "{0}{1}", name, objectId);
                        }
                    }
                }
            }

            foreach (var sceneBuilder in scripts.Values)
            {
                foreach (var meshBuilder in sceneBuilder.MeshBuilders)
                {
                    meshBuilder.ResetTransform();

                    if (source.HasDebugInfo)
                    {
                        //
                        // Polygons that belong to an object have useful object names in the debug info.
                        // Try to use them. 
                        //

                        var names = new List<string>();
                        int scriptId = 0;

                        foreach (var polygon in meshBuilder.Polygons)
                        {
                            scriptId = polygon.ScriptId;
                            names.Add(polygon.ObjectName);
                        }

                        string name = Utils.CommonPrefix(names);

                        if (!string.IsNullOrEmpty(name) && name.Length > 3)
                        {
                            if (!name.EndsWith("_", StringComparison.Ordinal))
                                name += "_";

                            meshBuilder.Name = string.Format(CultureInfo.InvariantCulture,
                                "{0}{1}", name, scriptId);
                        }
                    }
                }
            }

            world.Build();
            worldMarkers.Build();

            foreach (var sceneBuilder in objects)
            {
                if (sceneBuilder == null)
                    continue;

                sceneBuilder.Build();
            }

            foreach (var sceneBuilder in scripts.Values)
            {
                sceneBuilder.Build();
            }
        }

        private void WriteRooms()
        {
            rooms = new DaeSceneBuilder();
            rooms.ImagesFolder = "grids";

            for (int i = 0; i < source.Rooms.Count; i++)
            {
                var room = source.Rooms[i];

                var meshBuilder = rooms.GetMeshBuilder(
                    string.Format(CultureInfo.InvariantCulture, "room_{0}", i));

                var material = source.Materials.GetMaterial(
                    string.Format(CultureInfo.InvariantCulture, "bnv_grid_{0:d3}", i));

                material.Image = room.Grid.ToImage();

                foreach (var polygonPoints in room.GetFloorPolygons())
                {
                    var texCoords = new Vector2[polygonPoints.Length];

                    for (int j = 0; j < polygonPoints.Length; j++)
                    {
                        var point = polygonPoints[j];
                        var min = room.BoundingBox.Min;
                        var max = room.BoundingBox.Max;

                        min += new Vector3(room.Grid.TileSize * room.Grid.XOrigin, 0.0f, room.Grid.TileSize * room.Grid.ZOrigin);
                        max -= new Vector3(room.Grid.TileSize * room.Grid.XOrigin, 0.0f, room.Grid.TileSize * room.Grid.ZOrigin);

                        var size = max - min;

                        float x = (point.X - min.X) / size.X;
                        float z = (point.Z - min.Z) / size.Z;

                        texCoords[j] = new Vector2(x, z);
                    }

                    meshBuilder.AddPolygon(material, polygonPoints, texCoords);
                    meshBuilder.Build();
                }
            }

            var ghostTexCoords = new[] {
                new Vector2(0.0f, 0.0f),
                new Vector2(1.0f, 0.0f),
                new Vector2(1.0f, 1.0f),
                new Vector2(0.0f, 1.0f)
            };

            for (int i = 0; i < source.Ghosts.Count; i++)
            {
                var ghost = source.Ghosts[i];

                var meshBuilder = rooms.GetMeshBuilder(
                    string.Format(CultureInfo.InvariantCulture, "ghost_{0}", i));

                meshBuilder.AddPolygon(
                    source.Materials.Markers.Ghost,
                    ghost.Points.ToArray(),
                    ghostTexCoords);

                meshBuilder.Build();
                meshBuilder.ResetTransform();
            }

            rooms.Build();
        }
    }
}
