﻿using System;
using System.Collections.Generic;
using System.IO;
using Oni.Imaging;

namespace Oni.Akira
{
    internal class AkiraDatWriter
    {
        #region Private data
        private Importer importer;
        private string name;
        private bool debug;
        private PolygonMesh source;

        private List<DatPolygon> polygons = new List<DatPolygon>();
        private Dictionary<Polygon, DatPolygon> polygonMap = new Dictionary<Polygon, DatPolygon>();

        private UniqueList<Vector3> points = new UniqueList<Vector3>();
        private UniqueList<Vector2> texCoords = new UniqueList<Vector2>();
        private UniqueList<Material> materials = new UniqueList<Material>();
        private UniqueList<Plane> planes = new UniqueList<Plane>();

        private List<DatAlphaBspNode> alphaBspNodes = new List<DatAlphaBspNode>();

        private List<DatRoom> rooms = new List<DatRoom>();
        private Dictionary<Room, DatRoom> roomMap = new Dictionary<Room, DatRoom>();
        private List<DatRoomBspNode> roomBspNodes = new List<DatRoomBspNode>();
        private List<DatRoomSide> roomSides = new List<DatRoomSide>();
        private List<DatRoomAdjacency> roomAdjacencies = new List<DatRoomAdjacency>();

        private DatOctree octree;
        #endregion

        #region private class UniqueList<T>

        private class UniqueList<T> : ICollection<T>
        {
            private readonly List<T> list = new List<T>();
            private readonly Dictionary<T, int> indices = new Dictionary<T, int>();

            public int Add(T t)
            {
                int index;

                if (!indices.TryGetValue(t, out index))
                {
                    index = list.Count;
                    indices.Add(t, index);
                    list.Add(t);
                }

                return index;
            }

            #region ICollection<T> Members

            void ICollection<T>.Add(T item) => Add(item);

            public int Count => list.Count;

            void ICollection<T>.Clear()
            {
                list.Clear();
                indices.Clear();
            }

            bool ICollection<T>.Contains(T item) => indices.ContainsKey(item);

            void ICollection<T>.CopyTo(T[] array, int arrayIndex)
            {
                list.CopyTo(array, arrayIndex);
            }

            bool ICollection<T>.IsReadOnly => false;

            bool ICollection<T>.Remove(T item)
            {
                throw new NotImplementedException();
            }

            IEnumerator<T> IEnumerable<T>.GetEnumerator() => list.GetEnumerator();

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => list.GetEnumerator();
            
            #endregion
        }

        #endregion

        #region private class DatObject<T>

        private class DatObject<T>
        {
            private readonly T source;

            public DatObject(T source)
            {
                this.source = source;
            }

            public T Source => source;
        }

        #endregion
        #region private class DatPolygon

        private class DatPolygon : DatObject<Polygon>
        {
            private static readonly Color defaultColor = new Color(207, 207, 207, 255);
            private static readonly Color[] defaultColors = new[] { defaultColor, defaultColor, defaultColor, defaultColor };
            private GunkFlags flags;
            private readonly int index;
            public readonly int[] PointIndices = new int[4];
            public readonly int[] TexCoordIndices = new int[4];
            public readonly Color[] Colors = new Color[4];
            private readonly int objectId;
            private readonly int scriptId;
            private int materialIndex;
            private int planeIndex;

            public DatPolygon(Polygon source, int index, UniqueList<Vector3> points, UniqueList<Vector2> texCoords)
                : base(source)
            {
                this.index = index;
                this.scriptId = source.ScriptId;

                objectId = source.ObjectType << 24 | (source.ObjectId & 0xffffff);
                flags = source.Flags;

                if (source.VertexCount == 3)
                {
                    flags |= GunkFlags.Triangle;

                    Array.Copy(source.PointIndices, PointIndices, 3);
                    PointIndices[3] = PointIndices[2];

                    Array.Copy(source.TexCoordIndices, TexCoordIndices, 3);
                    TexCoordIndices[3] = TexCoordIndices[2];

                    if (source.Colors == null)
                    {
                        Colors = defaultColors;
                    }
                    else
                    {
                        Array.Copy(source.Colors, Colors, 3);
                        Colors[3] = Colors[2];
                    }
                }
                else
                {
                    Array.Copy(source.PointIndices, PointIndices, 4);
                    Array.Copy(source.TexCoordIndices, TexCoordIndices, 4);

                    if (source.Colors != null)
                        Colors = source.Colors;
                    else
                        Colors = defaultColors;
                }

                for (int i = 0; i < 4; i++)
                {
                    PointIndices[i] = points.Add(source.Mesh.Points[PointIndices[i]]);
                    TexCoordIndices[i] = texCoords.Add(source.Mesh.TexCoords[TexCoordIndices[i]]);
                }
            }

