﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;

namespace Oni.Level
{
    using Akira;
    using Metadata;
    using Motoko;
    using Physics;

    partial class LevelImporter
    {
        private List<Dae.Scene> roomScenes;
        private PolygonMesh model;
        private AkiraDaeReader daeReader;

        private void ReadModel(XmlReader xml, string basePath)
        {
            xml.ReadStartElement("Environment");

            xml.ReadStartElement("Model");

            daeReader = new AkiraDaeReader();
            model = daeReader.Mesh;
            level.model = model;

            info.WriteLine("Reading environment...");

            while (xml.IsStartElement())
            {
                switch (xml.LocalName)
                {
                    case "Import":
                    case "Scene":
                        ImportModelScene(xml, basePath);
                        break;

                    case "Object":
                        xml.Skip();
                        break;

                    case "Camera":
                        ReadCamera(xml, basePath);
                        break;

                    case "Texture":
                        textureImporter.ReadOptions(xml, basePath);
                        break;

                    default:
                        error.WriteLine("Unknown element {0}", xml.LocalName);
                        xml.Skip();
                        break;
                }
            }

            info.WriteLine("Reading rooms...");

            roomScenes = new List<Dae.Scene>();

            xml.ReadEndElement();
            xml.ReadStartElement("Rooms");

            while (xml.IsStartElement("Import"))
            {
                string filePath = xml.GetAttribute("Path");

                if (filePath == null)
                    filePath = xml.ReadElementContentAsString();
                else
                    xml.Skip();

                filePath = Path.Combine(basePath, filePath);

                roomScenes.Add(LoadScene(filePath));
            }

            xml.ReadEndElement();

            if (xml.IsStartElement("Textures"))
                ReadTextures(xml, basePath);

            xml.ReadEndElement();
        }

        private class NodePropertiesReader
        {
            private readonly string basePath;
            private readonly TextWriter error;
            public readonly Dictionary<string, AkiraDaeNodeProperties> properties = new Dictionary<string, AkiraDaeNodeProperties>(StringComparer.Ordinal);

            public NodePropertiesReader(string basePath, TextWriter error)
            {
                this.basePath = basePath;
                this.error = error;
            }

            public Dictionary<string, AkiraDaeNodeProperties> Properties
            {
                get { return properties; }
            }

            public void ReadScene(XmlReader xml, Dae.Node scene)
            {
                var nodeProperties = new ObjectDaeNodeProperties();
                properties.Add(scene.Id, nodeProperties);

                while (xml.IsStartElement())
                {
                    switch (xml.LocalName)
                    {
                        case "GunkFlags":
                            nodeProperties.GunkFlags = xml.ReadElementContentAsEnum<GunkFlags>();
                            break;
                        case "ScriptId":
                            nodeProperties.ScriptId = xml.ReadElementContentAsInt();
                            break;
                        case "Node":
                            ReadNode(xml, scene, nodeProperties);
                            break;
                        default:
                            xml.Skip();
                            break;
                    }
                }
            }

            private void ReadNode(XmlReader xml, Dae.Node parentNode, ObjectDaeNodeProperties parentNodeProperties)
            {
                string id = xml.GetAttribute("Id");

                if (string.IsNullOrEmpty(id))
                {
                    error.Write("Each import node must have an Id attribute");
                    xml.Skip();
                    return;
                }

                var nodeProperties = new ObjectDaeNodeProperties {
                    GunkFlags = parentNodeProperties.GunkFlags,
                    ScriptId = parentNodeProperties.ScriptId,
                    HasPhysics = parentNodeProperties.HasPhysics
                };

                properties.Add(id, nodeProperties);

                xml.ReadStartElement("Node");

                while (xml.IsStartElement())
                {
                    switch (xml.LocalName)
                    {
                        case "GunkFlags":
                            nodeProperties.GunkFlags |= xml.ReadElementContentAsEnum<GunkFlags>();
                            break;
                        case "ScriptId":
                            nodeProperties.ScriptId = xml.ReadElementContentAsInt();
                            break;
                        case "Physics":
                            nodeProperties.PhysicsType = xml.ReadElementContentAsEnum<ObjectPhysicsType>();
                            nodeProperties.HasPhysics = true;
                            break;
                        case "ObjectFlags":
                            nodeProperties.ObjectFlags = xml.ReadElementContentAsEnum<ObjectSetupFlags>();
                            nodeProperties.HasPhysics = true;
                            break;
                        case "Animation":
                            nodeProperties.Animations.Add(ReadAnimationClip(xml));
                            nodeProperties.HasPhysics = true;
                            break;
                        case "Particles":
                            nodeProperties.Particles.AddRange(ReadParticles(xml, basePath));
                            nodeProperties.HasPhysics = true;
                            break;
                        default:
                            error.WriteLine("Unknown physics object element {0}", xml.LocalName);
                            xml.Skip();
                            break;
                    }
                }

                xml.ReadEndElement();
            }

