using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using MPQ.Updater;
using CommonLang.IO;
using System.Text.RegularExpressions;

namespace MPQ.FileSystem
{
    public class MPQFileSystem : IDisposable
    {
        private Dictionary<long, Dictionary<string, MPQFileEntry>> indexer = new Dictionary<long, Dictionary<string, MPQFileEntry>>();
        private Dictionary<string, MPQStream> mpq_files = new Dictionary<string, MPQStream>();
        /// <summary>
        /// 用于老文件存在,新目录已没有的文件
        /// </summary>
        private Dictionary<string, string> dir = new Dictionary<string, string>();

        public MPQFileSystem()
        {

        }
        /// <summary>
        /// 搜索并加载目录里的所有MPQ文件
        /// </summary>
        /// <param name="mpq_dir"></param>
        /// <returns></returns>
        public bool init(DirectoryInfo mpq_dir)
        {
            if (mpq_dir.Exists)
            {
                loadDir(new FileInfo(mpq_dir.FullName + Path.DirectorySeparatorChar + ".dir"));
                try
                {
                    foreach (FileInfo file in mpq_dir.GetFiles())
                    {
                        if (file.Extension.ToLower().EndsWith(MPQ.Updater.MPQUpdater.MPQ_EXT))
                        {
                            if (load(file.FullName) == null)
                            {
                                return false;
                            }
                        }
                    }
                    foreach (DirectoryInfo sub_dir in mpq_dir.GetDirectories())
                    {
                        if (!init(sub_dir))
                        {
                            return false;
                        }
                    }
                }
                finally
                {
                    dir.Clear();
                }
                return true;
            }
            return false;
        }
        /// <summary>
        /// 将自动更新的MPQ加载到文件系统
        /// </summary>
        /// <param name="updater"></param>
        /// <returns></returns>
        public bool init(MPQUpdater updater)
        {
            loadDir(new FileInfo(updater.LocalSaveRoot.FullName + Path.DirectorySeparatorChar + ".dir"));
            try
            {
                foreach (FileInfo rmf in updater.GetAllFiles())
                {
                    if (rmf.FullName.ToLower().EndsWith(MPQUpdater.MPQ_EXT))
                    {
                        if (load(rmf.FullName) == null)
                        {
                            return false;
                        }
                    }
                }
            }
            finally
            {
                dir.Clear();
            }
            return true;
        }
        private void loadDir(FileInfo dir_file)
        {
            if (dir_file.Exists)
            {
                string[] lines = File.ReadAllLines(dir_file.FullName);
                foreach (var line in lines)
                {
                    dir.Add(line.Trim(), line);
                }
            }
        }

        /// <summary>
        /// 加载单个MPQ文件到文件系统
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public MPQStream load(string path)
        {
            FileInfo fileinfo = new FileInfo(Path.GetFullPath(path));
            //Console.WriteLine("MPQFile::Load : " + fileinfo.FullName);
            List<MPQFileEntry> entries = new List<MPQFileEntry>(1000);
            MPQStream mpq_stream = new MPQStream(fileinfo);
            if (mpq_stream.loadEntrys(entries))
            {
                mpq_files[fileinfo.FullName] = mpq_stream;
                foreach (MPQFileEntry e in entries)
                {
                    if (dir.Count == 0 || dir.ContainsKey(e.Key))
                    {
                        putEntry(e);
                    }
                    else
                    {
                        Console.WriteLine(string.Format("MPQFileSystem : ignore entry \"{0}\" not exist in .dir file!!!", e.Key));
                    }
                }
                return mpq_stream;
            }
            else
            {
                mpq_stream.Dispose();
                throw new Exception("Cannot Init MPQ file : " + path);
            }
        }
        public void Dispose()
        {
            indexer.Clear();
            foreach (MPQStream path in mpq_files.Values)
            {
                path.Dispose();
            }
            mpq_files.Clear();
        }

        virtual protected long hashCode(string data)
        {
            return data.GetHashCode();
        }

        private bool putEntry(MPQFileEntry re)
        {
            re.hash = hashCode(re.key);

            Dictionary<string, MPQFileEntry> ets = null;
            // 首个HASH
            if (!indexer.TryGetValue(re.hash, out ets))
            {
                ets = new Dictionary<string, MPQFileEntry>(1);
                ets[re.key] = re;
                indexer[re.hash] = ets;
                return true;
            }
            // 首个文件
            MPQFileEntry exist = null;
            if (!ets.TryGetValue(re.key, out exist))
            {
                ets[re.key] = re;
                return true;
            }
            // 如果当前文件较新,则更新
            if (exist.f_date < re.f_date)
            {
                ets[re.key] = re;
                return true;
            }
            // 当前文件较老,忽略
            return false;
        }

        private MPQFileEntry findEntry(long hash, String name)
        {
            Dictionary<string, MPQFileEntry> ets = null;
            if (indexer.TryGetValue(hash, out ets))
            {
                MPQFileEntry ret = null;
                if (ets.TryGetValue(name, out ret))
                {
                    return ret;
                }
            }
            return null;
        }

