﻿using System;
using System.Collections.Generic;

namespace Oni.Level
{
    internal class LevelDatWriter
    {
        private readonly Importer importer;
        private readonly DatLevel level;

        public class DatLevel
        {
            public string name;
            public string skyName;
            public readonly List<ObjectSetup> physics = new List<ObjectSetup>();
            public readonly List<Physics.ObjectParticle> particles = new List<Physics.ObjectParticle>();
            public readonly List<ScriptCharacter> characters = new List<ScriptCharacter>();
            public readonly List<Corpse> corpses = new List<Corpse>();
            public Akira.PolygonMesh model;
        }

        private LevelDatWriter(Importer importer, DatLevel level)
        {
            this.importer = importer;
            this.level = level;
        }

        public static void Write(Importer importer, DatLevel level)
        {
            var writer = new LevelDatWriter(importer, level);
            writer.WriteONLV();
        }

        private void WriteONLV()
        {
            var onlv = importer.CreateInstance(TemplateTag.ONLV, level.name);
            var oboa = importer.CreateInstance(TemplateTag.OBOA);
            var aisa = importer.CreateInstance(TemplateTag.AISA);
            var onoa = importer.CreateInstance(TemplateTag.ONOA);
            var envp = importer.CreateInstance(TemplateTag.ENVP);
            var crsa = importer.CreateInstance(TemplateTag.CRSA);
            var onsk = importer.CreateInstance(TemplateTag.ONSK, level.skyName);
            var akev = importer.CreateInstance(TemplateTag.AKEV, level.name);

            using (var writer = onlv.OpenWrite())
            {
                writer.Write(level.name, 64);
                writer.Write(akev);
                writer.Write(oboa);
                writer.Write(0);
                writer.Write(0);
                writer.Write(0);
                writer.Write(onsk);
                writer.Write(0.0f);
                writer.Write(aisa);
                writer.Write(0);
                writer.Write(0);
                writer.Write(0);
                writer.Write(onoa);
                writer.Write(envp);
                writer.Skip(644);
                writer.Write(crsa);
            }

            WriteOBOA(oboa);
            WriteAISA(aisa);
            WriteONOA(onoa);
            WriteENVP(envp, level.particles);
            WriteCRSA(crsa, level.corpses);
        }

        private void WriteOBOA(ImporterDescriptor oboa)
        {
            var objects = level.physics;
            var m3ga = new ImporterDescriptor[objects.Count];
            var oban = new ImporterDescriptor[objects.Count];
            var envp = new ImporterDescriptor[objects.Count];

            for (int i = 0; i < objects.Count; i++)
            {
                var obj = objects[i];

                var m3gms = new List<ImporterDescriptor>();

                foreach (var geom in obj.Geometries)
                {
                    if (geom is string)
                        m3gms.Add(importer.CreateInstance(TemplateTag.M3GM, (string)geom));
                    else
                        m3gms.Add(Motoko.GeometryDatWriter.Write((Motoko.Geometry)geom, importer.ImporterFile));
                }

                m3ga[i] = importer.CreateInstance(TemplateTag.M3GA);

                WriteM3GA(m3ga[i], m3gms);

                if (obj.Animation != null)
                    oban[i] = importer.CreateInstance(TemplateTag.OBAN, obj.Animation.Name);

                if (obj.Particles.Count > 0)
                {
                    envp[i] = importer.CreateInstance(TemplateTag.ENVP);
                    WriteENVP(envp[i], obj.Particles);
                }
            }

            int unused = 32;

            using (var writer = oboa.OpenWrite(22))
            {
                writer.WriteUInt16(objects.Count + unused);

                for (int i = 0; i != objects.Count; i++)
                {
                    var obj = objects[i];

                    writer.Write(m3ga[i]);
                    writer.Write(oban[i]);
                    writer.Write(envp[i]);
                    writer.Write((uint)(obj.Flags | Physics.ObjectSetupFlags.InUse));
                    //writer.Write(obj.DoorGunkIndex);
                    writer.Write(0);
                    writer.Write(obj.DoorScriptId);
                    writer.Write((uint)obj.PhysicsType);
                    writer.Write(obj.ScriptId);
                    writer.Write(obj.Position);
                    writer.Write(obj.Orientation);
                    writer.Write(obj.Scale);
                    writer.WriteMatrix4x3(obj.Origin);
                    writer.Write(obj.Name, 64);
                    writer.Write(obj.FileName, 64);
                }

                writer.Skip(unused * 240);
            }
        }

        private void WriteM3GA(ImporterDescriptor m3ga, ICollection<ImporterDescriptor> geometries)
        {
            using (var writer = m3ga.OpenWrite(20))
            {
                writer.Write(geometries.Count);
                writer.Write(geometries);
            }
        }

