using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Data.Extensions;
using SabreTools.Data.Models.InstallShieldCabinet;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
using SabreTools.Text.Extensions;
using static SabreTools.Data.Models.InstallShieldCabinet.Constants;

#pragma warning disable IDE0017 // Simplify object initialization
namespace SabreTools.Serialization.Readers
{
    public class InstallShieldCabinet : BaseBinaryReader<Cabinet>
    {
        /// <inheritdoc/>
        public override Cabinet? Deserialize(Stream? data)
        {
            // If the data is invalid
            if (data is null || !data.CanRead)
                return null;

            try
            {
                // Cache the current offset
                long initialOffset = data.Position;

                // Create a new cabinet to fill
                var cabinet = new Cabinet();

                #region Common Header

                // Try to parse the cabinet header
                var commonHeader = ParseCommonHeader(data);
                if (commonHeader.Signature != SignatureString)
                    return null;

                // Set the cabinet header
                cabinet.CommonHeader = commonHeader;

                #endregion

                // Get the major version
                int majorVersion = commonHeader.GetMajorVersion();

                #region Volume Header

                // Set the volume header
                cabinet.VolumeHeader = ParseVolumeHeader(data, majorVersion);

                #endregion

                #region Descriptor

                // If the descriptor does not exist
                if (commonHeader.DescriptorSize == 0)
                    return cabinet;

                // Get the descriptor offset
                long descriptorOffset = initialOffset + commonHeader.DescriptorOffset;
                if (descriptorOffset < initialOffset || descriptorOffset >= data.Length)
                    return null;

                // Seek to the descriptor
                data.SeekIfPossible(descriptorOffset, SeekOrigin.Begin);

                // Set the descriptor
                cabinet.Descriptor = ParseDescriptor(data);

                #endregion

                // TODO: Read the setup types table (SETUPTYPEHEADER and SETUPTYPEDESC)

                #region File Descriptor Offsets

                // Get the file table offset
                long fileTableOffset = descriptorOffset + cabinet.Descriptor.FileTableOffset;
                if (fileTableOffset < initialOffset || fileTableOffset >= data.Length)
                    return null;

                // Seek to the file table
                data.SeekIfPossible(fileTableOffset, SeekOrigin.Begin);

                // Get the number of file table items
                uint fileTableItems;
                if (majorVersion <= 5)
                    fileTableItems = cabinet.Descriptor.DirectoryCount + cabinet.Descriptor.FileCount;
                else
                    fileTableItems = cabinet.Descriptor.DirectoryCount;

                // Create and fill the file table
                cabinet.FileDescriptorOffsets = new uint[fileTableItems];
                for (int i = 0; i < cabinet.FileDescriptorOffsets.Length; i++)
                {
                    cabinet.FileDescriptorOffsets[i] = data.ReadUInt32LittleEndian();
                }

                #endregion

                #region Directory Descriptors

                // Create and fill the directory descriptors
                cabinet.DirectoryNames = new string[cabinet.Descriptor.DirectoryCount];
                for (int i = 0; i < cabinet.Descriptor.DirectoryCount; i++)
                {
                    // Get the directory descriptor offset
                    long offset = descriptorOffset
                        + cabinet.Descriptor.FileTableOffset
                        + cabinet.FileDescriptorOffsets[i];

                    // If we have an invalid offset
                    if (offset < initialOffset || offset >= data.Length)
                        continue;

                    // Seek to the file descriptor offset
                    data.SeekIfPossible(offset, SeekOrigin.Begin);

                    // Create and add the file descriptor
                    string? directoryName = ParseDirectoryName(data, majorVersion);
                    if (directoryName is not null)
                        cabinet.DirectoryNames[i] = directoryName;
                }

                #endregion

                #region File Descriptors

                // Create and fill the file descriptors
                cabinet.FileDescriptors = new FileDescriptor[cabinet.Descriptor.FileCount];
                for (int i = 0; i < cabinet.Descriptor.FileCount; i++)
                {
                    // Get the file descriptor offset
                    long offset;
                    if (majorVersion <= 5)
                    {
                        offset = descriptorOffset
                            + cabinet.Descriptor.FileTableOffset
                            + cabinet.FileDescriptorOffsets[cabinet.Descriptor.DirectoryCount + i];
                    }
                    else
                    {
                        offset = descriptorOffset
                            + cabinet.Descriptor.FileTableOffset
                            + cabinet.Descriptor.FileTableOffset2
                            + (uint)(i * 0x57);
                    }

                    // If we have an invalid offset
                    if (offset < initialOffset || offset >= data.Length)
                        continue;

                    // Seek to the file descriptor offset
                    data.SeekIfPossible(offset, SeekOrigin.Begin);

                    // Create and add the file descriptor
                    cabinet.FileDescriptors[i] = ParseFileDescriptor(data,
                        majorVersion,
                        descriptorOffset + cabinet.Descriptor.FileTableOffset);
                }

                #endregion

                #region File Group Offsets

                // Create and fill the file group offsets
                cabinet.FileGroupOffsets = [];
                for (int i = 0; i < cabinet.Descriptor.FileGroupOffsets.Length; i++)
                {
                    // Get the file group offset
                    long offset = cabinet.Descriptor.FileGroupOffsets![i];
                    if (offset == 0)
                        continue;

                    // Adjust the file group offset
                    offset += descriptorOffset;
                    if (offset < initialOffset || offset >= data.Length)
                        continue;

                    // Seek to the file group offset
                    data.SeekIfPossible(offset, SeekOrigin.Begin);

                    // Create and add the offset
                    OffsetList offsetList = ParseOffsetList(data, majorVersion, descriptorOffset);
                    cabinet.FileGroupOffsets[offset] = offsetList;

                    // If we have a nonzero next offset
                    long nextOffset = offsetList.NextOffset;
                    while (nextOffset != 0)
                    {
                        // Get the next offset to read
                        long internalOffset = descriptorOffset + nextOffset;

                        // Seek to the file group offset
                        data.SeekIfPossible(internalOffset, SeekOrigin.Begin);

                        // Create and add the offset
                        offsetList = ParseOffsetList(data, majorVersion, descriptorOffset);
                        cabinet.FileGroupOffsets[nextOffset] = offsetList;

                        // Set the next offset
                        nextOffset = offsetList.NextOffset;
                    }
                }

                #endregion

                #region File Groups

                // Create the file groups array
                cabinet.FileGroups = new FileGroup[cabinet.FileGroupOffsets.Count];

                // Create and fill the file groups
                int fileGroupId = 0;
                foreach (var kvp in cabinet.FileGroupOffsets)
                {
                    // Get the offset
                    OffsetList? list = kvp.Value;
                    if (list is null)
                        continue;

                    // If we have an invalid offset
                    if (list.DescriptorOffset <= 0)
                        continue;

                    /// Seek to the file group
                    data.SeekIfPossible(descriptorOffset + list.DescriptorOffset, SeekOrigin.Begin);

                    // Add the file group
                    cabinet.FileGroups[fileGroupId++] = ParseFileGroup(data, majorVersion, descriptorOffset);
                }

                // Order the groups by first file index
                Array.Sort(cabinet.FileGroups, (a, b) => (int)a.FirstFile - (int)b.FirstFile);

                #endregion

                #region Component Offsets

                // Create and fill the component offsets
                cabinet.ComponentOffsets = [];
                for (int i = 0; i < cabinet.Descriptor.ComponentOffsets.Length; i++)
                {
                    // Get the component offset
                    long offset = cabinet.Descriptor.ComponentOffsets![i];
                    if (offset == 0)
                        continue;

                    // Adjust the component offset
                    offset += descriptorOffset;
                    if (offset < initialOffset || offset >= data.Length)
                        continue;

                    // Seek to the component offset
                    data.SeekIfPossible(offset, SeekOrigin.Begin);

                    // Create and add the offset
                    OffsetList offsetList = ParseOffsetList(data, majorVersion, descriptorOffset);
                    cabinet.ComponentOffsets[cabinet.Descriptor.ComponentOffsets[i]] = offsetList;

                    // If we have a nonzero next offset
                    long nextOffset = offsetList.NextOffset;
                    while (nextOffset != 0)
                    {
                        // Get the next offset to read
                        long internalOffset = descriptorOffset + nextOffset;

                        // Seek to the file group offset
                        data.SeekIfPossible(internalOffset, SeekOrigin.Begin);

                        // Create and add the offset
                        offsetList = ParseOffsetList(data, majorVersion, descriptorOffset);
                        cabinet.ComponentOffsets[nextOffset] = offsetList;

                        // Set the next offset
                        nextOffset = offsetList.NextOffset;
                    }
                }

                #endregion

                #region Components

                // Create the components array
                cabinet.Components = new Component[cabinet.ComponentOffsets.Count];

                // Create and fill the components
                int componentId = 0;
                foreach (KeyValuePair<long, OffsetList?> kvp in cabinet.ComponentOffsets)
                {
                    // Get the offset
                    OffsetList? list = kvp.Value;
                    if (list is null)
                        continue;

                    // If we have an invalid offset
                    if (list.DescriptorOffset <= 0)
                        continue;

                    // Seek to the component
                    data.SeekIfPossible(descriptorOffset + list.DescriptorOffset, SeekOrigin.Begin);

                    // Add the component
                    cabinet.Components[componentId++] = ParseComponent(data, majorVersion, descriptorOffset);
                }

                #endregion

                return cabinet;
            }
            catch
            {
                // Ignore the actual error
                return null;
            }
        }

