using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;

namespace Decal.DecalDat
{
    /// <summary>
    /// Low-level reader for Asheron's Call DAT archive files.
    /// DAT files use a sector-based linked-list format with a hierarchical directory.
    /// </summary>
    internal sealed class DatFile : IDisposable
    {
        private const int FileCount = 62;
        private const int FileCountOffset = 0x03E;
        private const int RootDirPtrOffset = 0x148;

        // Directory entry layout (packed):
        //   DWORD subdirs[62]    = 248 bytes
        //   DWORD fileCount      =   4 bytes
        //   FileEntry files[62]  = 744 bytes (62 * 12)
        //   Total                = 996 bytes
        private const int SubdirsSize = FileCount * 4;
        private const int FileEntrySize = 12; // 3 DWORDs: ID, Offset, Size

        private MemoryMappedFile _mmf;
        private MemoryMappedViewAccessor _accessor;
        private readonly int _sectorSize;
        private readonly long _fileLength;

        public DatFile(string filename, int sectorSize = 256)
        {
            _sectorSize = sectorSize;

            var fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            _fileLength = fs.Length;
            _mmf = MemoryMappedFile.CreateFromFile(fs, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, false);
            _accessor = _mmf.CreateViewAccessor(0, _fileLength, MemoryMappedFileAccess.Read);
        }

        public DatFileEntry GetFile(uint fileId)
        {
            uint rootDirOffset = _accessor.ReadUInt32(RootDirPtrOffset);
            return FindFile(rootDirOffset, fileId);
        }

        private DatFileEntry FindFile(uint dirOffset, uint fileId)
        {
            // Read file count from directory
            uint fileCount = _accessor.ReadUInt32(dirOffset + SubdirsSize);

            // Binary search through sorted file entries
            int lo = 0, hi = (int)fileCount - 1;

            while (lo <= hi)
            {
                int mid = (lo + hi) / 2;
                long entryOffset = dirOffset + SubdirsSize + 4 + (mid * FileEntrySize);
                uint entryId = _accessor.ReadUInt32(entryOffset);

                if (entryId == fileId)
                {
                    uint offset = _accessor.ReadUInt32(entryOffset + 4);
                    uint size = _accessor.ReadUInt32(entryOffset + 8);
                    return new DatFileEntry(this, offset, size);
                }
                else if (fileId < entryId)
                {
                    // Recurse into left subdirectory
                    uint subdir = _accessor.ReadUInt32(dirOffset + (uint)(mid * 4));
                    if (subdir != 0)
                    {
                        try
                        {
                            return FindFile(subdir, fileId);
                        }
                        catch (FileNotFoundException)
                        {
                            // Not in this subtree
                        }
                    }
                    hi = mid - 1;
                }
                else
                {
                    lo = mid + 1;
                }
            }

            // Check rightmost subdirectory
            uint rightSubdir = _accessor.ReadUInt32(dirOffset + (uint)(lo * 4));
            if (rightSubdir != 0)
            {
                return FindFile(rightSubdir, fileId);
            }

            throw new FileNotFoundException($"File 0x{fileId:X8} not found in DAT");
        }

        internal int SectorSize => _sectorSize;
        internal int DataPerSector => _sectorSize - 4; // First 4 bytes = next sector pointer

        internal void ReadBytes(long offset, byte[] buffer, int bufferOffset, int count)
        {
            _accessor.ReadArray(offset, buffer, bufferOffset, count);
        }

        internal uint ReadUInt32(long offset)
        {
            return _accessor.ReadUInt32(offset);
        }

        public void Dispose()
        {
            _accessor?.Dispose();
            _accessor = null;
            _mmf?.Dispose();
            _mmf = null;
        }
    }

    /// <summary>
    /// Represents a single file within a DAT archive.
    /// Reads through a linked list of sectors.
    /// </summary>
    internal sealed class DatFileEntry
    {
        private readonly DatFile _source;
        private readonly uint _firstSectorOffset;
        private readonly uint _size;

        private uint _currentSectorOffset;
        private int _posInSector; // position within current sector's data area
        private int _totalRead;  // total bytes read so far

        public DatFileEntry(DatFile source, uint sectorOffset, uint size)
        {
            _source = source;
            _firstSectorOffset = sectorOffset;
            _size = size;
            Reset();
        }

        public int Size => (int)_size;
        public int Tell => _totalRead;

        public void Reset()
        {
            _currentSectorOffset = _firstSectorOffset;
            _posInSector = 0;
            _totalRead = 0;
        }

        public void Skip(int bytes)
        {
            int remaining = bytes;
            while (remaining > 0)
            {
                int dataPerSector = _source.DataPerSector;
                int availableInSector = dataPerSector - _posInSector;

                if (remaining < availableInSector)
                {
                    _posInSector += remaining;
                    _totalRead += remaining;
                    return;
                }

                // Skip rest of this sector and move to next
                _totalRead += availableInSector;
                remaining -= availableInSector;
                MoveToNextSector();
            }
        }

        public int Read(byte[] buffer, int offset, int count)
        {
            int remaining = Math.Min(count, (int)_size - _totalRead);
            int totalCopied = 0;

            while (remaining > 0)
            {
                int dataPerSector = _source.DataPerSector;
                int availableInSector = dataPerSector - _posInSector;
                int toCopy = Math.Min(remaining, availableInSector);

                // Data starts at sector offset + 4 (skip next-sector pointer)
                long readOffset = _currentSectorOffset + 4 + _posInSector;
                _source.ReadBytes(readOffset, buffer, offset + totalCopied, toCopy);

                _posInSector += toCopy;
                _totalRead += toCopy;
                totalCopied += toCopy;
                remaining -= toCopy;

                if (_posInSector >= dataPerSector && remaining > 0)
                {
                    MoveToNextSector();
                }
            }

            return totalCopied;
        }

        private void MoveToNextSector()
        {
            // Next sector pointer is stored in first 4 bytes of current sector
            uint nextSector = _source.ReadUInt32(_currentSectorOffset);
            _currentSectorOffset = nextSector;
            _posInSector = 0;
        }
    }
}
