﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using Oni.Xml;

namespace Oni.Level
{
    using Akira;
    using Motoko;
    using Objects;
    using Physics;

    partial class LevelImporter
    {
        private List<ObjectBase> objects;
        private ObjectLoadContext objectLoadContext;

        private void ReadObjects(XmlReader xml, string basePath)
        {
            info.WriteLine("Reading objects...");

            objects = new List<ObjectBase>();
            objectLoadContext = new ObjectLoadContext(FindSharedInstance, info);

            xml.ReadStartElement("Objects");

            while (xml.IsStartElement())
                ReadObjectFile(Path.Combine(basePath, xml.ReadElementContentAsString("Import", "")));

            xml.ReadEndElement();
        }

        private void ReadObjectFile(string filePath)
        {
            var basePath = Path.GetDirectoryName(filePath);

            objectLoadContext.BasePath = basePath;
            objectLoadContext.FilePath = filePath;

            var settings = new XmlReaderSettings {
                IgnoreWhitespace = true,
                IgnoreProcessingInstructions = true,
                IgnoreComments = true
            };

            using (var xml = XmlReader.Create(filePath, settings))
            {
                xml.ReadStartElement("Oni");

                switch (xml.LocalName)
                {
                    case "Objects":
                        objects.AddRange(ReadObjects(xml));
                        break;

                    case "Particles":
                        level.particles.AddRange(ReadParticles(xml, basePath));
                        break;

                    case "Characters":
                        level.characters.AddRange(ReadCharacters(xml, basePath));
                        break;

                    case "Physics":
                        ReadPhysics(xml, basePath);
                        break;

                    case "Corpses":
                    case "CRSA":
                        level.corpses.AddRange(ReadCorpses(xml, basePath));
                        break;

                    default:
                        error.WriteLine("Unknown object file type {0}", xml.LocalName);
                        xml.Skip();
                        break;
                }

                xml.ReadEndElement();
            }
        }

        private IEnumerable<ObjectBase> ReadObjects(XmlReader xml)
        {
            if (xml.SkipEmpty())
                yield break;

            xml.ReadStartElement();

            while (xml.IsStartElement())
            {
                ObjectBase obj;

                switch (xml.LocalName)
                {
                    case "CHAR":
                    case "Character":
                        obj = new Character();
                        break;
                    case "WEAP":
                    case "Weapon":
                        obj = new Weapon();
                        break;
                    case "PART":
                    case "Particle":
                        obj = new Particle();
                        break;
                    case "PWRU":
                    case "PowerUp":
                        obj = new PowerUp();
                        break;
                    case "FLAG":
                    case "Flag":
                        obj = new Flag();
                        break;
                    case "DOOR":
                    case "Door":
                        obj = new Door();
                        break;
                    case "CONS":
                    case "Console":
                        obj = new Console();
                        break;
                    case "FURN":
                    case "Furniture":
                        obj = new Furniture();
                        break;
                    case "TURR":
                    case "Turret":
                        obj = new Turret();
                        break;
                    case "SNDG":
                    case "Sound":
                        obj = new Sound();
                        break;
                    case "TRIG":
                    case "Trigger":
                        obj = new Trigger();
                        break;
                    case "TRGV":
                    case "TriggerVolume":
                        obj = new TriggerVolume();
                        break;
                    case "NEUT":
                    case "Neutral":
                        obj = new Neutral();
                        break;
                    case "PATR":
                    case "Patrol":
                        obj = new PatrolPath();
                        break;
                    default:
                        error.WriteLine("Unknonw object type {0}", xml.LocalName);
                        xml.Skip();
                        continue;
                }

                obj.Read(xml, objectLoadContext);

                var gunkObject = obj as GunkObject;

                if (gunkObject != null && gunkObject.GunkClass == null)
                    continue;

                yield return obj;
            }

            xml.ReadEndElement();
        }

        private void ImportGunkObjects()
        {
            int nextObjectId = 1;

            foreach (var obj in objects)
            {
                obj.ObjectId = nextObjectId++;

                if (obj is Door)
                    ImportDoor((Door)obj);
                else if (obj is Furniture)
                    ImportFurniture((Furniture)obj);
                else if (obj is GunkObject)
                    ImportGunkObject((GunkObject)obj, GunkFlags.NoOcclusion);
            }
        }

        private void ImportFurniture(Furniture furniture)
        {
            ImportGunkObject(furniture, GunkFlags.NoOcclusion | GunkFlags.Furniture);

            foreach (var particle in furniture.Class.Geometry.Particles)
                ImportParticle(furniture.ParticleTag, furniture.Transform, particle);
        }