        /// <summary>
        /// Parse a Stream into a CommonHeader
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled CommonHeader on success, null on error</returns>
        public static CommonHeader ParseCommonHeader(Stream data)
        {
            var obj = new CommonHeader();

            byte[] signature = data.ReadBytes(4);
            obj.Signature = Encoding.ASCII.GetString(signature);
            obj.Version = data.ReadUInt32LittleEndian();
            obj.VolumeInfo = data.ReadUInt32LittleEndian();
            obj.DescriptorOffset = data.ReadUInt32LittleEndian();
            obj.DescriptorSize = data.ReadUInt32LittleEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a Component
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="majorVersion">Major version of the cabinet</param>
        /// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
        /// <returns>Filled Component on success, null on error</returns>
        public static Component ParseComponent(Stream data, int majorVersion, long descriptorOffset)
        {
            var obj = new Component();

            obj.NameOffset = data.ReadUInt32LittleEndian();
            obj.DescriptionOffset = data.ReadUInt32LittleEndian();
            obj.StatusTextOffset = data.ReadUInt32LittleEndian();
            obj.Status = (ComponentStatus)data.ReadUInt16LittleEndian();
            obj.PasswordOffset = data.ReadUInt32LittleEndian();
            obj.MiscOffset = data.ReadUInt32LittleEndian();
            obj.ComponentIndex = data.ReadUInt16LittleEndian();
            obj.DisplayNameOffset = data.ReadUInt32LittleEndian();
            obj.CDRomFolderOffset = data.ReadUInt32LittleEndian();
            obj.HTTPLocationOffset = data.ReadUInt32LittleEndian();
            obj.FTPLocationOffset = data.ReadUInt32LittleEndian();
            obj.Guid = new Guid[2];
            for (int i = 0; i < obj.Guid.Length; i++)
            {
                obj.Guid[i] = data.ReadGuid();
            }

            obj.CLSIDOffset = data.ReadUInt32LittleEndian();
            obj.Reserved2 = data.ReadBytes(28);
            obj.Reserved3 = data.ReadBytes(majorVersion <= 5 ? 2 : 1);
            obj.DependsCount = data.ReadUInt16LittleEndian();
            obj.DependsOffset = data.ReadUInt32LittleEndian();
            obj.FileGroupCount = data.ReadUInt16LittleEndian();
            obj.FileGroupNamesOffset = data.ReadUInt32LittleEndian();
            obj.X3Count = data.ReadUInt16LittleEndian();
            obj.X3Offset = data.ReadUInt32LittleEndian();
            obj.SubComponentsCount = data.ReadUInt16LittleEndian();
            obj.SubComponentsOffset = data.ReadUInt32LittleEndian();
            obj.NextComponentOffset = data.ReadUInt32LittleEndian();
            obj.OnInstallingOffset = data.ReadUInt32LittleEndian();
            obj.OnInstalledOffset = data.ReadUInt32LittleEndian();
            obj.OnUninstallingOffset = data.ReadUInt32LittleEndian();
            obj.OnUninstalledOffset = data.ReadUInt32LittleEndian();

            // Cache the current position
            long currentPosition = data.Position;

            // Read the name, if possible
            if (obj.NameOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.NameOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.Name = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.Name = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the description, if possible
            if (obj.DescriptionOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.DescriptionOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.Description = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.Description = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the status text, if possible
            if (obj.StatusTextOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.StatusTextOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.StatusText = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.StatusText = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the display name, if possible
            if (obj.DisplayNameOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.DisplayNameOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.DisplayName = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.DisplayName = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the CD-ROM folder, if possible
            if (obj.CDRomFolderOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.CDRomFolderOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.CDRomFolder = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.CDRomFolder = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the HTTP location, if possible
            if (obj.HTTPLocationOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.HTTPLocationOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.HTTPLocation = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.HTTPLocation = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the FTP location, if possible
            if (obj.FTPLocationOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.FTPLocationOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.FTPLocation = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.FTPLocation = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the CLSID, if possible
            if (obj.CLSIDOffset != 0)
            {
                // Seek to the CLSID
                data.SeekIfPossible(descriptorOffset + obj.CLSIDOffset, SeekOrigin.Begin);

                // Read the GUID
                obj.CLSID = data.ReadGuid();
            }

            // Read the file group names, if possible
            if (obj.FileGroupCount != 0 && obj.FileGroupNamesOffset != 0)
            {
                // Seek to the file group table offset
                data.SeekIfPossible(descriptorOffset + obj.FileGroupNamesOffset, SeekOrigin.Begin);

                // Read the file group names table
                obj.FileGroupNames = new string[obj.FileGroupCount];
                for (int j = 0; j < obj.FileGroupCount; j++)
                {
                    // Get the name offset
                    uint nameOffset = data.ReadUInt32LittleEndian();

                    // Cache the current offset
                    long preNameOffset = data.Position;

                    // Seek to the string offset
                    data.SeekIfPossible(descriptorOffset + nameOffset, SeekOrigin.Begin);

                    if (majorVersion >= 17)
                        obj.FileGroupNames[j] = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                    else
                        obj.FileGroupNames[j] = data.ReadNullTerminatedAnsiString() ?? string.Empty;

                    // Seek back to the original position
                    data.SeekIfPossible(preNameOffset, SeekOrigin.Begin);
                }
            }

            // Seek back to the correct offset
            data.SeekIfPossible(currentPosition, SeekOrigin.Begin);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a Descriptor
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled Descriptor on success, null on error</returns>
        public static Descriptor ParseDescriptor(Stream data)
        {
            var obj = new Descriptor();

            obj.StringsOffset = data.ReadUInt32LittleEndian();
            obj.Reserved0 = data.ReadUInt32LittleEndian();
            obj.ComponentListOffset = data.ReadUInt32LittleEndian();
            obj.FileTableOffset = data.ReadUInt32LittleEndian();
            obj.Reserved1 = data.ReadUInt32LittleEndian();
            obj.FileTableSize = data.ReadUInt32LittleEndian();
            obj.FileTableSize2 = data.ReadUInt32LittleEndian();
            obj.DirectoryCount = data.ReadUInt16LittleEndian();
            obj.Reserved2 = data.ReadUInt32LittleEndian();
            obj.Reserved3 = data.ReadUInt16LittleEndian();
            obj.Reserved4 = data.ReadUInt32LittleEndian();
            obj.FileCount = data.ReadUInt32LittleEndian();
            obj.FileTableOffset2 = data.ReadUInt32LittleEndian();
            obj.ComponentTableInfoCount = data.ReadUInt16LittleEndian();
            obj.ComponentTableOffset = data.ReadUInt32LittleEndian();
            obj.Reserved5 = data.ReadUInt32LittleEndian();
            obj.Reserved6 = data.ReadUInt32LittleEndian();

            obj.FileGroupOffsets = new uint[71];
            for (int i = 0; i < 71; i++)
            {
                obj.FileGroupOffsets[i] = data.ReadUInt32LittleEndian();
            }

            obj.ComponentOffsets = new uint[71];
            for (int i = 0; i < 71; i++)
            {
                obj.ComponentOffsets[i] = data.ReadUInt32LittleEndian();
            }

            obj.SetupTypesOffset = data.ReadUInt32LittleEndian();
            obj.SetupTableOffset = data.ReadUInt32LittleEndian();
            obj.Reserved7 = data.ReadUInt32LittleEndian();
            obj.Reserved8 = data.ReadUInt32LittleEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a directory name
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="majorVersion">Major version of the cabinet</param>
        /// <returns>Filled directory name on success, null on error</returns>
        public static string? ParseDirectoryName(Stream data, int majorVersion)
        {
            // Read the string
            if (majorVersion >= 17)
                return data.ReadNullTerminatedUnicodeString();
            else
                return data.ReadNullTerminatedAnsiString();
        }

        /// <summary>
        /// Parse a Stream into a FileDescriptor
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="majorVersion">Major version of the cabinet</param>
        /// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
        /// <returns>Filled FileDescriptor on success, null on error</returns>
        public static FileDescriptor ParseFileDescriptor(Stream data, int majorVersion, long descriptorOffset)
        {
            var obj = new FileDescriptor();

            // Read the descriptor based on version
            if (majorVersion <= 5)
            {
                obj.Volume = 0xFFFF; // Set by the header index
                obj.NameOffset = data.ReadUInt32LittleEndian();
                obj.DirectoryIndex = data.ReadUInt32LittleEndian();
                obj.Flags = (FileFlags)data.ReadUInt16LittleEndian();
                obj.ExpandedSize = data.ReadUInt32LittleEndian();
                obj.CompressedSize = data.ReadUInt32LittleEndian();
                _ = data.ReadBytes(0x14); // Skip 0x14 bytes, unknown data?
                obj.DataOffset = data.ReadUInt32LittleEndian();

                if (majorVersion == 5)
                    obj.MD5 = data.ReadBytes(0x10);
            }
            else
            {
                obj.Flags = (FileFlags)data.ReadUInt16LittleEndian();
                obj.ExpandedSize = data.ReadUInt64LittleEndian();
                obj.CompressedSize = data.ReadUInt64LittleEndian();
                obj.DataOffset = data.ReadUInt64LittleEndian();
                obj.MD5 = data.ReadBytes(0x10);

                // TODO: This is actually 3 fields
                // - DWORD dwVerMS;
                // - DWORD dwVerLS;
                // - DWORD junk1[2];
                _ = data.ReadBytes(0x10); // Skip 0x10 bytes, unknown data?

                obj.NameOffset = data.ReadUInt32LittleEndian();
                obj.DirectoryIndex = data.ReadUInt16LittleEndian();

                // TODO: This is actually 4 fields
                // - DWORD Attrs;
                // - WORD FatDate
                // - WORD FatTime;
                // - DWORD junk2;
                _ = data.ReadBytes(0x0C); // Skip 0x0C bytes, unknown data?

                obj.LinkPrevious = data.ReadUInt32LittleEndian();
                obj.LinkNext = data.ReadUInt32LittleEndian();
                obj.LinkFlags = (LinkFlags)data.ReadByteValue();
                obj.Volume = data.ReadUInt16LittleEndian();
            }

            // Cache the current position
            long currentPosition = data.Position;

            // Read the name, if possible
            if (obj.NameOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.NameOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.Name = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.Name = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Seek back to the correct offset
            data.SeekIfPossible(currentPosition, SeekOrigin.Begin);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a FileGroup
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="majorVersion">Major version of the cabinet</param>
        /// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
        /// <returns>Filled FileGroup on success, null on error</returns>
        public static FileGroup ParseFileGroup(Stream data, int majorVersion, long descriptorOffset)
        {
            var obj = new FileGroup();

            obj.NameOffset = data.ReadUInt32LittleEndian();

            if (majorVersion <= 5)
            {
                obj.ExpandedSize = data.ReadUInt32LittleEndian();
                obj.CompressedSize = data.ReadUInt32LittleEndian();
            }
            else
            {
                obj.ExpandedSize = data.ReadUInt64LittleEndian();
                obj.CompressedSize = data.ReadUInt64LittleEndian();
            }

            obj.Attributes = (FileGroupAttributes)data.ReadUInt16LittleEndian();

            // TODO: Figure out what data lives in this area for V5 and below
            if (majorVersion <= 5)
                data.SeekIfPossible(0x36, SeekOrigin.Current);

            obj.FirstFile = data.ReadUInt32LittleEndian();
            obj.LastFile = data.ReadUInt32LittleEndian();
            obj.UnknownStringOffset = data.ReadUInt32LittleEndian();
            obj.OperatingSystemOffset = data.ReadUInt32LittleEndian();
            obj.LanguageOffset = data.ReadUInt32LittleEndian();
            obj.HTTPLocationOffset = data.ReadUInt32LittleEndian();
            obj.FTPLocationOffset = data.ReadUInt32LittleEndian();
            obj.MiscOffset = data.ReadUInt32LittleEndian();
            obj.TargetDirectoryOffset = data.ReadUInt32LittleEndian();
            obj.OverwriteFlags = (FileGroupFlags)data.ReadUInt32LittleEndian();
            obj.Reserved = new uint[4];
            for (int i = 0; i < obj.Reserved.Length; i++)
            {
                obj.Reserved[i] = data.ReadUInt32LittleEndian();
            }

            // Cache the current position
            long currentPosition = data.Position;

            // Read the name, if possible
            if (obj.NameOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.NameOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.Name = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.Name = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the language, if possible
            if (obj.LanguageOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.LanguageOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.Language = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.Language = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the HTTP location, if possible
            if (obj.HTTPLocationOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.HTTPLocationOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.HTTPLocation = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.HTTPLocation = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the FTP location, if possible
            if (obj.FTPLocationOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.FTPLocationOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.FTPLocation = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.FTPLocation = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the target directory, if possible
            if (obj.MiscOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.MiscOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.Misc = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.Misc = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Read the target directory, if possible
            if (obj.TargetDirectoryOffset != 0)
            {
                // Seek to the string
                data.SeekIfPossible(descriptorOffset + obj.TargetDirectoryOffset, SeekOrigin.Begin);

                // Read the string
                if (majorVersion >= 17)
                    obj.TargetDirectory = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
                else
                    obj.TargetDirectory = data.ReadNullTerminatedAnsiString() ?? string.Empty;
            }

            // Seek back to the correct offset
            data.SeekIfPossible(currentPosition, SeekOrigin.Begin);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into an OffsetList
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="majorVersion">Major version of the cabinet</param>
        /// <param name="descriptorOffset">Offset of the cabinet descriptor</param>
        /// <returns>Filled OffsetList on success, null on error</returns>
        public static OffsetList ParseOffsetList(Stream data, int majorVersion, long descriptorOffset)
        {
            var obj = new OffsetList();

            obj.NameOffset = data.ReadUInt32LittleEndian();
            obj.DescriptorOffset = data.ReadUInt32LittleEndian();
            obj.NextOffset = data.ReadUInt32LittleEndian();

            // Cache the current offset
            long currentOffset = data.Position;

            // Seek to the string offset
            data.SeekIfPossible(descriptorOffset + obj.NameOffset, SeekOrigin.Begin);

            // Read the string
            if (majorVersion >= 17)
                obj.Name = data.ReadNullTerminatedUnicodeString() ?? string.Empty;
            else
                obj.Name = data.ReadNullTerminatedAnsiString() ?? string.Empty;

            // Seek back to the correct offset
            data.SeekIfPossible(currentOffset, SeekOrigin.Begin);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a VolumeHeader
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="majorVersion">Major version of the cabinet</param>
        /// <returns>Filled VolumeHeader on success, null on error</returns>
        public static VolumeHeader ParseVolumeHeader(Stream data, int majorVersion)
        {
            var obj = new VolumeHeader();

            // Read the descriptor based on version
            if (majorVersion <= 5)
            {
                obj.DataOffset = data.ReadUInt32LittleEndian();
                _ = data.ReadBytes(0x04); // Skip 0x04 bytes, unknown data?
                obj.FirstFileIndex = data.ReadUInt32LittleEndian();
                obj.LastFileIndex = data.ReadUInt32LittleEndian();
                obj.FirstFileOffset = data.ReadUInt32LittleEndian();
                obj.FirstFileSizeExpanded = data.ReadUInt32LittleEndian();
                obj.FirstFileSizeCompressed = data.ReadUInt32LittleEndian();
                obj.LastFileOffset = data.ReadUInt32LittleEndian();
                obj.LastFileSizeExpanded = data.ReadUInt32LittleEndian();
                obj.LastFileSizeCompressed = data.ReadUInt32LittleEndian();
            }
            else
            {
                obj.DataOffset = data.ReadUInt64LittleEndian();
                obj.FirstFileIndex = data.ReadUInt32LittleEndian();
                obj.LastFileIndex = data.ReadUInt32LittleEndian();
                obj.FirstFileOffset = data.ReadUInt64LittleEndian();
                obj.FirstFileSizeExpanded = data.ReadUInt64LittleEndian();
                obj.FirstFileSizeCompressed = data.ReadUInt64LittleEndian();
                obj.LastFileOffset = data.ReadUInt64LittleEndian();
                obj.LastFileSizeExpanded = data.ReadUInt64LittleEndian();
                obj.LastFileSizeCompressed = data.ReadUInt64LittleEndian();
            }

            return obj;
        }
    }
}
