﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using Oni.Motoko;
using Oni.Physics;
using Oni.Totoro;

namespace Oni
{
    internal class SceneExporter
    {
        private readonly InstanceFileManager fileManager;
        private readonly string outputDirPath;
        private readonly TextureDaeWriter textureWriter;
        private readonly GeometryDaeWriter geometryWriter;
        private readonly BodyDaeWriter bodyWriter;
        private string basePath;

        private class SceneNode
        {
            public string Name;
            public readonly List<Geometry> Geometries = new List<Geometry>();
            public readonly List<SceneNodeAnimation> Animations = new List<SceneNodeAnimation>();
            public readonly List<SceneNode> Nodes = new List<SceneNode>();
            public Body Body;
            public bool IsCamera;
        }

        private class SceneNodeAnimation
        {
            public int Start;
            public ObjectAnimation ObjectAnimation;
        }

        public SceneExporter(InstanceFileManager fileManager, string outputDirPath)
        {
            this.fileManager = fileManager;
            this.outputDirPath = outputDirPath;

            textureWriter = new TextureDaeWriter(outputDirPath);
            geometryWriter = new GeometryDaeWriter(textureWriter);
            bodyWriter = new BodyDaeWriter(geometryWriter);
        }

        public void ExportScene(string sourceFilePath)
        {
            basePath = Path.GetDirectoryName(sourceFilePath);

            var scene = new Dae.Scene();

            var settings = new XmlReaderSettings
            {
                IgnoreWhitespace = true,
                IgnoreProcessingInstructions = true,
                IgnoreComments = true
            };

            var nodes = new List<SceneNode>();

            using (var xml = XmlReader.Create(sourceFilePath, settings))
            {
                scene.Name = xml.GetAttribute("Name");
                xml.ReadStartElement("Scene");

                while (xml.IsStartElement())
                    nodes.Add(ReadNode(xml));

                xml.ReadEndElement();
            }

            foreach (var node in nodes)
            {
                scene.Nodes.Add(WriteNode(node, null));
            }

            Dae.Writer.WriteFile(Path.Combine(outputDirPath, Path.GetFileNameWithoutExtension(sourceFilePath)) + ".dae", scene);
        }

        private string ResolvePath(string path)
        {
            return Path.Combine(basePath, path);
        }


        private SceneNode ReadNode(XmlReader xml)
        {
            var node = new SceneNode
            {
                Name = xml.GetAttribute("Name")
            };

            xml.ReadStartElement("Node");

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "Geometry":
                        ReadGeometry(xml, node);
                        break;
                    case "Body":
                        ReadBody(xml, node);
                        break;
                    case "Camera":
                        ReadCamera(xml, node);
                        break;
                    case "Animation":
                        ReadAnimation(xml, node);
                        break;
                    case "Node":
                        node.Nodes.Add(ReadNode(xml));
                        break;
                    default:
                        Console.WriteLine("Unknown element name {0}", xml.LocalName);
                        xml.Skip();
                        break;
                }
            }

            xml.ReadEndElement();