            public int Index => index;
            public int ObjectId => objectId;
            public int ScriptId => scriptId;

            public GunkFlags Flags
            {
                get { return flags; }
                set { flags = value; }
            }

            public int MaterialIndex
            {
                get { return materialIndex; }
                set { materialIndex = value; }
            }

            public int PlaneIndex
            {
                get { return planeIndex; }
                set { planeIndex = value; }
            }
        }

        #endregion
        #region private class DatBspNode

        private class DatBspNode<T> : DatObject<T>
            where T : BspNode<T>
        {
            public int PlaneIndex;
            public int FrontChildIndex = -1;
            public int BackChildIndex = -1;

            public DatBspNode(T source)
                : base(source)
            {
            }
        }

        #endregion
        #region private class DatAlphaBspNode

        private class DatAlphaBspNode : DatBspNode<AlphaBspNode>
        {
            public readonly int PolygonIndex;

            public DatAlphaBspNode(AlphaBspNode source, int polygonIndex)
                : base(source)
            {
                PolygonIndex = polygonIndex;
            }
        }

        #endregion
        #region private class DatRoom

        private class DatRoom : DatObject<Room>
        {
            private readonly int index;
            private readonly RoomFlags flags;

            public int BspTreeIndex;
            public int SideListStart;
            public int SideListEnd;
            public int ChildIndex = -1;
            public int SiblingIndex = -1;
            public byte[] CompressedGridData;
            public byte[] DebugData;

            public DatRoom(Room source, int index)
                : base(source)
            {
                this.index = index;
                this.flags = RoomFlags.Room;

                if (source.FloorPlane.Normal.Y < 0.999f)
                    this.flags |= RoomFlags.Stairs;
            }

            public int Index => index;
            public int Flags => (int)flags;
        }

        #endregion
        #region private class DatRoomBspNode

        private class DatRoomBspNode : DatBspNode<RoomBspNode>
        {
            public DatRoomBspNode(RoomBspNode source)
                : base(source)
            {
            }
        }

        #endregion
        #region private class DatRoomSide

        private class DatRoomSide
        {
            public int AdjacencyListStart;
            public int AdjacencyListEnd;
        }

        #endregion
        #region private class DatRoomAdjacency

        private class DatRoomAdjacency : DatObject<RoomAdjacency>
        {
            public DatRoomAdjacency(RoomAdjacency source)
                : base(source)
            {
            }

            public int AdjacentRoomIndex;
            public int GhostIndex;
        }

        #endregion
        #region private class DatOctree

        private class DatOctree
        {
            public int[] Nodes;
            public int[] QuadTrees;
            public int[] Adjacency;
            public DatOctreeBoundingBox[] BoundingBoxes;
            public DatOctreePolygonRange[] PolygonLists;
            public DatOctreeBnvRange[] BnvLists;
            public int[] PolygonIndex;
            public int[] BnvIndex;
        }

        #endregion
        #region private struct DatOctreeBoundingBox

        private struct DatOctreeBoundingBox
        {
            private readonly uint value;

            public DatOctreeBoundingBox(BoundingBox bbox)
            {
                int size = (int)(Math.Log(bbox.Max.X - bbox.Min.X, 2)) - 4;
                int maxX = (int)(bbox.Max.X + 4080.0f);
                int maxY = (int)(bbox.Max.Y + 4080.0f);
                int maxZ = (int)(bbox.Max.Z + 4080.0f);

                value = (uint)((maxX << 14) | (maxY << 5) | (maxZ >> 4) | (size << 27));
            }

            public uint PackedValue => value;
        }

        #endregion
        #region private struct DatOctreeBnvRange

        private struct DatOctreeBnvRange
        {
            private const int indexBitOffset = 8;
            private const int lengthBitMask = 255;
            private readonly uint value;