            private ObjectAnimationClip ReadAnimationClip(XmlReader xml)
            {
                var animClip = new ObjectAnimationClip(xml.GetAttribute("Name"));

                if (!xml.SkipEmpty())
                {
                    xml.ReadStartElement();

                    while (xml.IsStartElement())
                    {
                        switch (xml.LocalName)
                        {
                            case "Start":
                                animClip.Start = xml.ReadElementContentAsInt();
                                break;
                            case "Stop":
                                animClip.Stop = xml.ReadElementContentAsInt();
                                break;
                            case "End":
                                animClip.End = xml.ReadElementContentAsInt();
                                break;
                            case "Flags":
                                animClip.Flags = xml.ReadElementContentAsEnum<ObjectAnimationFlags>();
                                break;
                            default:
                                error.WriteLine("Unknown object animation property {0}", xml.LocalName);
                                xml.Skip();
                                break;
                        }
                    }

                    xml.ReadEndElement();
                }

                return animClip;
            }
        }

        private void ImportModelScene(XmlReader xml, string basePath)
        {
            var filePath = Path.Combine(basePath, xml.GetAttribute("Path"));
            var scene = LoadScene(filePath);

            var propertiesReader = new NodePropertiesReader(basePath, error);

            if (!xml.SkipEmpty())
            {
                xml.ReadStartElement();
                propertiesReader.ReadScene(xml, scene);
                xml.ReadEndElement();
            }

            daeReader.ReadScene(scene, propertiesReader.Properties);

            if (propertiesReader.Properties.Values.Any(p => p.HasPhysics))
            {
                var imp = new ObjectDaeImporter(textureImporter, propertiesReader.Properties);

                imp.Import(scene);

                foreach (var node in imp.Nodes.Where(n => n.Geometries.Length > 0))
                {
                    var setup = new ObjectSetup {
                        Name = node.Name,
                        FileName = node.FileName,
                        ScriptId = node.ScriptId,
                        Flags = node.Flags,
                        PhysicsType = ObjectPhysicsType.Animated
                    };

                    setup.Geometries = node.Geometries
                        .Where(n => (n.Flags & GunkFlags.Invisible) == 0)
                        .Select(n => n.Geometry.Name).ToArray();

                    foreach (var nodeGeometry in node.Geometries.Where(g => (g.Flags & GunkFlags.Invisible) == 0))
                    {
                        var writer = new DatWriter();
                        GeometryDatWriter.Write(nodeGeometry.Geometry, writer.ImporterFile);
                        writer.Write(outputDirPath);
                    }

                    setup.Position = Vector3.Zero;
                    setup.Orientation = Quaternion.Identity;
                    setup.Scale = 1.0f;
                    setup.Origin = Matrix.CreateFromQuaternion(setup.Orientation)
                        * Matrix.CreateScale(setup.Scale)
                        * Matrix.CreateTranslation(setup.Position);

                    //int i = 0;

                    foreach (var animation in node.Animations)
                    {
                        //if (nodes.Count > 1)
                        //    animation.Name += i.ToString("d2", CultureInfo.InvariantCulture);

                        if ((animation.Flags & ObjectAnimationFlags.Local) == 0)
                        {
                            //animation.Scale = Matrix.CreateScale(setup.Scale);

                            foreach (var key in animation.Keys)
                            {
                                key.Rotation = setup.Orientation * key.Rotation;
                                key.Translation += setup.Position;
                            }
                        }

                        if ((animation.Flags & ObjectAnimationFlags.AutoStart) != 0)
                        {
                            setup.Animation = animation;
                            setup.PhysicsType = ObjectPhysicsType.Animated;
                        }

                        var writer = new DatWriter();
                        writer.BeginImport();
                        ObjectDatWriter.WriteAnimation(animation, writer);
                        writer.Write(outputDirPath);
                    }

                    if (setup.Animation == null && node.Animations.Length > 0)
                    {
                        setup.Animation = node.Animations[0];
                    }

                    if (setup.Animation != null)
                    {
                        var frame0 = setup.Animation.Keys[0];

                        setup.Scale = frame0.Scale.X;
                        setup.Orientation = frame0.Rotation;
                        setup.Position = frame0.Translation;
                    }

                    level.physics.Add(setup);
                }
            }
        }

        private void ImportModel(string basePath)
        {
            info.WriteLine("Importing objects...");
            ImportGunkObjects();

            info.WriteLine("Importing textures...");
            ImportModelTextures();

            info.WriteLine("Generating grids...");

            string gridFilePath = Path.Combine(basePath, string.Format("temp/grids/{0}_grids.dae", level.name));

            var gridBuilder = new RoomGridBuilder(roomScenes[0], model);
            gridBuilder.Build();
            AkiraDaeWriter.WriteRooms(gridBuilder.Mesh, gridFilePath);

            daeReader.ReadScene(Dae.Reader.ReadFile(gridFilePath), new Dictionary<string, AkiraDaeNodeProperties>());

            info.WriteLine("Writing environment...");

            var writer = new DatWriter();
            AkiraDatWriter.Write(model, writer, level.name, debug);
            writer.Write(outputDirPath);
        }

        private void ImportGunkNode(int gunkId, Matrix transform, GunkFlags flags, Geometry geometry)
        {
            ImportGunk(gunkId, transform, flags, geometry, null);
        }

