using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Ionic.Zlib;

namespace Substrate.Core
{
    public class RegionFile : IDisposable
    {

        private const int VERSION_GZIP = 1;
        private const int VERSION_DEFLATE = 2;

        private const int SECTOR_BYTES = 4096;
        private const int SECTOR_INTS = SECTOR_BYTES / 4;

        const int CHUNK_HEADER_SIZE = 5;

        private static byte[] emptySector = new byte[4096];

        private string fileName;
        private FileStream file;

        /// <summary>
        /// The file lock used so that we do not seek in different areas
        /// of the same file at the same time. All file access should lock this
        /// object before moving the file pointer.
        /// The lock should also surround all access to the sectorFree free variable.
        /// </summary>
        private object fileLock = new object();

        private int[] offsets;
        private int[] chunkTimestamps;
        private List<Boolean> sectorFree;
        private int sizeDelta;
        private long lastModified = 0;

        private bool _disposed = false;

        public RegionFile (string path)
        {
            offsets = new int[SectorInts];
            chunkTimestamps = new int[SectorInts];

            fileName = path;
            Debugln("REGION LOAD " + fileName);

            sizeDelta = 0;

            ReadFile();
        }

        ~RegionFile ()
        {
            Dispose(false);
        }

        public void Dispose ()
        {
            Dispose(true);
            System.GC.SuppressFinalize(this);
        }

        protected virtual void Dispose (bool disposing)
        {
            if (!_disposed) {
                if (disposing) {
                    // Cleanup managed resources
                }

                // Cleanup unmanaged resources
                if (file != null) {
                    lock (this.fileLock) {
                        file.Close();
                        file = null;
                    }
                }
            }
            _disposed = true;
        }

        protected void ReadFile ()
        {
            if (_disposed) {
                throw new ObjectDisposedException("RegionFile", "Attempting to use a RegionFile after it has been disposed.");
            }

            // Get last udpate time
            long newModified = -1;
            try {
                if (File.Exists(fileName)) {
                    newModified = Timestamp(File.GetLastWriteTime(fileName));
                }
            }
            catch (UnauthorizedAccessException e) {
                Console.WriteLine(e.Message);
                return;
            }

            // If it hasn't been modified, we don't need to do anything
            if (newModified == lastModified) {
                return;
            }

            try {
                lock (this.fileLock) {
                    file = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);

                    //using (file) {
                    if (file.Length < SectorBytes) {
                        byte[] int0 = BitConverter.GetBytes((int)0);

                        /* we need to write the chunk offset table */
                        for (int i = 0; i < SectorInts; ++i) {
                            file.Write(int0, 0, 4);
                        }
                        // write another sector for the timestamp info
                        for (int i = 0; i < SectorInts; ++i) {
                            file.Write(int0, 0, 4);
                        }

                        file.Flush();

                        sizeDelta += SectorBytes * 2;
                    }

                    if ((file.Length & 0xfff) != 0) {
                        /* the file size is not a multiple of 4KB, grow it */
                        file.Seek(0, SeekOrigin.End);
                        for (int i = 0; i < (file.Length & 0xfff); ++i) {
                            file.WriteByte(0);
                        }

                        file.Flush();
                    }

                    /* set up the available sector map */
                    int nSectors = (int)file.Length / SectorBytes;
                    sectorFree = new List<Boolean>(nSectors);

                    for (int i = 0; i < nSectors; ++i) {
                        sectorFree.Add(true);
                    }

                    sectorFree[0] = false; // chunk offset table
                    sectorFree[1] = false; // for the last modified info

                    file.Seek(0, SeekOrigin.Begin);
                    for (int i = 0; i < SectorInts; ++i) {
                        byte[] offsetBytes = new byte[4];
                        file.Read(offsetBytes, 0, 4);

                        if (BitConverter.IsLittleEndian) {
                            Array.Reverse(offsetBytes);
                        }
                        int offset = BitConverter.ToInt32(offsetBytes, 0);

                        offsets[i] = offset;
                        if (offset != 0 && (offset >> 8) + (offset & 0xFF) <= sectorFree.Count) {
                            for (int sectorNum = 0; sectorNum < (offset & 0xFF); ++sectorNum) {
                                sectorFree[(offset >> 8) + sectorNum] = false;
                            }
                        }
                    }
                    for (int i = 0; i < SectorInts; ++i) {
                        byte[] modBytes = new byte[4];
                        file.Read(modBytes, 0, 4);

                        if (BitConverter.IsLittleEndian) {
                            Array.Reverse(modBytes);
                        }
                        int lastModValue = BitConverter.ToInt32(modBytes, 0);

                        chunkTimestamps[i] = lastModValue;
                    }
                }
            }
            catch (IOException e) {
                System.Console.WriteLine(e.Message);
                System.Console.WriteLine(e.StackTrace);
            }
        }