            public DatOctreeBnvRange(int start, int length)
            {
                ValidateRange(start, length);
                value = (uint)((start << indexBitOffset) | (length & lengthBitMask));
            }

            private static void ValidateRange(int start, int length)
            {
                if (start > 16777215)
                    throw new ArgumentException(string.Format("Invalid bnv list start index {0}", start), "start");

                if (length > 255)
                    throw new ArgumentException(string.Format("Invalid bnv list length {0}", length), "length");
            }

            public uint PackedValue => value;
        }

        #endregion
        #region private struct DatOctreePolygonRange

        private struct DatOctreePolygonRange
        {
            private const int indexBitOffset = 12;
            private const int lengthBitMask = 4095;
            private readonly uint value;

            public DatOctreePolygonRange(int start, int length)
            {
                //ValidateRange(start, length);

                if (start > 1048575)
                {
                    start = 1048575;
                    length = 0;
                }

                value = (uint)((start << indexBitOffset) | (length & lengthBitMask));
            }

            private static void ValidateRange(int start, int length)
            {
                if (start > 1048575)
                    throw new ArgumentException(string.Format("Invalid quad list start index {0}", start), "start");

                if (length > 4095)
                    throw new ArgumentException(string.Format("Invalid quad list length {0}", length), "length");
            }

            public uint PackedValue => value;
        }

        #endregion

        public static void Write(PolygonMesh mesh, Importer importer, string name, bool debug)
        {
            var writer = new AkiraDatWriter {
                name = name,
                importer = importer,
                source = mesh,
                debug = debug
            };

            writer.Write();
        }

        private void Write()
        {
            Console.Error.WriteLine("Environment bounding box is {0}", source.GetBoundingBox());

            RoomBuilder.BuildRooms(source);

            ConvertPolygons(source.Polygons);
            ConvertPolygons(source.Doors);
            ConvertPolygons(source.Ghosts);
            ConvertAlphaBspTree(AlphaBspBuilder.Build(source, debug));
            ConvertRooms();
            ConvertOctree();

            WriteAKEV();

            //foreach (Material material in materials)
            //{
            //    if (File.Exists(material.ImageFilePath))
            //        importer.AddDependency(material.ImageFilePath, TemplateTag.TXMP);
            //}
        }

        private void ConvertPolygons(List<Polygon> sourcePolygons)
        {
            foreach (var polygon in sourcePolygons)
            {
                if (polygon.VertexCount > 4)
                {
                    Console.Error.WriteLine("Geometry '{0}' has a {1}-gon, ignoring.", polygon.ObjectName, polygon.VertexCount);
                    continue;
                }

                if (polygon.TexCoordIndices == null)
                {
                    Console.Error.WriteLine("Geometry '{0}' does not contain texture coordinates, ignoring.", polygon.ObjectName);
                    continue;
                }

                var datPolygon = new DatPolygon(polygon, polygons.Count, points, texCoords) {
                    PlaneIndex = planes.Add(polygon.Plane),
                    MaterialIndex = materials.Add(polygon.Material)
                };

                polygons.Add(datPolygon);
                polygonMap.Add(polygon, datPolygon);
            }

            //var noneMaterial = source.Materials.GetMaterial("NONE");
            //int noneMaterialIndex = materials.Add(noneMaterial);

            //foreach (var polygon in polygons)
            //{
            //    var material = polygon.Source.Material;

            //if (material.IsMarker)
            //{
            //    polygon.MaterialIndex = noneMaterialIndex;

            //    if (material != source.Materials.Markers.Blackness && (material.Flags & GunkFlags.Invisible) == 0)
            //        polygon.Flags |= GunkFlags.Transparent;
            //}
            //else
            //{
            //    polygon.MaterialIndex = materials.Add(material);
            //}
            //}
        }

        private int ConvertAlphaBspTree(AlphaBspNode source)
        {
            if (source == null)
                return -1;

            int index = alphaBspNodes.Count;

            var node = new DatAlphaBspNode(source, polygonMap[source.Polygon].Index) {
                PlaneIndex = planes.Add(source.Plane)
            };

            alphaBspNodes.Add(node);

            if (source.FrontChild != null)
                node.FrontChildIndex = ConvertAlphaBspTree((AlphaBspNode)source.FrontChild);

            if (source.BackChild != null)
                node.BackChildIndex = ConvertAlphaBspTree((AlphaBspNode)source.BackChild);

            return index;
        }

