using System.Text;
using GsaEditor.Core.Models;

namespace GsaEditor.Core.IO;

/// <summary>
/// Reads a .gsa binary archive from a stream or file and produces a <see cref="GsaArchive"/>.
/// Supports both NARC (legacy) and NARD (enhanced with padding) format variants.
/// </summary>
public static class GsaReader
{
    /// <summary>Magic word for the NARC (legacy) format.</summary>
    private const uint MAGIC_NARC = 0x4E415243;

    /// <summary>Magic word for the NARD (enhanced) format.</summary>
    private const uint MAGIC_NARD = 0x4E415244;

    /// <summary>Maximum allowed alias length in characters.</summary>
    private const int MAX_ALIAS_LENGTH = 511;

    /// <summary>
    /// Reads a GSA archive from the specified file path.
    /// </summary>
    /// <param name="filePath">The path to the .gsa archive file.</param>
    /// <returns>A fully populated <see cref="GsaArchive"/>.</returns>
    /// <exception cref="InvalidDataException">Thrown if the file has an invalid magic word.</exception>
    public static GsaArchive Read(string filePath)
    {
        using var stream = File.OpenRead(filePath);
        return Read(stream);
    }

    /// <summary>
    /// Reads a GSA archive from a stream by performing a sequential scan of all entries.
    /// </summary>
    /// <param name="stream">A readable, seekable stream positioned at the start of the archive.</param>
    /// <returns>A fully populated <see cref="GsaArchive"/>.</returns>
    /// <exception cref="InvalidDataException">Thrown if the stream has an invalid magic word.</exception>
    public static GsaArchive Read(Stream stream)
    {
        using var reader = new BinaryReader(stream, Encoding.ASCII, leaveOpen: true);
        var archive = new GsaArchive();

        // Read global header
        uint magic = reader.ReadUInt32();
        if (magic == MAGIC_NARC)
        {
            archive.Format = GsaFormat.NARC;
        }
        else if (magic == MAGIC_NARD)
        {
            archive.Format = GsaFormat.NARD;
            archive.OffsetPadding = reader.ReadInt32();
            archive.SizePadding = reader.ReadInt32();
        }
        else
        {
            throw new InvalidDataException(
                $"Invalid GSA magic word: 0x{magic:X8}. Expected NARC (0x{MAGIC_NARC:X8}) or NARD (0x{MAGIC_NARD:X8}).");
        }

        // Sequentially read all entries until EOF
        while (stream.Position < stream.Length)
        {
            var entry = ReadEntry(reader, archive);
            if (entry == null)
                break;
            archive.Entries.Add(entry);
        }

        return archive;
    }

    /// <summary>
    /// Reads a single file entry from the current stream position.
    /// </summary>
    /// <param name="reader">The binary reader positioned at the start of an entry.</param>
    /// <param name="archive">The parent archive (used for NARD padding alignment).</param>
    /// <returns>A populated <see cref="GsaEntry"/>, or null if no more entries can be read.</returns>
    private static GsaEntry? ReadEntry(BinaryReader reader, GsaArchive archive)
    {
        var stream = reader.BaseStream;

        if (stream.Position >= stream.Length)
            return null;

        // NARD: align stream position before reading entry header
        if (archive.Format == GsaFormat.NARD && archive.OffsetPadding > 0)
            AlignStream(stream, archive.OffsetPadding);

        // Check if enough bytes remain for the alias length field
        if (stream.Length - stream.Position < 4)
            return null;

        // Read alias
        uint aliasLength = reader.ReadUInt32();
        if (aliasLength == 0 || aliasLength > MAX_ALIAS_LENGTH)
            return null;

        if (stream.Length - stream.Position < aliasLength)
            return null;

        byte[] aliasBytes = reader.ReadBytes((int)aliasLength);
        string alias = Encoding.ASCII.GetString(aliasBytes);

        // Read compression flag (bit 0: 1=compressed, 0=raw)
        byte compressionByte = reader.ReadByte();
        bool isCompressed = (compressionByte & 0x01) != 0;

        // Read original (decompressed) length
        uint originalLength = reader.ReadUInt32();

        // Read compressed length (only present if compressed)
        uint compressedLength;
        if (isCompressed)
        {
            compressedLength = reader.ReadUInt32();
        }
        else
        {
            compressedLength = originalLength;
        }

        // NARD: align stream position before reading the data block
        if (archive.Format == GsaFormat.NARD && archive.OffsetPadding > 0)
            AlignStream(stream, archive.OffsetPadding);

        // Read raw data
        long dataOffset = stream.Position;
        uint dataSize = isCompressed ? compressedLength : originalLength;

        if (stream.Length - stream.Position < dataSize)
            return null;

        byte[] rawData = reader.ReadBytes((int)dataSize);

        return new GsaEntry
        {
            Alias = alias,
            IsCompressed = isCompressed,
            OriginalLength = originalLength,
            CompressedLength = compressedLength,
            RawData = rawData,
            DataOffset = dataOffset
        };
    }

    /// <summary>
    /// Advances the stream position to the next aligned boundary.
    /// </summary>
    /// <param name="stream">The stream to align.</param>
    /// <param name="alignment">The alignment boundary in bytes.</param>
    private static void AlignStream(Stream stream, int alignment)
    {
        if (alignment <= 1) return;
        long pos = stream.Position;
        long remainder = pos % alignment;
        if (remainder != 0)
        {
            stream.Position = pos + (alignment - remainder);
        }
    }
}