        /* the modification date of the region file when it was first opened */
        public long LastModified ()
        {
            return lastModified;
        }

        /* gets how much the region file has grown since it was last checked */
        public int GetSizeDelta ()
        {
            int ret = sizeDelta;
            sizeDelta = 0;
            return ret;
        }

        // various small debug printing helpers
        private void Debug (String str)
        {
            //        System.Consle.Write(str);
        }

        private void Debugln (String str)
        {
            Debug(str + "\n");
        }

        private void Debug (String mode, int x, int z, String str)
        {
            Debug("REGION " + mode + " " + fileName + "[" + x + "," + z + "] = " + str);
        }

        private void Debug (String mode, int x, int z, int count, String str)
        {
            Debug("REGION " + mode + " " + fileName + "[" + x + "," + z + "] " + count + "B = " + str);
        }

        private void Debugln (String mode, int x, int z, String str)
        {
            Debug(mode, x, z, str + "\n");
        }

        /*
         * gets an (uncompressed) stream representing the chunk data returns null if
         * the chunk is not found or an error occurs
         */
        public Stream GetChunkDataInputStream (int x, int z)
        {
            if (_disposed) {
                throw new ObjectDisposedException("RegionFile", "Attempting to use a RegionFile after it has been disposed.");
            }

            if (OutOfBounds(x, z)) {
                Debugln("READ", x, z, "out of bounds");
                return null;
            }

            try {
                int offset = GetOffset(x, z);
                if (offset == 0) {
                    // Debugln("READ", x, z, "miss");
                    return null;
                }

                int sectorNumber = offset >> 8;
                int numSectors = offset & 0xFF;

                lock (this.fileLock)
                {
                    if (sectorNumber + numSectors > sectorFree.Count) {
                        Debugln("READ", x, z, "invalid sector");
                        return null;
                    }

                    file.Seek(sectorNumber * SectorBytes, SeekOrigin.Begin);
                    byte[] lengthBytes = new byte[4];
                    file.Read(lengthBytes, 0, 4);

                    if (BitConverter.IsLittleEndian) {
                        Array.Reverse(lengthBytes);
                    }
                    int length = BitConverter.ToInt32(lengthBytes, 0);

                    if (length > SectorBytes * numSectors) {
                        Debugln("READ", x, z, "invalid length: " + length + " > 4096 * " + numSectors);
                        return null;
                    }

                    byte version = (byte)file.ReadByte();
                    if (version == VERSION_GZIP) {
                        byte[] data = new byte[length - 1];
                        file.Read(data, 0, data.Length);
                        Stream ret = new GZipStream(new MemoryStream(data), CompressionMode.Decompress);

                        return ret;
                    }
                    else if (version == VERSION_DEFLATE) {
                        byte[] data = new byte[length - 1];
                        file.Read(data, 0, data.Length);

                        Stream ret = new ZlibStream(new MemoryStream(data), CompressionMode.Decompress, true);
                        return ret;

                        /*MemoryStream sinkZ = new MemoryStream();
                        ZlibStream zOut = new ZlibStream(sinkZ, CompressionMode.Decompress, true);
                        zOut.Write(data, 0, data.Length);
                        zOut.Flush();
                        zOut.Close();

                        sinkZ.Seek(0, SeekOrigin.Begin);
                        return sinkZ;*/
                    }

                    Debugln("READ", x, z, "unknown version " + version);
                    return null;
                }
            }
            catch (IOException) {
                Debugln("READ", x, z, "exception");
                return null;
            }
        }