        private void ConvertRooms()
        {
            foreach (var room in source.Rooms)
            {
                var datRoom = new DatRoom(room, rooms.Count) {
                    BspTreeIndex = ConvertRoomBspTree(room.BspTree),
                    CompressedGridData = room.Grid.Compress(),
                    DebugData = room.Grid.DebugData,
                    SideListStart = roomSides.Count
                };

                if (room.Ajacencies.Count > 0)
                {
                    var datSide = new DatRoomSide {
                        AdjacencyListStart = roomAdjacencies.Count
                    };

                    foreach (var adjacency in room.Ajacencies)
                    {
                        roomAdjacencies.Add(new DatRoomAdjacency(adjacency) {
                            AdjacentRoomIndex = source.Rooms.IndexOf(adjacency.AdjacentRoom),
                            GhostIndex = polygonMap[adjacency.Ghost].Index
                        });
                    }

                    datSide.AdjacencyListEnd = roomAdjacencies.Count;
                    roomSides.Add(datSide);
                }

                datRoom.SideListEnd = roomSides.Count;

                rooms.Add(datRoom);
                roomMap.Add(room, datRoom);
            }
        }

        private int ConvertRoomBspTree(RoomBspNode node)
        {
            int index = roomBspNodes.Count;

            var datNode = new DatRoomBspNode(node) {
                PlaneIndex = planes.Add(node.Plane)
            };

            roomBspNodes.Add(datNode);

            if (node.FrontChild != null)
                datNode.FrontChildIndex = ConvertRoomBspTree(node.FrontChild);

            if (node.BackChild != null)
                datNode.BackChildIndex = ConvertRoomBspTree(node.BackChild);

            return index;
        }