        private void ImportGunkObject(GunkObject gunkObject, GunkFlags flags)
        {
            foreach (var node in gunkObject.GunkClass.GunkNodes)
                ImportGunkNode(gunkObject.GunkId, gunkObject.Transform, node.Flags | flags, node.Geometry);
        }

        private void ImportDoor(Door door)
        {
            var placement = door.Transform;

            float minY = 0.0f;
            float minX = 0.0f;
            float maxX = 0.0f;

            var geometryTransform = Matrix.CreateScale(door.Class.Animation.Keys[0].Scale) * Matrix.CreateRotationX(-MathHelper.HalfPi);

            foreach (var node in door.Class.GunkNodes)
            {
                var bbox = BoundingBox.CreateFromPoints(Vector3.Transform(node.Geometry.Points, ref geometryTransform));

                minY = Math.Min(minY, bbox.Min.Y);
                minX = Math.Min(minX, bbox.Min.X);
                maxX = Math.Max(maxX, bbox.Max.X);
            }

            placement.Translation -= Vector3.UnitY * minY;

            float xOffset;
            int sides;

            if ((door.Flags & DoorFlags.DoubleDoor) == 0)
            {
                xOffset = 0.0f;
                sides = 1;
            }
            else
            {
                xOffset = (maxX - minX) / 2.0f;
                sides = 2;
            }

            for (int side = 0; side < sides; side++)
            {
                Matrix origin, gunkTransform;

                if (side == 0)
                {
                    var m1 = Matrix.CreateTranslation(xOffset, 0.0f, 0.0f) * placement;
                    origin = geometryTransform * m1;
                    gunkTransform = origin;
                }
                else
                {
                    var m2 = Matrix.CreateTranslation(-xOffset, 0.0f, 0.0f) * placement;
                    origin = Matrix.CreateRotationY(MathHelper.Pi) * geometryTransform * m2;
                    gunkTransform = geometryTransform * Matrix.CreateRotationY(MathHelper.Pi) * m2;
                }

                var scriptId = door.ScriptId | (side << 12);
                var geometries = ImportDoorGeometry(door, side);

                level.physics.Add(new ObjectSetup {
                    Name = string.Format("door_{0}", scriptId),
                    Flags = ObjectSetupFlags.FaceCollision,
                    DoorScriptId = scriptId,
                    Origin = origin,
                    Geometries = geometries
                });

                foreach (var geometry in geometries)
                    ImportGunkNode(door.GunkId, gunkTransform, GunkFlags.NoDecals | GunkFlags.NoCollision, geometry);
            }
        }

        private Geometry[] ImportDoorGeometry(Door door, int side)
        {
            InstanceDescriptor overrideTexture = null;

            if (!string.IsNullOrEmpty(door.Textures[side]))
                overrideTexture = FindSharedInstance(TemplateTag.TXMP, door.Textures[side]);

            var nodes = door.Class.Geometry.Geometries;
            var geometries = new Geometry[nodes.Length];

            for (int i = 0; i < nodes.Length; i++)
            {
                var node = nodes[i];

                var geometry = new Geometry {
                    Points = node.Geometry.Points,
                    TexCoords = node.Geometry.TexCoords,
                    Normals = node.Geometry.Normals,
                    Triangles = node.Geometry.Triangles
                };

                if (overrideTexture != null)
                {
                    geometry.Texture = overrideTexture;
                    geometry.TextureName = overrideTexture.Name;
                }
                else if (node.Geometry.Texture != null)
                {
                    geometry.TextureName = node.Geometry.Texture.Name;
                }

                geometries[i] = geometry;
            }

            return geometries;
        }

        private void WriteObjects()
        {
            ObjcDatWriter.Write(objects, outputDirPath);
        }