        public MPQFileEntry findEntry(string path)
        {
            long hash = hashCode(path);
            return findEntry(hash, path);
        }

        public byte[] getEntryData(MPQFileEntry e)
        {
            if (e != null)
            {
                byte[] data = new byte[e.f_size];
                e.fs.Read(e, 0, data, 0, data.Length);
                return data;
            }
            return null;
        }
        public byte[] getData(String name)
        {
            long hash = hashCode(name);
            MPQFileEntry e = findEntry(hash, name);
            byte[] ed = getEntryData(e);
            return ed;
        }

        public Stream openEntryStream(MPQFileEntry e)
        {
            if (e != null)
            {
                return new EntryStream(e);
            }
            return null;
        }
        public Stream openStream(String name)
        {
            long hash = hashCode(name);
            MPQFileEntry e = findEntry(hash, name);
            Stream ed = openEntryStream(e);
            return ed;
        }

        public List<MPQFileEntry> listEntrys()
        {
            List<MPQFileEntry> ret = new List<MPQFileEntry>();
            foreach (Dictionary<string, MPQFileEntry> fe in indexer.Values)
            {
                foreach (MPQFileEntry e in fe.Values)
                {
                    ret.Add(e);
                }
            }
            return ret;
        }

        public List<MPQFileEntry> listEntrys(string pettern)
        {
            Regex regex = new Regex(pettern);
            List<MPQFileEntry> ret = new List<MPQFileEntry>();
            foreach (Dictionary<string, MPQFileEntry> fe in indexer.Values)
            {
                foreach (MPQFileEntry e in fe.Values)
                {
                    if (regex.IsMatch(e.key))
                    {
                        ret.Add(e);
                    }
                }
            }
            return ret;
        }

        //----------------------------------------------------------------------------------------------------------------------

        public class MPQStream : IDisposable
        {
            public static byte[] FS_HEAD_START = { (byte)'M', (byte)'F', (byte)'F', (byte)'S' };
            public static byte[] FS_ENTRY_START = { (byte)'M', (byte)'F', (byte)'E', (byte)'T' };
            public static byte[] FS_TRUNK_START = { (byte)'M', (byte)'F', (byte)'T', (byte)'K' };
            public static byte[] FS_END = { (byte)'M', (byte)'F', (byte)'E', (byte)'D' };
            public static byte[] VERSION = { 0, 0, 0, 1 };

            private FileInfo info;
            private FileStream fis;
            private long trunk_start;

            public long TrunkStart
            {
                get { return trunk_start; }
            }

            public FileInfo MPQFile
            {
                get { return info; }
            }

            public MPQStream(FileInfo file)
            {
                this.info = file;
                this.fis = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read);
            }

            private bool headEquals(byte[] a, byte[] b)
            {
                for (int i = 0; i < a.Length; i++)
                {
                    if (a[i] != b[i])
                    {
                        return false;
                    }
                }
                return true;
            }

            //             public List<MPQFileEntry> ListEntrys()
            //             {
            //                 List<MPQFileEntry> ret = new List<MPQFileEntry>();
            //                 if (loadEntrys(ret))
            //                 {
            //                 }
            //                 return ret;
            //             }

            public bool loadEntrys(List<MPQFileEntry> entries)
            {
                lock (fis)
                {
                    fis.Position = 0;
                    BinaryReader bis = new BinaryReader(fis, Encoding.UTF8);
                    byte[] head_trunk = IOUtil.ReadExpect(fis, MPQStream.FS_HEAD_START.Length); // head
                    if (headEquals(head_trunk, MPQStream.FS_HEAD_START))
                    {
                        head_trunk = IOUtil.ReadExpect(fis, MPQStream.VERSION.Length);// version
                        long total_size = bis.ReadInt64();
                        head_trunk = IOUtil.ReadExpect(fis, MPQStream.FS_ENTRY_START.Length);// entry start
                        if (headEquals(head_trunk, MPQStream.FS_ENTRY_START))
                        {
                            int entry_count = bis.ReadInt32();
                            for (int i = 0; i < entry_count; i++)
                            {
                                MPQFileEntry re = new MPQFileEntry();
                                re.fs = this;
                                re.load(bis);
                                entries.Add(re);
                            }
                            head_trunk = IOUtil.ReadExpect(fis, MPQStream.FS_TRUNK_START.Length);// trunk start
                            if (headEquals(head_trunk, MPQStream.FS_TRUNK_START))
                            {
                                // record file trunk start
                                this.trunk_start = fis.Position;
                                return true;
                            }
                        }
                    }
                }
                return false;
            }

            public void Dispose()
            {
                try
                {
                    lock (fis)
                    {
                        fis.Close();
                        fis.Dispose();
                    }
                }
                catch (Exception err)
                {
                    Console.WriteLine(err.Message + "\n" + err.StackTrace);
                }
            }