        private void ConvertOctree()
        {
            Console.Error.WriteLine("Building octtree for {0} polygons...", source.Polygons.Count);

            var root = OctreeBuilder.Build(source, debug);

            var nodeList = new List<OctreeNode>();
            var leafList = new List<OctreeNode>();
            int quadListLength = 0;
            int roomListLength = 0;

            //
            // Assign indices to nodes/leafs and compute the length of the quad and room indexes.
            //

            root.DfsTraversal(node => {
                if (node.IsLeaf)
                {
                    node.Index = leafList.Count;
                    leafList.Add(node);
                    quadListLength += node.Polygons.Count;
                    roomListLength += node.Rooms.Count;
                }
                else
                {
                    node.Index = nodeList.Count;
                    nodeList.Add(node);
                }
            });

            //
            // Create the octtree data structure that will be written to the file.
            //

            octree = new DatOctree {
                Nodes = new int[nodeList.Count * OctreeNode.ChildCount],
                Adjacency = new int[leafList.Count * OctreeNode.FaceCount],
                BoundingBoxes = new DatOctreeBoundingBox[leafList.Count],
                PolygonIndex = new int[quadListLength],
                PolygonLists = new DatOctreePolygonRange[leafList.Count],
                BnvLists = new DatOctreeBnvRange[leafList.Count],
                BnvIndex = new int[roomListLength]
            };

            Console.WriteLine("Octtree: {0} interior nodes, {1} leafs", nodeList.Count, leafList.Count);

            //
            // Populate the node array.
            // The octree builder stores child nodes in a different order than Oni (by mistake)
            //

            var interiorNodeIndexRemap = new int[] { 0, 4, 2, 6, 1, 5, 3, 7 };
            var datOcttree = octree;

            foreach (var node in nodeList)
            {
                int i = node.Index * OctreeNode.ChildCount;

                for (int j = 0; j < OctreeNode.ChildCount; j++)
                {
                    var child = node.Children[j];
                    int k = interiorNodeIndexRemap[j];

                    if (child.IsLeaf)
                        datOcttree.Nodes[i + k] = child.Index | int.MinValue;
                    else
                        datOcttree.Nodes[i + k] = child.Index;
                }
            }

            //
            // Generate the data needed by the leafs: bounding box, quad range and room range.
            //

            int quadListIndex = 0;
            int bnvListIndex = 0;

            foreach (var leaf in leafList)
            {
                datOcttree.BoundingBoxes[leaf.Index] = new DatOctreeBoundingBox(leaf.BoundingBox);

                if (leaf.Polygons.Count > 0)
                {
                    datOcttree.PolygonLists[leaf.Index] = new DatOctreePolygonRange(quadListIndex, leaf.Polygons.Count);

                    foreach (var polygon in leaf.Polygons)
                        datOcttree.PolygonIndex[quadListIndex++] = polygonMap[polygon].Index;
                }

                if (leaf.Rooms.Count > 0)
                {
                    datOcttree.BnvLists[leaf.Index] = new DatOctreeBnvRange(bnvListIndex, leaf.Rooms.Count);

                    foreach (var room in leaf.Rooms)
                        datOcttree.BnvIndex[bnvListIndex++] = roomMap[room].Index;
                }
            }

            //
            // Generate the quad trees. Note that the octtree builder doesn't build quad trees because
            // they're only needed when writing the octtree to the file. Currently OniSplit doesn't use
            // the octtree for raycasting.
            //

            var quadTrees = new List<int>();

            foreach (var leaf in leafList)
            {
                leaf.RefineAdjacency();

                foreach (var face in OctreeNode.Face.All)
                {
                    var adjacentLeaf = leaf.Adjacency[face.Index];
                    int index = leaf.Index * OctreeNode.FaceCount + face.Index;

                    if (adjacentLeaf == null)
                    {
                        //
                        // There's no adjacent node or leaf, this should only happen
                        // on the edges of the octtree.
                        //

                        datOcttree.Adjacency[index] = -1;
                    }
                    else if (adjacentLeaf.IsLeaf)
                    {
                        //
                        // The adjacent node is a leaf, there's no need for a quad tree.
                        //

                        datOcttree.Adjacency[index] = adjacentLeaf.Index | int.MinValue;
                    }
                    else
                    {
                        //
                        // The adjacent node has children, a quad tree needs to be built.
                        //

                        int quadTreeBaseIndex = quadTrees.Count / 4;
                        datOcttree.Adjacency[index] = quadTreeBaseIndex;

                        var quadTreeRoot = leaf.BuildFaceQuadTree(face);

                        foreach (var node in quadTreeRoot.GetDfsList())
                        {
                            for (int i = 0; i < 4; i++)
                            {
                                if (node.Nodes[i] != null)
                                    quadTrees.Add(quadTreeBaseIndex + node.Nodes[i].Index);
                                else
                                    quadTrees.Add(node.Leafs[i].Index | int.MinValue);
                            }
                        }
                    }
                }
            }

            datOcttree.QuadTrees = quadTrees.ToArray();
        }

        private void WriteAKEV()
        {
            var akev = importer.CreateInstance(TemplateTag.AKEV, name);
            var pnta = importer.CreateInstance(TemplateTag.PNTA);
            var plea = importer.CreateInstance(TemplateTag.PLEA);
            var txca = importer.CreateInstance(TemplateTag.TXCA);
            var agqg = importer.CreateInstance(TemplateTag.AGQG);
            var agqr = importer.CreateInstance(TemplateTag.AGQR);
            var agqc = importer.CreateInstance(TemplateTag.AGQC);
            var agdb = importer.CreateInstance(TemplateTag.AGDB);
            var txma = importer.CreateInstance(TemplateTag.TXMA);
            var akva = importer.CreateInstance(TemplateTag.AKVA);
            var akba = importer.CreateInstance(TemplateTag.AKBA);
            var idxa1 = importer.CreateInstance(TemplateTag.IDXA);
            var idxa2 = importer.CreateInstance(TemplateTag.IDXA);
            var akbp = importer.CreateInstance(TemplateTag.AKBP);
            var abna = importer.CreateInstance(TemplateTag.ABNA);
            var akot = importer.CreateInstance(TemplateTag.AKOT);
            var akaa = importer.CreateInstance(TemplateTag.AKAA);
            var akda = importer.CreateInstance(TemplateTag.AKDA);

            using (var writer = akev.OpenWrite())
            {
                writer.Write(pnta);
                writer.Write(plea);
                writer.Write(txca);
                writer.Write(agqg);
                writer.Write(agqr);
                writer.Write(agqc);
                writer.Write(agdb);
                writer.Write(txma);
                writer.Write(akva);
                writer.Write(akba);
                writer.Write(idxa1);
                writer.Write(idxa2);
                writer.Write(akbp);
                writer.Write(abna);
                writer.Write(akot);
                writer.Write(akaa);
                writer.Write(akda);
                writer.Write(source.GetBoundingBox());
                writer.Skip(24);
                writer.Write(12.0f);
            }

            pnta.WritePoints(points);
            plea.WritePlanes(planes);
            txca.WriteTexCoords(texCoords);
            WriteAGQG(agqg);
            WriteAGQR(agqr);
            WriteAGQC(agqc);
            WriteTXMA(txma);
            WriteAKVA(akva);
            WriteAKBA(akba);
            WriteAKBP(akbp);
            WriteABNA(abna);
            WriteAKOT(akot);
            WriteAKAA(akaa);
            WriteAKDA(akda);
            WriteScriptIds(idxa1, idxa2);
            WriteAGDB(agdb);
        }

