﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Oni.Collections;
using Oni.Imaging;

namespace Oni.Akira
{
    internal class RoomDaeWriter
    {
        #region Private data
        private readonly PolygonMesh source;
        private DaeSceneBuilder world;

        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;
                this.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
                //

                Vector3 center = BoundingSphere.CreateFromPoints(points).Center;

                BoundingBox bbox = BoundingBox.CreateFromPoints(points);
                center.Y = bbox.Min.Y;

                translation = center;

                for (int i = 0; i < points.Count; i++)
                    points[i] -= center;
            }

            public void AddPolygon(Polygon polygon)
            {
                polygons.Add(new DaePolygon(
                    polygon,
                    Remap(polygon.Mesh.Points, polygon.PointIndices, points, uniquePoints),
                    null,
                    null));
            }

            public IEnumerable<Polygon> Polygons => 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()
            {
                var positionSource = new Dae.Source(points);
                var primitives = new Dae.MeshPrimitives(Dae.MeshPrimitiveType.Polygons);
                var posInput = new Dae.IndexedInput(Dae.Semantic.Position, positionSource);

                primitives.Inputs.Add(posInput);

                foreach (var poly in polygons)
                {
                    primitives.VertexCounts.Add(poly.PointIndices.Length);

                    posInput.Indices.AddRange(poly.PointIndices);
                }

                geometry = new Dae.Geometry
                {
                    Name = Name + "_geo",
                    Vertices = { new Dae.Input(Dae.Semantic.Position, positionSource) },
                    Primitives = { primitives }
                };
            }

            public Dae.Geometry Geometry
            {
                get
                {
                    return geometry;
                }
            }
        }

        #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
            {
                get { return 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 inst = new Dae.GeometryInstance(meshBuilder.Geometry);

                    var node = new Dae.Node();
                    node.Name = meshBuilder.Name;
                    node.Instances.Add(inst);

                    if (meshBuilder.Translation != Vector3.Zero)
                        node.Transforms.Add(new Dae.TransformTranslate(meshBuilder.Translation));

                    scene.Nodes.Add(node);
                }
            }

            private void BuildMaterials()
            {
                foreach (KeyValuePair<Material, Dae.Material> pair in materials)
                {
                    var material = pair.Key;

                    var image = new Dae.Image
                    {
                        FilePath = "./" + GetImageFileName(material).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)
                        }
                    };

                    var daeMaterial = pair.Value;
                    daeMaterial.Name = material.Name;
                    daeMaterial.Effect = effect;
                }
            }

            private string GetImageFileName(Material material)
            {
                if (material.IsMarker)
                    return Path.Combine("markers", material.Name + ".tga");

                return Path.Combine(imagesFolder, material.Name + ".tga");
            }

            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 Write(PolygonMesh mesh, string filePath)
        {
            var writer = new RoomDaeWriter(mesh);
            writer.WriteGeometry();
            writer.world.Write(filePath);
        }

        private RoomDaeWriter(PolygonMesh source)
        {
            this.source = source;
        }

        private void WriteGeometry()
        {
            world = new DaeSceneBuilder();

            for (int i = 0; i < source.Polygons.Count; i++)
            {
                var polygon = source.Polygons[i];

                var name = string.Format(CultureInfo.InvariantCulture, "floor_{0}", i);
                var meshBuilder = world.GetMeshBuilder(name);
                meshBuilder.AddPolygon(polygon);
            }

            foreach (var meshBuilder in world.MeshBuilders)
            {
                meshBuilder.ResetTransform();
            }

            world.Build();
        }
    }
}