            public int Read(MPQFileEntry src, long src_pos, byte[] dst, int dst_pos, int length)
            {
                if (src.fs == this)
                {
                    lock (fis)
                    {
                        try
                        {
                            fis.Position = trunk_start + src.f_start + src_pos;
                            IOUtil.ReadToEnd(fis, dst, dst_pos, length);
                        }
                        catch (Exception err)
                        {
                            throw new Exception("MPQStream read error : " + err.Message, err);
                        }
                    }
                    return length;
                }
                throw new Exception("MPQStream read error");
            }

        }

        public class MPQFileEntry
        {
            internal static DateTime JAVA_START_DATE = new DateTime(1970, 1, 1, 0, 0, 0);

            internal long hash;         // 文件名HASH
                                        //internal int index;		    // HASH对应所在Entry位置
            internal String key;            // 文件名
                                            //internal int key_size;	    // 文件名长度
                                            //internal String key_md5;	// 文件名MD5
            internal int f_start;           // 文件内容开始位置
            internal int f_size;            // 文件内容尺寸
            internal long f_date;           // 文件日期(1970-1-1起始秒) 
                                            //internal String f_md5;		// 文件内容MD5

            internal MPQFileSystem.MPQStream fs;

            public string Key
            {
                get { return key; }
            }
            public int Size
            {
                get { return f_size; }
            }
            public DateTime Date
            {
                get { return JAVA_START_DATE.AddSeconds(f_date); }
            }

            public override string ToString()
            {
                return key + "(" + f_size + ")";
            }

            public byte[] getFileData()
            {
                byte[] data = new byte[this.f_size];
                this.fs.Read(this, 0, data, 0, data.Length);
                return data;
            }

            internal void load(BinaryReader bis)
            {
                /*
                 * hash 		= LittleIODeserialize.getLong	(is);
                 * index	 	= LittleIODeserialize.getInt	(is);
                 * key			= LittleIODeserialize.getString	(is, "UTF-8");
                 * key_size 	= LittleIODeserialize.getInt	(is);
                 * key_md5 	    = LittleIODeserialize.getString	(is, "UTF-8");
                 * f_start 	    = LittleIODeserialize.getInt	(is);
                 * f_size 		= LittleIODeserialize.getInt	(is);
                 * f_date 		= LittleIODeserialize.getLong	(is);
                 * f_md5 		= LittleIODeserialize.getString	(is, "UTF-8");
                 */
                this.hash = bis.ReadInt64();
                bis.ReadInt32();
                this.key = readUTF(bis);
                bis.ReadInt32();
                readUTF(bis);
                this.f_start = bis.ReadInt32();
                this.f_size = bis.ReadInt32();
                this.f_date = bis.ReadInt64();
                readUTF(bis);
            }

            private static string readUTF(BinaryReader bis)
            {
                int len = bis.ReadUInt16();
                byte[] bytes = new byte[len];
                int readed = bis.Read(bytes, 0, len);
                while (readed < len)
                {
                    readed += bis.Read(bytes, readed, len - readed);
                }
                return Encoding.UTF8.GetString(bytes, 0, len);
            }

            public bool Equals(MPQFileEntry b)
            {
                if (!b.key.Equals(this.key)) return false;
                if (!b.f_size.Equals(this.f_size)) return false;
                if (!b.f_date.Equals(this.f_date)) return false;
                if (!b.f_start.Equals(this.f_start)) return false;
                return true;
            }

        }

        /// <summary>
        /// 外部读取用流
        /// </summary>
        internal class EntryStream : Stream
        {
            private long pos = 0;
            private MPQFileEntry e;

            public EntryStream(MPQFileEntry entry)
            {
                this.e = entry;
            }
            public override long Position
            {
                get { return pos; }
                set { pos = value; }
            }
            public override long Length
            {
                get { return e.Size; }
            }
            public override bool CanRead
            {
                get { return true; }
            }
            public override bool CanSeek
            {
                get { return false; }
            }
            public override bool CanWrite
            {
                get { return false; }
            }
            public override int Read(byte[] buffer, int offset, int count)
            {
                long avaliable = e.Size - pos;
                if (avaliable > 0)
                {
                    count = (int)Math.Min(avaliable, count);
                    int readed = e.fs.Read(e, pos, buffer, offset, count);
                    pos += readed;
                    return readed;
                }
                else if (avaliable == 0)
                {
                    return 0;
                }
                throw new IOException("EOF of MPQEntry");
            }
            public override long Seek(long offset, SeekOrigin origin)
            {
                throw new NotImplementedException();
            }
            public override void SetLength(long value)
            {
                throw new NotImplementedException();
            }
            public override void Write(byte[] buffer, int offset, int count)
            {
                throw new NotImplementedException();
            }
            public override void Flush()
            {
            }
        }
    }

}