﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace FSO.Files.FAR3
{
    /// <summary>
    /// Represents a single FAR3 archive.
    /// </summary>
    public class FAR3Archive : IDisposable
    {
        private BinaryReader m_Reader;
        public static bool isReadingSomething = false;

        private string m_ArchivePath;
        private Dictionary<string, Far3Entry> m_Entries = new Dictionary<string, Far3Entry>();
        private List<Far3Entry> m_EntriesList = new List<Far3Entry>();
        private Dictionary<uint, Far3Entry> m_EntryByID = new Dictionary<uint, Far3Entry>();
        private uint m_ManifestOffset;

        /// <summary>
        /// Creates a new FAR3Archive instance from a path.
        /// </summary>
        /// <param name="Path">The path to the archive.</param>
        public FAR3Archive(string Path)
        {
            m_ArchivePath = Path;

            if (isReadingSomething == false)
            {
                isReadingSomething = true;

                try
                {
                    m_Reader = new BinaryReader(File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read));
                }
                catch (Exception ex)
                {
                    throw new FAR3Exception("Could not open the specified archive - " + Path + "! (FAR3Archive())");
                }

                string Header = Encoding.ASCII.GetString(m_Reader.ReadBytes(8));
                uint Version = m_Reader.ReadUInt32();

                if ((Header != "FAR!byAZ") || (Version != 3))
                {
                    throw new FAR3Exception("Archive wasn't a valid FAR V.3 archive! (FAR3Archive())");
                }

                uint ManifestOffset = m_Reader.ReadUInt32();
                m_ManifestOffset = ManifestOffset;

                m_Reader.BaseStream.Seek(ManifestOffset, SeekOrigin.Begin);

                uint NumFiles = m_Reader.ReadUInt32();

                for (int i = 0; i < NumFiles; i++)
                {
                    Far3Entry Entry = new Far3Entry();
                    Entry.DecompressedFileSize = m_Reader.ReadUInt32();
                    byte dummy0 = m_Reader.ReadByte();
                    byte dummy1 = m_Reader.ReadByte();
                    byte dummy2 = m_Reader.ReadByte();
                    Entry.CompressedFileSize = (uint)((dummy0 << 0) | (dummy1 << 8) | (dummy2) << 16);
                    Entry.DataType = m_Reader.ReadByte();
                    Entry.DataOffset = m_Reader.ReadUInt32();
                    //Entry.HasFilename = m_Reader.ReadUInt16();
                    Entry.IsCompressed = m_Reader.ReadByte();
                    Entry.AccessNumber = m_Reader.ReadByte();
                    Entry.FilenameLength = m_Reader.ReadUInt16();
                    Entry.TypeID = m_Reader.ReadUInt32();
                    Entry.FileID = m_Reader.ReadUInt32();
                    Entry.Filename = Encoding.ASCII.GetString(m_Reader.ReadBytes(Entry.FilenameLength));

                    if (!m_Entries.ContainsKey(Entry.Filename))
                        m_Entries.Add(Entry.Filename, Entry);
                    m_EntriesList.Add(Entry);

                    m_EntryByID.Add(Entry.FileID, Entry); //isn't this a bad idea? i have a feeling this is a bad idea...
                }

                //Keep the stream open, it helps peformance.
                //m_Reader.Close();
                isReadingSomething = false;
            }
        }

        /// <summary>
        /// Gets an entry's data from a Far3Entry instance.
        /// </summary>
        /// <param name="Entry">The Far3Entry instance.</param>
        /// <returns>The entry's data.</returns>
        public byte[] GetEntry(Far3Entry Entry)
        {
            lock (m_Reader)
            {
                m_Reader.BaseStream.Seek((long)Entry.DataOffset, SeekOrigin.Begin);

                isReadingSomething = true;

                if (Entry.IsCompressed == 0x01)
                {
                    m_Reader.BaseStream.Seek(9, SeekOrigin.Current);
                    uint Filesize = m_Reader.ReadUInt32();
                    ushort CompressionID = m_Reader.ReadUInt16();

                    if (CompressionID == 0xFB10)
                    {
                        byte dummy0 = m_Reader.ReadByte();
                        byte dummy1 = m_Reader.ReadByte();
                        byte dummy2 = m_Reader.ReadByte();
                        uint DecompressedSize = (uint)((dummy0 << 0x10) | (dummy1 << 0x08) | +dummy2);

                        Decompresser Dec = new Decompresser();
                        Dec.CompressedSize = Filesize;
                        Dec.DecompressedSize = DecompressedSize;

                        byte[] DecompressedData = Dec.Decompress(m_Reader.ReadBytes((int)Filesize));
                        //m_Reader.Close();

                        isReadingSomething = false;

                        return DecompressedData;
                    }
                    else
                    {
                        m_Reader.BaseStream.Seek((m_Reader.BaseStream.Position - 15), SeekOrigin.Begin);

                        byte[] Data = m_Reader.ReadBytes((int)Entry.DecompressedFileSize);
                        //m_Reader.Close();

                        isReadingSomething = false;

                        return Data;
                    }
                }
                else
                {
                    byte[] Data = m_Reader.ReadBytes((int)Entry.DecompressedFileSize);
                    //m_Reader.Close();

                    isReadingSomething = false;

                    return Data;
                }
            }

            throw new FAR3Exception("FAR3Entry didn't exist in archive - FAR3Archive.GetEntry()");
        }

        /// <summary>
        /// Returns the entries of this FAR3Archive as byte arrays together with their corresponding FileIDs.
        /// </summary>
        /// <returns>A List of KeyValuePair instances.</returns>
        public List<KeyValuePair<uint, byte[]>> GetAllEntries()
        {
            List<KeyValuePair<uint, byte[]>> toReturn = new List<KeyValuePair<uint, byte[]>>();
            foreach (Far3Entry Entry in m_EntriesList)
            {
                toReturn.Add(new KeyValuePair<uint, byte[]>(Entry.FileID, GetEntry(Entry)));
            }
            return toReturn;
        }

        /// <summary>
        /// Returns the entries of this FAR3Archive as FAR3Entry instances in a List.
        /// </summary>
        /// <returns>Returns the entries of this FAR3Archive as FAR3Entry instances in a List.</returns>
        public List<Far3Entry> GetAllFAR3Entries()
        {
            List<Far3Entry> Entries = new List<Far3Entry>();

            foreach (KeyValuePair<string, Far3Entry> KVP in m_Entries)
                Entries.Add(KVP.Value);

            return Entries;
        }

        /// <summary>
        /// Gets an entry from a FileID.
        /// </summary>
        /// <param name="FileID">The entry's FileID.</param>
        /// <returns>The entry's data.</returns>
        public byte[] GetItemByID(uint FileID)
        {
            var item = m_EntryByID[FileID];
            if (item == null)
            {
                throw new FAR3Exception("Didn't find entry!");
            }
            return GetEntry(item);
        }

        /// <summary>
        /// Gets an entry from its ID (TypeID + FileID).
        /// </summary>
        /// <param name="ID">The ID of the entry.</param>
        /// <returns>The entry's data.</returns>
        public byte[] GetItemByID(ulong ID)
        {
            byte[] Bytes = BitConverter.GetBytes(ID);
            uint FileID = BitConverter.ToUInt32(Bytes, 4);
            uint TypeID = BitConverter.ToUInt32(Bytes, 0);

            var item = m_EntryByID[FileID];
            if (item == null || item.TypeID != TypeID)
            {
                throw new FAR3Exception("Didn't find entry!");
            }

            return GetEntry(item);
        }

        /// <summary>
        /// Gets an entry's data from a filename.
        /// </summary>
        /// <param name="Filename">The filename of the entry.</param>
        /// <returns>The entry's data.</returns>
        public byte[] this[string Filename]
        {
            get
            {
                return GetEntry(m_Entries[Filename]);
            }
        }

        #region IDisposable Members

        /// <summary>
        /// Disposes this FAR3Archive instance.
        /// </summary>
        public void Dispose()
        {
            if (m_Reader != null)
            {
                m_Reader.Close();
            }
        }

        #endregion
    }
}