        private void WriteAGQG(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(polygons.Count);

                foreach (var polygon in polygons)
                {
                    writer.Write(polygon.PointIndices);
                    writer.Write(polygon.TexCoordIndices);
                    writer.Write(polygon.Colors);
                    writer.Write((uint)polygon.Flags);
                    writer.Write(polygon.ObjectId);
                }
            }
        }

        private void WriteAGQC(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(polygons.Count);

                foreach (var polygon in polygons)
                {
                    writer.Write(polygon.PlaneIndex);
                    writer.Write(polygon.Source.BoundingBox);
                }
            }
        }

        private void WriteAGQR(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(polygons.Count);

                foreach (var polygon in polygons)
                {
                    writer.Write(polygon.MaterialIndex);
                }
            }
        }

        private void WriteTXMA(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(materials.Count);

                foreach (var material in materials)
                {
                    writer.Write(importer.CreateInstance(TemplateTag.TXMP, material.Name));
                }
            }
        }

        private void WriteABNA(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(alphaBspNodes.Count);

                foreach (var node in alphaBspNodes)
                {
                    writer.Write(node.PolygonIndex);
                    writer.Write(node.PlaneIndex);
                    writer.Write(node.FrontChildIndex);
                    writer.Write(node.BackChildIndex);
                }
            }
        }

        private void WriteAKVA(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(rooms.Count);

                foreach (var room in rooms)
                {
                    writer.Write(room.BspTreeIndex);
                    writer.Write(room.Index);
                    writer.Write(room.SideListStart);
                    writer.Write(room.SideListEnd);
                    writer.Write(room.ChildIndex);
                    writer.Write(room.SiblingIndex);
                    writer.Skip(4);
                    writer.Write(room.Source.Grid.XTiles);
                    writer.Write(room.Source.Grid.ZTiles);
                    writer.Write(importer.WriteRawPart(room.CompressedGridData));
                    writer.Write(room.CompressedGridData.Length);
                    writer.Write(room.Source.Grid.TileSize);
                    writer.Write(room.Source.BoundingBox);
                    writer.WriteInt16(room.Source.Grid.XOrigin);
                    writer.WriteInt16(room.Source.Grid.ZOrigin);
                    writer.Write(room.Index);
                    writer.Skip(4);

                    if (room.DebugData != null)
                    {
                        writer.Write(room.DebugData.Length);
                        writer.Write(importer.WriteRawPart(room.DebugData));
                    }
                    else
                    {
                        writer.Write(0);
                        writer.Write(0);
                    }

                    writer.Write(room.Flags);
                    writer.Write(room.Source.FloorPlane);
                    writer.Write(room.Source.Height);
                }
            }
        }

        private void WriteAKBA(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(roomSides.Count);

                foreach (var side in roomSides)
                {
                    writer.Write(0);
                    writer.Write(side.AdjacencyListStart);
                    writer.Write(side.AdjacencyListEnd);
                    writer.Skip(16);
                }
            }
        }

        private void WriteAKBP(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(22))
            {
                writer.WriteUInt16(roomBspNodes.Count);

                foreach (var node in roomBspNodes)
                {
                    writer.Write(node.PlaneIndex);
                    writer.Write(node.BackChildIndex);
                    writer.Write(node.FrontChildIndex);
                }
            }
        }

        private void WriteAKAA(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(roomAdjacencies.Count);

                foreach (var adjacency in roomAdjacencies)
                {
                    writer.Write(adjacency.AdjacentRoomIndex);
                    writer.Write(adjacency.GhostIndex);
                    writer.Write(0);
                }
            }
        }

        private void WriteAKDA(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(0);
            }
        }

        private void WriteAKOT(ImporterDescriptor akot)
        {
            var otit = importer.CreateInstance(TemplateTag.OTIT);
            var otlf = importer.CreateInstance(TemplateTag.OTLF);
            var qtna = importer.CreateInstance(TemplateTag.QTNA);
            var idxa1 = importer.CreateInstance(TemplateTag.IDXA);
            var idxa2 = importer.CreateInstance(TemplateTag.IDXA);

            using (var writer = akot.OpenWrite())
            {
                writer.Write(otit);
                writer.Write(otlf);
                writer.Write(qtna);
                writer.Write(idxa1);
                writer.Write(idxa2);
            }

            WriteOTIT(otit);
            WriteOTLF(otlf);
            WriteQTNA(qtna);

            idxa1.WriteIndices(octree.PolygonIndex);
            idxa2.WriteIndices(octree.BnvIndex);
        }

        private void WriteOTIT(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(octree.Nodes.Length / OctreeNode.ChildCount);
                writer.Write(octree.Nodes);
            }
        }

        private void WriteOTLF(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(octree.BoundingBoxes.Length);

                for (int i = 0; i < octree.BoundingBoxes.Length; i++)
                {
                    writer.Write(octree.PolygonLists[i].PackedValue);
                    writer.Write(octree.Adjacency, i * OctreeNode.FaceCount, OctreeNode.FaceCount);
                    writer.Write(octree.BoundingBoxes[i].PackedValue);
                    writer.Write(octree.BnvLists[i].PackedValue);
                }
            }
        }

        private void WriteQTNA(ImporterDescriptor descriptor)
        {
            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(octree.QuadTrees.Length / 4);
                writer.Write(octree.QuadTrees);
            }
        }

        private void WriteScriptIds(ImporterDescriptor idxa1, ImporterDescriptor idxa2)
        {
            var scriptIdMap = new List<KeyValuePair<int, int>>(256);

            foreach (var polygon in polygons)
            {
                if (polygon.ScriptId != 0)
                    scriptIdMap.Add(new KeyValuePair<int, int>(polygon.ScriptId, polygon.Index));
            }

            scriptIdMap.Sort((x, y) => x.Key.CompareTo(y.Key));

            var scriptIds = new int[scriptIdMap.Count];
            var polygonIndices = new int[scriptIdMap.Count];

            for (int i = 0; i < scriptIdMap.Count; i++)
            {
                scriptIds[i] = scriptIdMap[i].Key;
                polygonIndices[i] = scriptIdMap[i].Value;
            }

            idxa1.WriteIndices(polygonIndices);
            idxa2.WriteIndices(scriptIds);
        }

        private void WriteAGDB(ImporterDescriptor descriptor)
        {
            if (!debug)
            {
                using (var writer = descriptor.OpenWrite(20))
                {
                    writer.Write(0);
                }

                return;
            }

            var objectNames = new Dictionary<string, int>(polygons.Count, StringComparer.Ordinal);
            var fileNames = new Dictionary<string, int>(polygons.Count, StringComparer.Ordinal);

            using (var writer = descriptor.OpenWrite(20))
            {
                writer.Write(polygons.Count);

                foreach (var polygon in polygons)
                {
                    var objectName = polygon.Source.ObjectName;
                    var fileName = polygon.Source.FileName;

                    if (string.IsNullOrEmpty(objectName))
                        objectName = "(none)";

                    if (string.IsNullOrEmpty(fileName))
                        fileName = "(none)";

                    int objectOffset;
                    int fileOffset;

                    if (!objectNames.TryGetValue(objectName, out objectOffset))
                    {
                        objectOffset = importer.WriteRawPart(objectName);
                        objectNames.Add(objectName, objectOffset);
                    }

                    if (!fileNames.TryGetValue(fileName, out fileOffset))
                    {
                        fileOffset = importer.WriteRawPart(fileName);
                        fileNames.Add(fileName, fileOffset);
                    }

                    writer.Write(objectOffset);
                    writer.Write(fileOffset);
                }
            }
        }
    }
}
