﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Oni
{
    internal sealed class ImporterFile
    {
        private static readonly byte[] txcaPadding = new byte[480];
        private readonly long templateChecksum = InstanceFileHeader.OniPCTemplateChecksum;
        private MemoryStream rawStream;
        private BinaryWriter rawWriter;
        private List<ImporterFileDescriptor> descriptors;
        private int nameOffset;

        #region private class FileHeader

        private class FileHeader
        {
            public const int Size = 64;

            public long TemplateChecksum;
            public int Version;
            public int InstanceCount;
            public int DataTableOffset;
            public int DataTableSize;
            public int NameTableOffset;
            public int NameTableSize;
            public int RawTableOffset;
            public int RawTableSize;

            public void Write(BinaryWriter writer)
            {
                writer.Write(TemplateChecksum);
                writer.Write(Version);
                writer.Write(InstanceFileHeader.Signature);
                writer.Write(InstanceCount);
                writer.Write(0ul);
                writer.Write(DataTableOffset);
                writer.Write(DataTableSize);
                writer.Write(NameTableOffset);
                writer.Write(NameTableSize);
                writer.Write(RawTableOffset);
                writer.Write(RawTableSize);
                writer.Write(0ul);
            }
        }

        #endregion

        public ImporterFile()
        {
        }

        public ImporterFile(long templateChecksum)
        {
            this.templateChecksum = templateChecksum;
        }

        public void BeginImport()
        {
            rawStream = null;
            rawWriter = null;

            descriptors = new List<ImporterFileDescriptor>();
            nameOffset = 0;
        }

        public BinaryWriter RawWriter
        {
            get
            {
                if (rawWriter == null)
                {
                    rawStream = new MemoryStream();
                    rawWriter = new BinaryWriter(rawStream);
                    rawWriter.Write(new byte[32]);
                }

                return rawWriter;
            }
        }

        public ImporterDescriptor CreateInstance(TemplateTag tag, string name = null)
        {
            var descriptor = new ImporterFileDescriptor(this, tag, descriptors.Count, MakeInstanceName(tag, name));

            descriptors.Add(descriptor);

            return descriptor;
        }

        public int WriteRawPart(byte[] data)
        {
            int offset = RawWriter.Align32();
            RawWriter.Write(data);
            return offset;
        }

        public int WriteRawPart(string text)
        {
            return WriteRawPart(Encoding.UTF8.GetBytes(text));
        }

        private sealed class ImporterFileDescriptor : ImporterDescriptor
        {
            public const int Size = 20;

            private int nameOffset;
            private int dataOffset;
            private byte[] data;

            public ImporterFileDescriptor(ImporterFile file, TemplateTag tag, int index, string name)
                : base(file, tag, index, name)
            {
                if (!string.IsNullOrEmpty(name))
                {
                    nameOffset = file.nameOffset;
                    file.nameOffset += name.Length + 1;
                }
            }

            public int NameOffset
            {
                get { return nameOffset; }
                set { nameOffset = value; }
            }

            public int DataOffset
            {
                get { return dataOffset; }
                set { dataOffset = value; }
            }

            public int DataSize
            {
                get
                {
                    if (data == null)
                        return 0;

                    return data.Length + 8;
                }
            }

            public byte[] Data
            {
                get { return data; }
            }

            public override BinaryWriter OpenWrite()
            {
                if (data != null)
                    throw new InvalidOperationException("Descriptor has already been written to");

                return new InstanceWriter(this);
            }

            public override BinaryWriter OpenWrite(int offset)
            {
                if (data != null)
                    throw new InvalidOperationException("Descriptor has already been written to");

                var writer = new InstanceWriter(this);
                writer.Skip(offset);
                return writer;
            }

            public void Close(byte[] data)
            {
                this.data = data;
            }
        }

        private class InstanceWriter : BinaryWriter
        {
            private readonly ImporterFileDescriptor descriptor;

            public InstanceWriter(ImporterFileDescriptor descriptor)
                : base(new MemoryStream())
            {
                this.descriptor = descriptor;
            }

            protected override void Dispose(bool disposing)
            {
                var stream = (MemoryStream)BaseStream;

                if (descriptor.Tag == TemplateTag.TXCA)
                    stream.Write(txcaPadding, 0, txcaPadding.Length);
                else if (stream.Position > stream.Length)
                    stream.SetLength(stream.Position);

                descriptor.Close(stream.ToArray());

                base.Dispose(disposing);
            }
        }

        public void Write(string outputDirPath)
        {
            var filePath = Path.Combine(outputDirPath, Importer.EncodeFileName(descriptors[0].Name) + ".oni");

            Directory.CreateDirectory(outputDirPath);

            int nameTableOffset = Utils.Align32(FileHeader.Size + ImporterFileDescriptor.Size * descriptors.Count);
            int nameTableSize = nameOffset;
            int dataTableOffset = Utils.Align32(nameTableOffset + nameOffset);
            int dataTableSize = 0;

            foreach (var descriptor in descriptors.Where(d => d.Data != null))
            {
                descriptor.DataOffset = dataTableSize + 8;

                dataTableSize += Utils.Align32(descriptor.DataSize);
            }

            var header = new FileHeader
            {
                TemplateChecksum = templateChecksum,
                Version = InstanceFileHeader.Version32,
                InstanceCount = descriptors.Count,
                DataTableOffset = dataTableOffset,
                DataTableSize = dataTableSize,
                NameTableOffset = nameTableOffset,
                NameTableSize = nameTableSize
            };

            using (var stream = File.Create(filePath))
            using (var writer = new BinaryWriter(stream))
            {
                bool hasRawParts = (rawStream != null && rawStream.Length > 32);

                if (hasRawParts)
                {
                    header.RawTableOffset = Utils.Align32(header.DataTableOffset + header.DataTableSize);
                    header.RawTableSize = (int)rawStream.Length;
                }

                header.Write(writer);

                foreach (var descriptor in descriptors)
                {
                    WriteDescriptor(writer, descriptor);
                }

                writer.Position = header.NameTableOffset;

                foreach (var entry in descriptors)
                {
                    if (entry.Name != null)
                        writer.Write(entry.Name, entry.Name.Length + 1);
                }

                writer.Position = header.DataTableOffset;

                foreach (var descriptor in descriptors.Where(d => d.Data != null))
                {
                    writer.Align32();
                    writer.WriteInstanceId(descriptor.Index);
                    writer.Write(0);
                    writer.Write(descriptor.Data);
                }

                if (hasRawParts)
                {
                    writer.Position = header.RawTableOffset;
                    rawStream.WriteTo(stream);
                }
            }
        }

        private void WriteDescriptor(BinaryWriter writer, ImporterFileDescriptor descriptor)
        {
            var flags = InstanceDescriptorFlags.None;

            if (descriptor.Name == null)
                flags |= InstanceDescriptorFlags.Private;

            if (descriptor.Data == null)
                flags |= InstanceDescriptorFlags.Placeholder;

            if (descriptor.Name == null && descriptor.Data == null)
                throw new InvalidOperationException("Link descriptors must have names");

            writer.Write((int)descriptor.Tag);
            writer.Write(descriptor.DataOffset);
            writer.Write(descriptor.NameOffset);
            writer.Write(descriptor.DataSize);
            writer.Write((int)flags);
        }

        private static string MakeInstanceName(TemplateTag tag, string name)
        {
            if (string.IsNullOrEmpty(name))
                return null;

            var tagName = tag.ToString();

            if (!name.StartsWith(tagName, StringComparison.Ordinal))
                name = tagName + name;

            return name;
        }
    }
}
