﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Oni.Collections;

namespace Oni.Dae.IO
{
    internal class ObjWriter
    {
        private int vBase = 1, vtBase = 1, vnBase = 1;
        private Set<Material> materials = new Set<Material>();
        private StreamWriter objWriter;
        private StreamWriter mtlWriter;

        internal static void WriteFile(string filePath, Scene scene)
        {
            var writer = new ObjWriter();
            writer.Write(filePath, scene);
        }

        private void Write(string filePath, Scene scene)
        {
            using (objWriter = File.CreateText(filePath))
            {
                objWriter.WriteLine("# Generated by OniSplit v{0}", Utils.Version);
                objWriter.WriteLine();

                objWriter.WriteLine("mtllib {0}", Path.ChangeExtension(Path.GetFileName(filePath), ".mtl"));
                objWriter.WriteLine();

                var rootTransform = Matrix.Identity;

                WriteNode(scene, ref rootTransform);
            }

            using (mtlWriter = File.CreateText(Path.ChangeExtension(filePath, ".mtl")))
            {
                mtlWriter.WriteLine("# Generated by OniSplit v{0}", Utils.Version);
                mtlWriter.WriteLine();

                WriteMaterialLibrary();
            }
        }

        private void WriteNode(Node node, ref Matrix parentTransform)
        {
            if (!string.IsNullOrEmpty(node.Name))
                objWriter.WriteLine("g {0}", node.Name);

            var transform = node.Transforms.ToMatrix() * parentTransform;

            foreach (var geometryInstance in node.GeometryInstances)
                WriteGeometry(geometryInstance, ref transform);

            foreach (var child in node.Nodes)
                WriteNode(child, ref transform);
        }

        private void WriteGeometry(GeometryInstance geometryInstance, ref Matrix transform)
        {
            var geometry = geometryInstance.Target;

            foreach (var primitives in geometry.Primitives)
            {
                var posInput = WritePositions(primitives, ref transform);
                var texCoordInput = WriteTexCoords(primitives);
                var normalInput = WriteNormals(primitives, ref transform);

                WriteUseMaterial(geometryInstance, primitives);
                WriteFaces(primitives, posInput, texCoordInput, normalInput);

                vBase += posInput.Source.Count;
                vtBase += texCoordInput == null ? 0 : texCoordInput.Source.Count;
                vnBase += normalInput == null ? 0: normalInput.Source.Count;
            }
        }

        private IndexedInput WritePositions(MeshPrimitives primitives, ref Matrix transform)
        {
            var input = primitives.Inputs.FirstOrDefault(i => i.Semantic == Semantic.Position);
            var source = input.Source;

            for (int i = 0; i < source.Count; i++)
            {
                var position = Vector3.Transform(Source.ReadVector3(source, i), ref transform);

                objWriter.Write("v ");
                objWriter.Write(position.X.ToString(CultureInfo.InvariantCulture));
                objWriter.Write(' ');
                objWriter.Write(position.Y.ToString(CultureInfo.InvariantCulture));
                objWriter.Write(' ');
                objWriter.Write(position.Z.ToString(CultureInfo.InvariantCulture));
                objWriter.WriteLine();
            }

            objWriter.WriteLine();

            return input;
        }

        private IndexedInput WriteTexCoords(MeshPrimitives primitives)
        {
            var input = primitives.Inputs.FirstOrDefault(i => i.Semantic == Semantic.TexCoord);

            if (input == null)
                return null;

            var source = input.Source;
            var data = source.FloatData;

            for (int i = 0; i < data.Length; i += source.Stride)
            {
                objWriter.Write("vt ");
                objWriter.Write(data[i + 0].ToString(CultureInfo.InvariantCulture));
                objWriter.Write(' ');
                objWriter.Write(data[i + 1].ToString(CultureInfo.InvariantCulture));
                objWriter.WriteLine();
            }

            objWriter.WriteLine();

            return input;
        }