        public Stream GetChunkDataOutputStream (int x, int z)
        {
            if (OutOfBounds(x, z)) return null;

            return new ZlibStream(new ChunkBuffer(this, x, z), CompressionMode.Compress);
        }

        public Stream GetChunkDataOutputStream (int x, int z, int timestamp)
        {
            if (OutOfBounds(x, z)) return null;

            return new ZlibStream(new ChunkBuffer(this, x, z, timestamp), CompressionMode.Compress);
        }

        /*
         * lets chunk writing be multithreaded by not locking the whole file as a
         * chunk is serializing -- only writes when serialization is over
         */
        class ChunkBuffer : MemoryStream
        {
            private int x, z;
            private RegionFile region;

            private int? _timestamp;

            public ChunkBuffer (RegionFile r, int x, int z)
                : base(8096)
            {
                this.region = r;
                this.x = x;
                this.z = z;
            }

            public ChunkBuffer (RegionFile r, int x, int z, int timestamp)
                : this(r, x, z)
            {
                _timestamp = timestamp;
            }

            public override void Close ()
            {
                if (_timestamp == null) {
                    region.Write(x, z, this.GetBuffer(), (int)this.Length);
                }
                else {
                    region.Write(x, z, this.GetBuffer(), (int)this.Length, (int)_timestamp);
                }
            }
        }

        protected void Write (int x, int z, byte[] data, int length)
        {
            Write(x, z, data, length, Timestamp());
        }

        /* write a chunk at (x,z) with length bytes of data to disk */
        protected void Write (int x, int z, byte[] data, int length, int timestamp)
        {
            if (_disposed) {
                throw new ObjectDisposedException("RegionFile", "Attempting to use a RegionFile after it has been disposed.");
            }

            try {
                int offset = GetOffset(x, z);
                int sectorNumber = offset >> 8;
                int sectorsAllocated = offset & 0xFF;
                int sectorsNeeded = (length + CHUNK_HEADER_SIZE) / SectorBytes + 1;

                // maximum chunk size is 1MB
                if (sectorsNeeded >= 256) {
                    return;
                }

                if (sectorNumber != 0 && sectorsAllocated == sectorsNeeded) {
                    /* we can simply overwrite the old sectors */
                    Debug("SAVE", x, z, length, "rewrite");
                    Write(sectorNumber, data, length);
                }
                else {
                    /* we need to allocate new sectors */

                    lock (this.fileLock) {
                        /* mark the sectors previously used for this chunk as free */
                        for (int i = 0; i < sectorsAllocated; ++i) {
                            sectorFree[sectorNumber + i] = true;
                        }

                        /* scan for a free space large enough to store this chunk */
                        int runStart = sectorFree.FindIndex(b => b == true);
                        int runLength = 0;
                        if (runStart != -1) {
                            for (int i = runStart; i < sectorFree.Count; ++i) {
                                if (runLength != 0) {
                                    if (sectorFree[i]) runLength++;
                                    else runLength = 0;
                                }
                                else if (sectorFree[i]) {
                                    runStart = i;
                                    runLength = 1;
                                }
                                if (runLength >= sectorsNeeded) {
                                    break;
                                }
                            }
                        }

                        if (runLength >= sectorsNeeded) {
                            /* we found a free space large enough */
                            Debug("SAVE", x, z, length, "reuse");
                            sectorNumber = runStart;
                            SetOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
                            for (int i = 0; i < sectorsNeeded; ++i) {
                                sectorFree[sectorNumber + i] = false;
                            }
                            Write(sectorNumber, data, length);
                        }
                        else {
                            /*
                             * no free space large enough found -- we need to grow the
                             * file
                             */
                            Debug("SAVE", x, z, length, "grow");
                            file.Seek(0, SeekOrigin.End);
                            sectorNumber = sectorFree.Count;
                            for (int i = 0; i < sectorsNeeded; ++i) {
                                file.Write(emptySector, 0, emptySector.Length);
                                sectorFree.Add(false);
                            }
                            sizeDelta += SectorBytes * sectorsNeeded;

                            Write(sectorNumber, data, length);
                            SetOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
                        }
                    }
                }
                SetTimestamp(x, z, timestamp);
            }
            catch (IOException e) {
                Console.WriteLine(e.StackTrace);
            }
        }

