using System;
using System.Collections.Generic;
using System.IO;
using Oni.Metadata;

namespace Oni
{
    internal sealed class InstanceFile
    {
        private readonly InstanceFileManager fileManager;
        private readonly string filePath;
        private InstanceFileHeader header;
        private Dictionary<int, int> rawParts;
        private Dictionary<int, int> sepParts;
        private string rawFilePath;
        private string sepFilePath;
        private List<InstanceDescriptor> descriptors;
        private IList<InstanceDescriptor> readOnlyDescriptors;

        private InstanceFile(InstanceFileManager fileManager, string filePath)
        {
            this.fileManager = fileManager;
            this.filePath = filePath;
        }

        public static InstanceFile Read(InstanceFileManager fileManager, string filePath)
        {
            var file = new InstanceFile(fileManager, filePath);

            using (var reader = new BinaryReader(filePath))
            {
                var header = InstanceFileHeader.Read(reader);
                var descriptors = new List<InstanceDescriptor>(header.InstanceCount);

                file.header = header;
                file.descriptors = descriptors;

                for (int i = 0; i < file.header.InstanceCount; i++)
                    descriptors.Add(InstanceDescriptor.Read(file, reader, i));

                var names = ReadNames(header, reader);

                for (int i = 0; i < file.header.InstanceCount; i++)
                    descriptors[i].ReadName(names);
            }

            //
            // Force AGDB instances to have a name so they get exported into separate .oni files
            // instead of being exported inside an AKEV.oni file. This code assumes that there
            // is only 1 AGDB/AKEV file per .dat file (or none at all).
            //

            foreach (var descriptor in file.descriptors)
            {
                if (descriptor.Template.Tag != TemplateTag.AGDB)
                    continue;

                var akevDescriptors = file.GetNamedDescriptors(TemplateTag.AKEV);

                if (akevDescriptors.Count == 1)
                {
                    string agdbName = "AGDB" + akevDescriptors[0].Name;
                    descriptor.SetName(agdbName);
                    break;
                }
            }

            return file;
        }

        private static Dictionary<int, string> ReadNames(InstanceFileHeader header, BinaryReader reader)
        {
            reader.Position = header.NameTableOffset;

            var nameTable = reader.ReadBytes(header.NameTableSize);
            int nameOffset = 0;

            var names = new Dictionary<int, string>(header.NameCount);
            var buffer = new char[64];

            while (nameOffset < nameTable.Length)
            {
                int i = 0;

                while (true)
                {
                    byte c = nameTable[nameOffset + i];

                    if (c == 0)
                        break;

                    buffer[i++] = (char)c;
                }

                names.Add(nameOffset, new string(buffer, 0, i));
                nameOffset += i + 1;
            }

            return names;
        }

        public InstanceFileManager FileManager
        {
            get { return fileManager; }
        }

        public string FilePath
        {
            get { return filePath; }
        }

        public InstanceFileHeader Header
        {
            get { return header; }
        }

        public List<InstanceDescriptor> GetReferencedDescriptors(InstanceDescriptor descriptor)
        {
            var result = new List<InstanceDescriptor>();

            result.Add(descriptor);

            var stack = new Stack<InstanceDescriptor>();
            var seen = new bool[descriptors.Count];

            stack.Push(descriptor);
            seen[descriptor.Index] = true;

            using (var reader = new BinaryReader(filePath))
            {
                var linkVisitor = new LinkVisitor(reader);

                while (stack.Count > 0)
                {
                    descriptor = stack.Pop();
                    reader.Position = descriptor.DataOffset;
                    linkVisitor.Links.Clear();

                    descriptor.Template.Type.Accept(linkVisitor);

                    foreach (int id in linkVisitor.Links)
                    {
                        if (!seen[id >> 8])
                        {
                            var referencedDescriptor = GetDescriptor(id);

                            if (!referencedDescriptor.IsPlaceholder && !referencedDescriptor.HasName)
                                stack.Push(referencedDescriptor);

                            result.Add(referencedDescriptor);
                            seen[referencedDescriptor.Index] = true;
                        }
                    }
                }
            }

            return result;
        }

        public int GetRawPartSize(int offset)
        {
            EnsureRawAndSepParts();
            return rawParts[offset];
        }

        public int GetSepPartSize(int offset)
        {
            EnsureRawAndSepParts();
            return sepParts[offset];
        }

        public string RawFilePath
        {
            get
            {
                if (rawFilePath == null)
                {
                    if (header.Version == InstanceFileHeader.Version31)
                        rawFilePath = Path.ChangeExtension(filePath, ".raw");
                    else
                        rawFilePath = filePath;
                }

                return rawFilePath;
            }
        }

        public string SepFilePath
        {
            get
            {
                if (sepFilePath == null)
                    sepFilePath = Path.ChangeExtension(filePath, ".sep");

                return sepFilePath;
            }
        }

        public BinaryReader GetRawReader(int offset)
        {
            return GetBinaryReader(offset, RawFilePath);
        }

        public BinaryReader GetSepReader(int offset)
        {
            return GetBinaryReader(offset, SepFilePath);
        }

        private void EnsureRawAndSepParts()
        {
            if (rawParts != null)
                return;

            rawParts = new Dictionary<int, int>();
            sepParts = new Dictionary<int, int>();

            InstanceMetadata.GetRawAndSepParts(this, rawParts, sepParts);
        }

        private BinaryReader GetBinaryReader(int offset, string binaryFilePath)
        {
            var reader = new BinaryReader(binaryFilePath);
            reader.Position = offset + header.RawTableOffset;
            return reader;
        }

        public InstanceDescriptor ResolveLink(int id)
        {
            var descriptor = GetDescriptor(id);

            if (descriptor == null || !descriptor.IsPlaceholder)
                return descriptor;

            if (!descriptor.HasName)
                return null;

            var file = fileManager.FindInstance(descriptor.FullName, this);

            if (file == null || file == this)
            {
                Console.Error.WriteLine("Cannot find instance '{0}'", descriptor.FullName);
                return null;
            }

            if (file.header.Version == InstanceFileHeader.Version32)
                return file.GetDescriptor(1);

            foreach (var target in file.descriptors)
            {
                if (target.HasName && target.FullName == descriptor.FullName)
                    return target;
            }

            return null;
        }

        public InstanceDescriptor GetDescriptor(int id)
        {
            if (id == 0)
                return null;

            return descriptors[id >> 8];
        }

        public IList<InstanceDescriptor> Descriptors
        {
            get
            {
                if (readOnlyDescriptors == null)
                    readOnlyDescriptors = descriptors.AsReadOnly();

                return readOnlyDescriptors;
            }
        }

        public List<InstanceDescriptor> GetNamedDescriptors()
        {
            return descriptors.FindAll(x => x.HasName && !x.IsPlaceholder);
        }

        public List<InstanceDescriptor> GetNamedDescriptors(TemplateTag tag)
        {
            return descriptors.FindAll(x => x.Template.Tag == tag && x.HasName && !x.IsPlaceholder);
        }

        public List<InstanceDescriptor> GetPlaceholders()
        {
            return descriptors.FindAll(x => x.HasName && x.IsPlaceholder);
        }
    }
}