        private IndexedInput WriteNormals(MeshPrimitives primitives, ref Matrix transform)
        {
            var input = primitives.Inputs.FirstOrDefault(i => i.Semantic == Semantic.Normal);

            if (input == null)
                return null;

            var source = input.Source;

            for (int i = 0; i < source.Count; i++)
            {
                var normal = Vector3.TransformNormal(Source.ReadVector3(source, i), ref transform);

                objWriter.Write("vn ");
                objWriter.Write(normal.X.ToString(CultureInfo.InvariantCulture));
                objWriter.Write(' ');
                objWriter.Write(normal.Y.ToString(CultureInfo.InvariantCulture));
                objWriter.Write(' ');
                objWriter.Write(normal.Z.ToString(CultureInfo.InvariantCulture));
                objWriter.WriteLine();
            }

            objWriter.WriteLine();

            return input;
        }

        private void WriteFaces(MeshPrimitives primitives, IndexedInput posInput, IndexedInput texCoordInput, IndexedInput normalInput)
        {
            var positionIndices = posInput.Indices;
            var texCoordIndices = texCoordInput == null ? null : texCoordInput.Indices;
            var normalIndices = normalInput == null ? null : normalInput.Indices;
            int vertexIndex = 0;

            foreach (var vertexCount in primitives.VertexCounts)
            {
                objWriter.Write("f");

                for (int i = vertexIndex; i < vertexIndex + vertexCount; i++)
                {
                    objWriter.Write(' ');
                    objWriter.Write((vBase + positionIndices[i]).ToString(CultureInfo.InvariantCulture));

                    if (texCoordIndices != null)
                    {
                        objWriter.Write('/');
                        objWriter.Write((vtBase + texCoordIndices[i]).ToString(CultureInfo.InvariantCulture));
                    }
                    else if (normalIndices != null)
                    {
                        objWriter.Write('/');
                    }

                    if (normalIndices != null)
                    {
                        objWriter.Write('/');
                        objWriter.Write((vnBase + normalIndices[i]).ToString(CultureInfo.InvariantCulture));
                    }
                }

                objWriter.WriteLine();

                vertexIndex += vertexCount;
            }

            objWriter.WriteLine();
        }

        private void WriteUseMaterial(GeometryInstance geometryInstance, MeshPrimitives primitives)
        {
            if (string.IsNullOrEmpty(primitives.MaterialSymbol))
            {
                objWriter.WriteLine("usemtl");
            }
            else
            {
                var materialInstance = geometryInstance.Materials.Find(m => m.Symbol == primitives.MaterialSymbol);

                if (materialInstance != null && materialInstance.Target != null)
                {
                    objWriter.WriteLine("usemtl {0}", materialInstance.Target.Name);
                    materials.Add(materialInstance.Target);
                }
            }
        }

        private void WriteMaterialLibrary()
        {
            foreach (var material in materials)
            {
                mtlWriter.WriteLine("newmtl {0}", material.Name);

                var effect = material.Effect;

                WriteMaterialColor("Ka", effect.Ambient);
                WriteMaterialColor("Kd", effect.Diffuse);

                mtlWriter.WriteLine("Ks 0 0 0");
                mtlWriter.WriteLine("Ns 0");

                WriteMaterialTextureMap("map_Kd", effect.Diffuse);
                WriteMaterialTextureMap("map_Tr", effect.Transparent);

                if (effect.TransparentValue is EffectTexture)
                    mtlWriter.WriteLine("illum 9");
                else
                    mtlWriter.WriteLine("illum 2");

                mtlWriter.WriteLine();
            }
        }

        private void WriteMaterialColor(string mtlCommand, EffectParameter effectParam)
        {
            if (effectParam == null || !(effectParam.Value is Vector4))
                return;

            var color = (Vector4)effectParam.Value;
            mtlWriter.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3}", mtlCommand, color.X, color.Y, color.Z));
        }

        private void WriteMaterialTextureMap(string mtlCommand, EffectParameter effectParam)
        {
            if (effectParam == null)
                return;

            var texture = effectParam.Value as EffectTexture;

            if (texture == null)
                return;

            mtlWriter.WriteLine("{0} {1}", mtlCommand, texture.Sampler.Surface.InitFrom.FilePath);
        }
    }
}