        private IEnumerable<Corpse> ReadCorpses(XmlReader xml, string basePath)
        {
            var fileName = Path.GetFileName(objectLoadContext.FilePath);

            var isOldFormat = xml.IsStartElement("CRSA");
            int readCount = 0, fixedCount = 0, usedCount = 0;

            if (isOldFormat)
            {
                xml.ReadStartElement("CRSA");

                if (xml.IsStartElement("FixedCount"))
                    fixedCount = xml.ReadElementContentAsInt("FixedCount", "");

                if (xml.IsStartElement("UsedCount"))
                    usedCount = xml.ReadElementContentAsInt("UsedCount", "");

                if (usedCount < fixedCount)
                {
                    error.WriteLine("There are more fixed corpses ({0}) than used corpses ({1}) - assuming fixed = used", fixedCount, usedCount);
                    fixedCount = usedCount;
                }
            }

            xml.ReadStartElement("Corpses");

            while (xml.IsStartElement())
            {
                var corpse = new Corpse();
                corpse.IsFixed = isOldFormat && readCount < fixedCount;
                corpse.IsUsed = !isOldFormat || readCount < usedCount;
                corpse.FileName = fileName;

                if (xml.IsEmptyElement)
                {
                    corpse.IsUsed = false;
                }

                if (!corpse.IsUsed)
                {
                    xml.Skip();
                }
                else if (xml.LocalName == "Corpse" || xml.LocalName == "CRSACorpse")
                {
                    xml.ReadStartElement();

                    if (!isOldFormat)
                    {
                        if (xml.IsStartElement("CanDelete"))
                        {
                            corpse.IsFixed = false;
                            xml.Skip();
                        }
                        else
                        {
                            corpse.IsFixed = true;
                        }
                    }

                    if (xml.IsStartElement("Class") || xml.IsStartElement("CharacterClass"))
                        corpse.CharacterClass = xml.ReadElementContentAsString();

                    if (string.IsNullOrEmpty(corpse.CharacterClass))
                    {
                        corpse.IsUsed = false;
                        corpse.IsFixed = false;
                    }

                    xml.ReadStartElement("Transforms");

                    for (int j = 0; j < corpse.Transforms.Length; j++)
                    {
                        if (xml.IsStartElement("Matrix4x3"))
                            corpse.Transforms[j] = xml.ReadElementContentAsMatrix43("Matrix4x3");
                        else if (xml.IsStartElement("Matrix"))
                            corpse.Transforms[j] = xml.ReadElementContentAsMatrix43("Matrix");
                    }

                    xml.ReadEndElement();

                    if (xml.IsStartElement("BoundingBox"))
                    {
                        xml.ReadStartElement("BoundingBox");
                        corpse.BoundingBox.Min = xml.ReadElementContentAsVector3("Min");
                        corpse.BoundingBox.Max = xml.ReadElementContentAsVector3("Max");
                        xml.ReadEndElement();
                    }
                    else
                    {
                        corpse.BoundingBox.Min = corpse.Transforms[0].Translation;
                        corpse.BoundingBox.Max = corpse.Transforms[0].Translation;
                        corpse.BoundingBox.Inflate(new Vector3(10.0f, 5.0f, 10.0f));
                    }

                    xml.ReadEndElement();
                }
                else
                {
                    var filePath = xml.ReadElementContentAsString("Import", "");
                    filePath = Path.Combine(basePath, filePath);

                    using (var reader = new BinaryReader(filePath))
                    {
                        corpse.FileName = Path.GetFileName(filePath);
                        corpse.IsUsed = true;
                        corpse.IsFixed = true;

                        corpse.CharacterClass = reader.ReadString(128);
                        reader.Skip(4);

                        for (int i = 0; i < corpse.Transforms.Length; i++)
                            corpse.Transforms[i] = reader.ReadMatrix4x3();

                        corpse.BoundingBox = reader.ReadBoundingBox();
                    }
                }

                readCount++;
                yield return corpse;
            }

            if (readCount < usedCount)
            {
                error.WriteLine("{0} corpses were expected but only {1} have been read", usedCount, readCount);
            }

            info.WriteLine("Read {0} corpses", readCount);
        }

        private InstanceFileManager fileManager;

        private InstanceDescriptor FindSharedInstance(TemplateTag tag, string name, ObjectLoadContext loadContext)
        {
            if (!name.EndsWith(".oni", StringComparison.OrdinalIgnoreCase))
                return FindSharedInstance(tag, name);

            string filePath = Path.GetFullPath(Path.Combine(loadContext.BasePath, name));

            if (File.Exists(filePath))
            {
                if (fileManager == null)
                    fileManager = new InstanceFileManager();

                return fileManager.OpenFile(filePath).Descriptors[0];
            }

            filePath = Path.GetFullPath(Path.Combine(sharedPath, name));

            if (File.Exists(filePath))
            {
                var file = sharedManager.OpenFile(filePath);

                return file.Descriptors[0];
            }

            error.WriteLine("Could not find {0}", name);

            return null;
        }
    }
}
