﻿using System;
using System.Collections.Generic;
using System.IO;

namespace Oni.Akira
{
    internal class RoomExtractor
    {
        private readonly IEnumerable<string> fromFiles;
        private readonly string outputFilePath;

        private PolygonMesh mesh;
        private List<Vector3> positions;
        private Stack<Matrix> nodeTransformStack;
        private Matrix nodeTransform;
        private string nodeName;

        public RoomExtractor(IEnumerable<string> fromFiles, string outputFilePath)
        {
            this.fromFiles = fromFiles;
            this.outputFilePath = outputFilePath;
        }

        public void Extract()
        {
            mesh = new PolygonMesh(new MaterialLibrary());

            positions = mesh.Points;

            nodeTransformStack = new Stack<Matrix>();
            nodeTransform = Matrix.Identity;

            foreach (var filePath in fromFiles)
                ReadScene(Dae.Reader.ReadFile(filePath));

            var q = new PolygonQuadrangulate(mesh);
            q.Execute();

            RoomDaeWriter.Write(mesh, outputFilePath);
        }

        private void ReadScene(Dae.Scene scene)
        {
            foreach (var node in scene.Nodes)
                ReadNode(node);
        }

        private void ReadNode(Dae.Node node)
        {
            nodeTransformStack.Push(nodeTransform);

            foreach (var transform in node.Transforms)
                nodeTransform = transform.ToMatrix() * nodeTransform;

            nodeName = node.Name;

            foreach (var geometryInstance in node.GeometryInstances)
                ReadGeometryInstance(geometryInstance);

            foreach (var child in node.Nodes)
                ReadNode(child);

            nodeTransform = nodeTransformStack.Pop();
        }

        private void ReadGeometryInstance(Dae.GeometryInstance instance)
        {
            foreach (var primitives in instance.Target.Primitives)
            {
                if (primitives.PrimitiveType != Dae.MeshPrimitiveType.Polygons)
                {
                    Console.Error.WriteLine("Unsupported primitive type '{0}' found in geometry '{1}', ignoring.", primitives.PrimitiveType, instance.Name);
                    continue;
                }

                ReadPolygonPrimitives(primitives, instance.Materials.Find(m => m.Symbol == primitives.MaterialSymbol));
            }
        }

        private void ReadPolygonPrimitives(Dae.MeshPrimitives primitives, Dae.MaterialInstance materialInstance)
        {
            int[] positionIndices = null;

            foreach (var input in primitives.Inputs)
            {
                switch (input.Semantic)
                {
                    case Dae.Semantic.Position:
                        positionIndices = ReadInputIndexed(input, positions, Dae.Source.ReadVector3);
                        break;
                }
            }

            foreach (int i in positionIndices)
                positions[i] = Vector3.Transform(positions[i], ref nodeTransform);

            int startIndex = 0;

            foreach (int vertexCount in primitives.VertexCounts)
            {
                var polygonPointIndices = new int[vertexCount];
                Array.Copy(positionIndices, startIndex, polygonPointIndices, 0, vertexCount);

                var polygon = new Polygon(mesh, polygonPointIndices);

                if (Vector3.Dot(polygon.Plane.Normal, Vector3.UnitY) >= 0.3420201f)
                    mesh.Polygons.Add(polygon);

                startIndex += vertexCount;
            }
        }

        private static int[] ReadInputIndexed<T>(Dae.IndexedInput input, List<T> list, Func<Dae.Source, int, T> elementReader)
            where T : struct
        {
            var indices = new int[input.Indices.Count];

            for (int i = 0; i < input.Indices.Count; i++)
            {
                var v = elementReader(input.Source, input.Indices[i]);
                indices[i] = list.Count;
                list.Add(v);
            }

            return indices;
        }
    }
}