            return node;
        }

        private void ReadGeometry(XmlReader xml, SceneNode node)
        {
            var file = fileManager.OpenFile(ResolvePath(xml.ReadElementContentAsString()));
            var geometry = GeometryDatReader.Read(file.Descriptors[0]);

            node.Geometries.Add(geometry);
        }

        private void ReadBody(XmlReader xml, SceneNode node)
        {
            var file = fileManager.OpenFile(ResolvePath(xml.ReadElementContentAsString()));
            var body = BodyDatReader.Read(file.Descriptors[0]);

            node.Body = body;

            ReadBodyNode(node, body.Root);
        }

        private static void ReadBodyNode(SceneNode node, BodyNode bodyNode)
        {
            node.Name = bodyNode.Name;
            node.Geometries.Add(bodyNode.Geometry);

            foreach (var childBodyNode in bodyNode.Nodes)
            {
                var childNode = new SceneNode();
                node.Nodes.Add(childNode);
                ReadBodyNode(childNode, childBodyNode);
            }
        }

        private void ReadAnimation(XmlReader xml, SceneNode node)
        {
            var startValue = xml.GetAttribute("Start");
            var isMax = xml.GetAttribute("Type") == "Max";
            var noRotation = xml.GetAttribute("NoRotation") == "true";
            var filePath = xml.ReadElementContentAsString();

            var start = string.IsNullOrEmpty(startValue) ? 0 : int.Parse(startValue);
            var file = fileManager.OpenFile(ResolvePath(filePath));

            if (node.Body != null)
            {
                var animations = AnimationDatReader.Read(file.Descriptors[0]).ToObjectAnimation(node.Body);

                ReadBodyAnimation(start, node, node.Body.Root, animations);
            }
            else
            {
                node.Animations.Add(new SceneNodeAnimation
                {
                    Start = start,
                    ObjectAnimation = ObjectDatReader.ReadAnimation(file.Descriptors[0])
                });

                if (noRotation)
                {
                    foreach (var key in node.Animations.Last().ObjectAnimation.Keys)
                        key.Rotation = Quaternion.Identity;
                }
                else if (isMax)
                {
                    foreach (var key in node.Animations.Last().ObjectAnimation.Keys)
                        key.Rotation *= Quaternion.CreateFromAxisAngle(Vector3.UnitX, MathHelper.HalfPi);
                }
            }
        }

        private void ReadBodyAnimation(int start, SceneNode node, BodyNode bodyNode, ObjectAnimation[] animations)
        {
            node.Animations.Add(new SceneNodeAnimation
            {
                Start = start,
                ObjectAnimation = animations[bodyNode.Index]
            });

            for (int i = 0; i < node.Nodes.Count; i++)
                ReadBodyAnimation(start, node.Nodes[i], bodyNode.Nodes[i], animations);
        }

        private void ReadCamera(XmlReader xml, SceneNode node)
        {
            node.IsCamera = true;

            xml.Skip();
        }


        private Dae.Node WriteNode(SceneNode node, List<ObjectAnimationKey> parentFrames)
        {
            var daeNode = new Dae.Node
            {
                Name = node.Name
            };

            foreach (var geometry in node.Geometries)
                daeNode.Instances.Add(geometryWriter.WriteGeometryInstance(geometry, geometry.Name));

            if (node.IsCamera)
                WriteCamera(daeNode);

            List<ObjectAnimationKey> frames = null;

            if (node.Animations.Count > 0)
            {
                frames = BuildFrames(node);
                WriteAnimation(daeNode, BuildLocalFrames(node.Body == null ? parentFrames : null, frames));
            }

            foreach (var child in node.Nodes)
                daeNode.Nodes.Add(WriteNode(child, frames));

            return daeNode;
        }

        private static List<ObjectAnimationKey> BuildFrames(SceneNode node)
        {
            var frames = new List<ObjectAnimationKey>();

            foreach (var animation in node.Animations)
            {
                var animFrames = animation.ObjectAnimation.Interpolate();
                var start = animation.Start;

                if (frames.Count > 0)
                    start += frames.Last().Time + 1;

                foreach (var frame in animFrames)
                    frame.Time += start;

                if (frames.Count > 0)
                {
                    while (frames.Last().Time >= animFrames.First().Time)
                    {
                        frames.RemoveAt(frames.Count - 1);
                    }

                    while (frames.Last().Time + 1 < animFrames.First().Time)
                    {
                        frames.Add(new ObjectAnimationKey
                        {
                            Time = frames.Last().Time + 1,
                            Rotation = frames.Last().Rotation,
                            Translation = frames.Last().Translation,
                            Scale = frames.Last().Scale
                        });
                    }
                }

                frames.AddRange(animFrames);
            }

            return frames;
        }

        private static List<ObjectAnimationKey> BuildLocalFrames(List<ObjectAnimationKey> parentFrames, List<ObjectAnimationKey> frames)
        {
            var localFrames = frames;

            if (parentFrames != null)
            {
                localFrames = new List<ObjectAnimationKey>(localFrames.Count);

                for (int i = 0; i < frames.Count; i++)
                {
                    var frame = frames[i];
                    var parentFrame = parentFrames[i];

                    localFrames.Add(new ObjectAnimationKey
                    {
                        Time = frame.Time,
                        Scale = frame.Scale / parentFrame.Scale,
                        Rotation = Quaternion.Conjugate(parentFrame.Rotation) * frame.Rotation,
                        Translation = Vector3.Transform(frame.Translation - parentFrame.Translation, parentFrame.Rotation.Inverse()) / parentFrame.Scale
                    });
                }
            }

            return localFrames;
        }

        private static void WriteAnimation(Dae.Node node, List<ObjectAnimationKey> frames)
        {
            var times = new float[frames.Count];
            var interpolations = new string[times.Length];
            var positions = new Vector3[frames.Count];
            var angles = new Vector3[frames.Count];

            for (int i = 0; i < times.Length; ++i)
                times[i] = frames[i].Time / 60.0f;

            for (int i = 0; i < interpolations.Length; i++)
                interpolations[i] = "LINEAR";

            for (int i = 0; i < frames.Count; i++)
                positions[i] = frames[i].Translation;

            for (int i = 0; i < frames.Count; i++)
                angles[i] = frames[i].Rotation.ToEulerXYZ();

            var translate = node.Transforms.Translate("translate", positions[0]); ;
            var rotateX = node.Transforms.Rotate("rotX", Vector3.UnitX, angles[0].X);
            var rotateY = node.Transforms.Rotate("rotY", Vector3.UnitY, angles[0].Y);
            var rotateZ = node.Transforms.Rotate("rotZ", Vector3.UnitZ, angles[0].Z);
            var scale = node.Transforms.Scale("scale", frames[0].Scale);

            WriteSampler(times, interpolations, i => positions[i].X, translate, "X");
            WriteSampler(times, interpolations, i => positions[i].Y, translate, "Y");
            WriteSampler(times, interpolations, i => positions[i].Z, translate, "Z");
            WriteSampler(times, interpolations, i => angles[i].X, rotateX, "ANGLE");
            WriteSampler(times, interpolations, i => angles[i].Y, rotateY, "ANGLE");
            WriteSampler(times, interpolations, i => angles[i].Z, rotateZ, "ANGLE");
        }

        private static void WriteSampler(float[] times, string[] interpolations, Func<int, float> getValue, Dae.Transform transform, string targetName)
        {
            var values = new float[times.Length];

            for (int i = 0; i < values.Length; ++i)
                values[i] = getValue(i);

            transform.BindAnimation(targetName, new Dae.Sampler
            {
                Inputs = {
                    new Dae.Input(Dae.Semantic.Input, new Dae.Source(times, 1)),
                    new Dae.Input(Dae.Semantic.Output, new Dae.Source(values, 1)),
                    new Dae.Input(Dae.Semantic.Interpolation, new Dae.Source(interpolations, 1))
                }
            });
        }

        private static void WriteCamera(Dae.Node daeNode)
        {
            daeNode.Instances.Add(new Dae.CameraInstance
            {
                Target = new Dae.Camera
                {
                    XFov = 45.0f,
                    AspectRatio = 4.0f / 3.0f,
                    ZNear = 1.0f,
                    ZFar = 10000.0f
                }
            });
        }
    }
}