        /* write a chunk data to the region file at specified sector number */
        private void Write (int sectorNumber, byte[] data, int length)
        {
            lock (this.fileLock) {
                Debugln(" " + sectorNumber);
                file.Seek(sectorNumber * SectorBytes, SeekOrigin.Begin);

                byte[] bytes = BitConverter.GetBytes(length + 1);
                if (BitConverter.IsLittleEndian) {
                    ;
                    Array.Reverse(bytes);
                }
                file.Write(bytes, 0, 4); // chunk length
                file.WriteByte(VERSION_DEFLATE); // chunk version number
                file.Write(data, 0, length); // chunk data
            }
        }

        public void DeleteChunk (int x, int z)
        {
            lock (this.fileLock) {
                int offset = GetOffset(x, z);
                int sectorNumber = offset >> 8;
                int sectorsAllocated = offset & 0xFF;

                file.Seek(sectorNumber * SectorBytes, SeekOrigin.Begin);
                for (int i = 0; i < sectorsAllocated; i++) {
                    file.Write(emptySector, 0, SectorBytes);
                }

                SetOffset(x, z, 0);
                SetTimestamp(x, z, 0);
            }
        }

        /* is this an invalid chunk coordinate? */
        private bool OutOfBounds (int x, int z)
        {
            return x < 0 || x >= 32 || z < 0 || z >= 32;
        }

        private int GetOffset (int x, int z)
        {
            return offsets[x + z * 32];
        }

        public bool HasChunk (int x, int z)
        {
            return GetOffset(x, z) != 0;
        }

        private void SetOffset (int x, int z, int offset)
        {
            lock (this.fileLock) {
                offsets[x + z * 32] = offset;
                file.Seek((x + z * 32) * 4, SeekOrigin.Begin);

                byte[] bytes = BitConverter.GetBytes(offset);
                if (BitConverter.IsLittleEndian) {
                    ;
                    Array.Reverse(bytes);
                }

                file.Write(bytes, 0, 4);
            }
        }

        private int Timestamp ()
        {
            DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return (int)((DateTime.UtcNow - epoch).Ticks / (10000L * 1000L));
        }

        private int Timestamp (DateTime time)
        {
            DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return (int)((time - epoch).Ticks / (10000L * 1000L));
        }

        public int GetTimestamp (int x, int z)
        {
            return chunkTimestamps[x + z * 32];
        }

        public void SetTimestamp (int x, int z, int value)
        {
            lock (this.fileLock) {
                chunkTimestamps[x + z * 32] = value;
                file.Seek(SectorBytes + (x + z * 32) * 4, SeekOrigin.Begin);

                byte[] bytes = BitConverter.GetBytes(value);
                if (BitConverter.IsLittleEndian) {
                    ;
                    Array.Reverse(bytes);
                }

                file.Write(bytes, 0, 4);
            }
        }

        public void Close ()
        {
            lock (this.fileLock) {
                file.Close();
            }
        }

        protected virtual int SectorBytes
        {
            get { return SECTOR_BYTES; }
        }

        protected virtual int SectorInts
        {
            get { return SECTOR_BYTES / 4; }
        }

        protected virtual byte[] EmptySector
        {
            get { return emptySector; }
        }
    }
}