        private void ImportGunk(int gunkId, Matrix transform, GunkFlags flags, Geometry geometry, string textureName)
        {
            TextureFormat? textureFormat = null;

            if (geometry.Texture != null)
            {
                Texture texture = null;

                if (!geometry.Texture.IsPlaceholder)
                {
                    texture = TextureDatReader.ReadInfo(geometry.Texture);
                }
                else
                {
                    var txmp = FindSharedInstance(TemplateTag.TXMP, geometry.Texture.Name);

                    if (txmp != null)
                        texture = TextureDatReader.ReadInfo(txmp);
                }

                if (texture != null)
                    textureFormat = texture.Format;
            }
            else
            {
                if (geometry.TextureName != null)
                {
                    var options = textureImporter.GetOptions(geometry.TextureName, false);

                    if (options != null)
                        textureFormat = options.Format;
                }
            }

            switch (textureFormat)
            {
                case TextureFormat.BGRA4444:
                case TextureFormat.BGRA5551:
                case TextureFormat.RGBA:
                    flags |= GunkFlags.Transparent | GunkFlags.TwoSided | GunkFlags.NoOcclusion;
                    break;
            }

            Material material;

            if (!string.IsNullOrEmpty(textureName))
                material = model.Materials.GetMaterial(textureName);
            else if (!string.IsNullOrEmpty(geometry.TextureName))
                material = model.Materials.GetMaterial(geometry.TextureName);
            else if (geometry.Texture != null)
                material = model.Materials.GetMaterial(geometry.Texture.Name);
            else
                material = model.Materials.GetMaterial("NONE");

            int pointIndexBase = model.Points.Count;
            int texCoordIndexBase = model.TexCoords.Count;

            model.Points.AddRange(Vector3.Transform(geometry.Points, ref transform));
            model.TexCoords.AddRange(geometry.TexCoords);

            foreach (var quad in Quadify.Do(geometry))
            {
                var pointIndices = new int[quad.Length];
                var texCoordIndices = new int[quad.Length];
                var colors = new Imaging.Color[quad.Length];

                for (int j = 0; j < quad.Length; j++)
                {
                    pointIndices[j] = pointIndexBase + quad[j];
                    texCoordIndices[j] = texCoordIndexBase + quad[j];
                    colors[j] = new Imaging.Color(207, 207, 207);
                }

                var poly = new Polygon(model, pointIndices) {
                    TexCoordIndices = texCoordIndices,
                    Colors = colors,
                    Material = material,
                    ObjectId = gunkId & 0xffffff,
                    ObjectType = gunkId >> 24
                };

                poly.Flags |= flags;
                model.Polygons.Add(poly);
            }
        }

        private void ImportModelTextures()
        {
            int imported = 0;
            int copied = 0;

            var copy = new List<InstanceDescriptor>();

            foreach (var material in model.Polygons.Select(p => p.Material).Distinct())
            {
                //if (material.IsMarker)
                //    continue;

                if (File.Exists(material.ImageFilePath))
                {
                    var options = textureImporter.AddMaterial(material);

                    if (options != null)
                        material.Flags |= options.GunkFlags;

                    imported++;
                }
                else
                {
                    var txmp = FindSharedInstance(TemplateTag.TXMP, material.Name);

                    if (txmp != null)
                        copy.Add(txmp);
                }
            }

            Parallel.ForEach(copy, txmp => {
                var texture = TextureDatReader.Read(txmp);

                if ((texture.Flags & TextureFlags.HasMipMaps) == 0)
                {
                    texture.GenerateMipMaps();
                    TextureDatWriter.Write(texture, outputDirPath);
                    System.Threading.Interlocked.Increment(ref imported);
                }
                else
                {
                    string sourceFilePath = txmp.File.FilePath;
                    File.Copy(sourceFilePath, Path.Combine(outputDirPath, Path.GetFileName(sourceFilePath)), true);
                    System.Threading.Interlocked.Increment(ref copied);
                }
            });

            error.WriteLine("Imported {0} textures, copied {1} textures", imported, copied);
        }

        private ObjectAnimationClip ReadAnimationClip(XmlReader xml)
        {
            var anim = new ObjectAnimationClip(xml.GetAttribute("Name"));

            if (!xml.SkipEmpty())
            {
                xml.ReadStartElement();

                while (xml.IsStartElement())
                {
                    switch (xml.LocalName)
                    {
                        case "Start":
                            anim.Start = xml.ReadElementContentAsInt();
                            break;
                        case "Stop":
                            anim.Stop = xml.ReadElementContentAsInt();
                            break;
                        case "End":
                            anim.End = xml.ReadElementContentAsInt();
                            break;
                        case "Flags":
                            anim.Flags = xml.ReadElementContentAsEnum<ObjectAnimationFlags>();
                            break;
                        default:
                            error.WriteLine("Unknown object animation parameter {0}", xml.LocalName);
                            xml.Skip();
                            break;
                    }
                }

                xml.ReadEndElement();
            }

            return anim;
        }
    }
}