        private void WriteAISA(ImporterDescriptor aisa)
        {
            var characterClasses = new Dictionary<string, ImporterDescriptor>(StringComparer.Ordinal);
            var weaponClasses = new Dictionary<string, ImporterDescriptor>(StringComparer.Ordinal);

            foreach (var chr in level.characters)
            {
                if (!characterClasses.ContainsKey(chr.className))
                    characterClasses.Add(chr.className, importer.CreateInstance(TemplateTag.ONCC, chr.className));

                if (!string.IsNullOrEmpty(chr.weaponClassName) && !weaponClasses.ContainsKey(chr.weaponClassName))
                    weaponClasses.Add(chr.weaponClassName, importer.CreateInstance(TemplateTag.ONWC, chr.weaponClassName));
            }

            using (var writer = aisa.OpenWrite(22))
            {
                writer.WriteUInt16(level.characters.Count);

                foreach (var chr in level.characters)
                {
                    ImporterDescriptor characterClass, weaponClass;

                    characterClasses.TryGetValue(chr.className, out characterClass);

                    if (!string.IsNullOrEmpty(chr.weaponClassName))
                        weaponClasses.TryGetValue(chr.weaponClassName, out weaponClass);
                    else
                        weaponClass = null;

                    writer.Write(chr.name, 32);
                    writer.WriteInt16(chr.scriptId);
                    writer.WriteInt16(chr.flagId);
                    writer.WriteUInt16((ushort)chr.flags);
                    writer.WriteUInt16((ushort)chr.team);
                    writer.Write(characterClass);
                    writer.Skip(36);
                    writer.Write(chr.onSpawn, 32);
                    writer.Write(chr.onDeath, 32);
                    writer.Write(chr.onSeenEnemy, 32);
                    writer.Write(chr.onAlarmed, 32);
                    writer.Write(chr.onHurt, 32);
                    writer.Write(chr.onDefeated, 32);
                    writer.Write(chr.onOutOfAmmo, 32);
                    writer.Write(chr.onNoPath, 32);
                    writer.Write(weaponClass);
                    writer.WriteInt16(chr.ammo);
                    writer.Skip(10);
                }
            }
        }

        private void WriteONOA(ImporterDescriptor onoa)
        {
            var map = new Dictionary<int, List<int>>();
            int pi = 0;

            foreach (var poly in level.model.Polygons)
            {
                if (poly.ObjectId > 0)
                {
                    List<int> indices;

                    int objectId = poly.ObjectType << 24 | poly.ObjectId;

                    if (!map.TryGetValue(objectId, out indices))
                    {
                        indices = new List<int>();
                        map[objectId] = indices;
                    }

                    indices.Add(pi);
                }

                pi++;
            }

            var elt = new List<KeyValuePair<int, ImporterDescriptor>>();

            foreach (var pair in map)
            {
                var idxa = importer.CreateInstance(TemplateTag.IDXA);

                elt.Add(new KeyValuePair<int, ImporterDescriptor>(pair.Key, idxa));

                using (var idxaWriter = idxa.OpenWrite(20))
                {
                    idxaWriter.Write(pair.Value.Count);
                    idxaWriter.Write(pair.Value.ToArray());
                }
            }

            using (var writer = onoa.OpenWrite(20))
            {
                writer.Write(elt.Count);

                foreach (var e in elt)
                {
                    writer.Write(e.Key);
                    writer.Write(e.Value);
                }
            }
        }

        private void WriteENVP(ImporterDescriptor envp, List<Physics.ObjectParticle> particles)
        {
            using (var writer = envp.OpenWrite(22))
            {
                writer.WriteUInt16(particles.Count);

                foreach (var particle in particles)
                {
                    writer.Write(particle.ParticleClass, 64);
                    writer.Write(particle.Tag, 48);
                    writer.WriteMatrix4x3(particle.Matrix);
                    writer.Write(particle.DecalScale);
                    writer.Write((ushort)particle.Flags);
                    writer.Skip(38);
                }
            }
        }

        private void WriteCRSA(ImporterDescriptor crsa, List<Corpse> corpses)
        {
            //
            // Ensure that there are at least 20 corpses
            //

            while (corpses.Count < 20)
                corpses.Add(new Corpse());

            int fixedCount = corpses.Count(c => c.IsFixed);
            int usedCount = corpses.Count(c => c.IsUsed);

            //
            // Ensure that there are at least 5 unused corpses 
            // so new corpses can be created at runtime
            //

            while (corpses.Count - usedCount < 5)
                corpses.Add(new Corpse());

            corpses.Sort((x, y) => x.Order.CompareTo(y.Order));

            var onccDesriptors = new Dictionary<string, ImporterDescriptor>();

            using (var writer = crsa.OpenWrite(12))
            {
                writer.Write(fixedCount);
                writer.Write(usedCount);
                writer.Write(corpses.Count);

                foreach (var corpse in corpses)
                {
                    writer.Write(corpse.FileName ?? "", 32);
                    writer.Skip(128);

                    if (corpse.IsUsed)
                    {
                        ImporterDescriptor oncc = null;

                        if (!string.IsNullOrEmpty(corpse.CharacterClass))
                        {
                            if (!onccDesriptors.TryGetValue(corpse.CharacterClass, out oncc))
                            {
                                oncc = importer.CreateInstance(TemplateTag.ONCC, corpse.CharacterClass);
                                onccDesriptors.Add(corpse.CharacterClass, oncc);
                            }
                        }

                        writer.Write(oncc);

                        foreach (var transform in corpse.Transforms)
                            writer.WriteMatrix4x3(transform);

                        writer.Write(corpse.BoundingBox);
                    }
                    else
                    {
                        writer.Skip(940);
                    }
                }
            }
        }
    }
}
