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

#pragma warning disable IDE0017 // Simplify object initialization
namespace SabreTools.Serialization.Readers
{
    public class PKZIP : BaseBinaryReader<Archive>
    {
        /// <inheritdoc/>
        public override Archive? 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;

                var archive = new Archive();

                // Setup all of the collections
                var localFiles = new List<LocalFile>();
                var cdrs = new List<CentralDirectoryFileHeader>();

                // Read all blocks
                do
                {
                    // Read the signature
                    long beforeSignature = data.Position;
                    uint signature = data.ReadUInt32LittleEndian();
                    data.SeekIfPossible(beforeSignature, SeekOrigin.Begin);

                    // Switch based on the signature found
                    bool validBlock = false;
                    switch (signature)
                    {
                        // Central Directory File Header
                        case CentralDirectoryFileHeaderSignature:
                            var cdr = ParseCentralDirectoryFileHeader(data, Debug);
                            if (cdr is null)
                                return null;

                            // Add the central directory record
                            validBlock = true;
                            cdrs.Add(cdr);
                            break;

                        // Local File
                        case LocalFileHeaderSignature:
                            var lf = ParseLocalFile(data, Debug);
                            if (lf is null)
                                return null;

                            // Add the local file
                            validBlock = true;
                            localFiles.Add(lf);
                            break;

                        // TODO: Implement
                        case DigitalSignatureSignature:
                            break;

                        // End of Central Directory Record
                        case EndOfCentralDirectoryRecordSignature:
                            var eocdr = ParseEndOfCentralDirectoryRecord(data);
                            if (eocdr is null)
                                return null;

                            // Assign the end of central directory record
                            validBlock = true;
                            archive.EndOfCentralDirectoryRecord = eocdr;
                            break;

                        // ZIP64 End of Central Directory
                        case EndOfCentralDirectoryRecord64Signature:
                            var eocdr64 = ParseEndOfCentralDirectoryRecord64(data);
                            if (eocdr64 is null)
                                return null;

                            // Assign the ZIP64 end of central directory record
                            validBlock = true;
                            archive.ZIP64EndOfCentralDirectoryRecord = eocdr64;
                            break;

                        // ZIP64 End of Central Directory Locator
                        case EndOfCentralDirectoryLocator64Signature:
                            var eocdl64 = ParseEndOfCentralDirectoryLocator64(data);
                            if (eocdl64 is null)
                                return null;

                            // Assign the ZIP64 end of central directory record
                            validBlock = true;
                            archive.ZIP64EndOfCentralDirectoryLocator = eocdl64;
                            break;

                        // Archive Extra Data Record
                        case ArchiveExtraDataRecordSignature:
                            var aedr = ParseArchiveExtraDataRecord(data);
                            if (aedr is null)
                                return null;

                            // Assign the archive extra data record
                            validBlock = true;
                            archive.ArchiveExtraDataRecord = aedr;
                            break;

                        default:
                            // TODO: Log invalid values
                            break;
                    }

                    // If there was an invalid block
                    if (!validBlock)
                        break;

                } while (data.Position < data.Length);

                // If no blocks were read
                if (localFiles.Count == 0 && cdrs.Count == 0)
                    return null;

                // Assign the local files
                archive.LocalFiles = [.. localFiles];

                // Assign the central directory records
                archive.CentralDirectoryHeaders = [.. cdrs];

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

        /// <summary>
        /// Parse a Stream into an archive extra data record
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled archive extra data record on success, null on error</returns>
        public static ArchiveExtraDataRecord? ParseArchiveExtraDataRecord(Stream data)
        {
            var obj = new ArchiveExtraDataRecord();

            obj.Signature = data.ReadUInt32LittleEndian();
            if (obj.Signature != ArchiveExtraDataRecordSignature)
                return null;

            obj.ExtraFieldLength = data.ReadUInt32LittleEndian();
            if (obj.ExtraFieldLength > 0 && data.Position + obj.ExtraFieldLength <= data.Length)
            {
                byte[] extraBytes = data.ReadBytes((int)obj.ExtraFieldLength);
                if (extraBytes.Length != obj.ExtraFieldLength)
                    return null;

                obj.ExtraFieldData = extraBytes;
            }

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a central directory file header
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="includeDebug">True to include debug data, false otherwise</param>
        /// <returns>Filled central directory file header on success, null on error</returns>
        public static CentralDirectoryFileHeader? ParseCentralDirectoryFileHeader(Stream data, bool includeDebug = false)
        {
            var obj = new CentralDirectoryFileHeader();

            obj.Signature = data.ReadUInt32LittleEndian();
            if (obj.Signature != CentralDirectoryFileHeaderSignature)
                return null;

            obj.HostSystem = (HostSystem)data.ReadByteValue();
            obj.VersionMadeBy = data.ReadByteValue();
            obj.VersionNeededToExtract = data.ReadUInt16LittleEndian();
            obj.Flags = (GeneralPurposeBitFlags)data.ReadUInt16LittleEndian();
            obj.CompressionMethod = (CompressionMethod)data.ReadUInt16LittleEndian();
            obj.LastModifedFileTime = data.ReadUInt16LittleEndian();
            obj.LastModifiedFileDate = data.ReadUInt16LittleEndian();
            obj.CRC32 = data.ReadUInt32LittleEndian();
            obj.CompressedSize = data.ReadUInt32LittleEndian();
            obj.UncompressedSize = data.ReadUInt32LittleEndian();
            obj.FileNameLength = data.ReadUInt16LittleEndian();
            obj.ExtraFieldLength = data.ReadUInt16LittleEndian();
            obj.FileCommentLength = data.ReadUInt16LittleEndian();
            obj.DiskNumberStart = data.ReadUInt16LittleEndian();
            obj.InternalFileAttributes = (InternalFileAttributes)data.ReadUInt16LittleEndian();
            obj.ExternalFileAttributes = data.ReadUInt32LittleEndian();
            obj.RelativeOffsetOfLocalHeader = data.ReadUInt32LittleEndian();

#if NET20 || NET35
            bool utf8 = (obj.Flags & GeneralPurposeBitFlags.LanguageEncodingFlag) != 0;
#else
            bool utf8 = obj.Flags.HasFlag(GeneralPurposeBitFlags.LanguageEncodingFlag);
#endif

            if (obj.FileNameLength > 0 && data.Position + obj.FileNameLength <= data.Length)
            {
                byte[] filenameBytes = data.ReadBytes(obj.FileNameLength);
                if (filenameBytes.Length != obj.FileNameLength)
                    return null;

                if (utf8)
                    obj.FileName = Encoding.UTF8.GetString(filenameBytes);
                else
                    obj.FileName = Encoding.ASCII.GetString(filenameBytes);
            }

            if (obj.ExtraFieldLength > 0 && data.Position + obj.ExtraFieldLength <= data.Length)
            {
                byte[] extraBytes = data.ReadBytes(obj.ExtraFieldLength);
                if (extraBytes.Length != obj.ExtraFieldLength)
                    return null;

                obj.ExtraFields = ParseExtraFields(obj, extraBytes, includeDebug);
            }

            if (obj.FileCommentLength > 0 && data.Position + obj.FileCommentLength <= data.Length)
            {
                byte[] commentBytes = data.ReadBytes(obj.FileCommentLength);
                if (commentBytes.Length != obj.FileCommentLength)
                    return null;

                if (utf8)
                    obj.FileComment = Encoding.UTF8.GetString(commentBytes);
                else
                    obj.FileComment = Encoding.ASCII.GetString(commentBytes);
            }

            return obj;
        }

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

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

            // Check if the block is a 12- or 16-byte
            bool isShort = false;
            if (data.Position + 12 == data.Length)
            {
                isShort = true;
            }
            else
            {
                data.SeekIfPossible(12, SeekOrigin.Current);
                byte[] nextBlock = data.ReadBytes(2);
                data.SeekIfPossible(currentPosition, SeekOrigin.Begin);
                if (nextBlock.EqualsExactly([0x50, 0x4B]))
                    isShort = true;
            }

            // If the 16-byte variant
            if (!isShort)
            {
                obj.Signature = data.ReadUInt32LittleEndian();
                if (obj.Signature != DataDescriptorSignature)
                    return null;
            }

            obj.CRC32 = data.ReadUInt32LittleEndian();
            obj.CompressedSize = data.ReadUInt32LittleEndian();
            obj.UncompressedSize = data.ReadUInt32LittleEndian();

            return obj;
        }

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

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

            // Check if the block is a 20- or 24-byte
            bool isShort = false;
            if (data.Position + 20 == data.Length)
            {
                isShort = true;
            }
            else
            {
                data.SeekIfPossible(20, SeekOrigin.Current);
                byte[] nextBlock = data.ReadBytes(2);
                data.SeekIfPossible(currentPosition, SeekOrigin.Begin);
                if (nextBlock.EqualsExactly([0x50, 0x4B]))
                    isShort = true;
            }

            // If the 24-byte variant
            if (!isShort)
            {
                obj.Signature = data.ReadUInt32LittleEndian();
                if (obj.Signature != DataDescriptorSignature)
                    return null;
            }

            obj.CRC32 = data.ReadUInt32LittleEndian();
            obj.CompressedSize = data.ReadUInt64LittleEndian();
            obj.UncompressedSize = data.ReadUInt64LittleEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a ZIP64 end of central directory locator
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled ZIP64 end of central directory locator on success, null on error</returns>
        public static EndOfCentralDirectoryLocator64? ParseEndOfCentralDirectoryLocator64(Stream data)
        {
            var obj = new EndOfCentralDirectoryLocator64();

            // Signatures are expected but not required
            obj.Signature = data.ReadUInt32LittleEndian();
            if (obj.Signature != EndOfCentralDirectoryLocator64Signature)
                data.SeekIfPossible(-4, SeekOrigin.Current);

            obj.StartDiskNumber = data.ReadUInt32LittleEndian();
            obj.CentralDirectoryOffset = data.ReadUInt64LittleEndian();
            obj.TotalDisks = data.ReadUInt32LittleEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into an end of central directory record
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled end of central directory record on success, null on error</returns>
        public static EndOfCentralDirectoryRecord? ParseEndOfCentralDirectoryRecord(Stream data)
        {
            var obj = new EndOfCentralDirectoryRecord();

            obj.Signature = data.ReadUInt32LittleEndian();
            if (obj.Signature != EndOfCentralDirectoryRecordSignature)
                return null;

            obj.DiskNumber = data.ReadUInt16LittleEndian();
            obj.StartDiskNumber = data.ReadUInt16LittleEndian();
            obj.TotalEntriesOnDisk = data.ReadUInt16LittleEndian();
            obj.TotalEntries = data.ReadUInt16LittleEndian();
            obj.CentralDirectorySize = data.ReadUInt32LittleEndian();
            obj.CentralDirectoryOffset = data.ReadUInt32LittleEndian();
            obj.FileCommentLength = data.ReadUInt16LittleEndian();
            if (obj.FileCommentLength > 0 && data.Position + obj.FileCommentLength <= data.Length)
            {
                byte[] commentBytes = data.ReadBytes(obj.FileCommentLength);
                if (commentBytes.Length != obj.FileCommentLength)
                    return null;

                obj.FileComment = Encoding.ASCII.GetString(commentBytes);
            }

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a ZIP64 end of central directory record
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled ZIP64 end of central directory record on success, null on error</returns>
        public static EndOfCentralDirectoryRecord64? ParseEndOfCentralDirectoryRecord64(Stream data)
        {
            var obj = new EndOfCentralDirectoryRecord64();

            obj.Signature = data.ReadUInt32LittleEndian();
            if (obj.Signature != EndOfCentralDirectoryRecord64Signature)
                return null;

            obj.DirectoryRecordSize = data.ReadUInt64LittleEndian();
            obj.HostSystem = (HostSystem)data.ReadByteValue();
            obj.VersionMadeBy = data.ReadByteValue();
            obj.VersionNeededToExtract = data.ReadUInt16LittleEndian();
            obj.DiskNumber = data.ReadUInt32LittleEndian();
            obj.StartDiskNumber = data.ReadUInt32LittleEndian();
            obj.TotalEntriesOnDisk = data.ReadUInt64LittleEndian();
            obj.TotalEntries = data.ReadUInt64LittleEndian();
            obj.CentralDirectorySize = data.ReadUInt64LittleEndian();
            obj.CentralDirectoryOffset = data.ReadUInt64LittleEndian();

            // TODO: Handle the ExtensibleDataSector -- How to detect if exists?

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a local file
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="includeDebug">True to include debug data, false otherwise</param>
        /// <returns>Filled local file on success, null on error</returns>
        public static LocalFile? ParseLocalFile(Stream data, bool includeDebug = false)
        {
            var obj = new LocalFile();

            #region Local File Header

            // Try to read the header
            var localFileHeader = ParseLocalFileHeader(data, includeDebug);
            if (localFileHeader is null)
                return null;

            // Assign the header
            obj.LocalFileHeader = localFileHeader;

            ulong compressedSize = localFileHeader.CompressedSize;
            if (localFileHeader.ExtraFields is not null)
            {
                foreach (var field in localFileHeader.ExtraFields)
                {
                    if (field is not Zip64ExtendedInformationExtraField infoField)
                        continue;
                    if (infoField.CompressedSize is null)
                        continue;

                    compressedSize = infoField.CompressedSize.Value;
                }
            }

            #endregion

            #region Encryption Header

            // Only read the encryption header if necessary
#if NET20 || NET35
            if ((localFileHeader.Flags & GeneralPurposeBitFlags.FileEncrypted) != 0)
#else
            if (localFileHeader.Flags.HasFlag(GeneralPurposeBitFlags.FileEncrypted))
#endif
            {
                // Try to read the encryption header data -- TODO: Verify amount to read
                byte[] encryptionHeaders = data.ReadBytes(12);
                if (encryptionHeaders.Length != 12)
                    return null;

                // Set the encryption headers
                obj.EncryptionHeaders = encryptionHeaders;
            }
            else
            {
                // Add the empty encryption header
                obj.EncryptionHeaders = [];
            }

            #endregion

            #region File Data

            // Try to read the file data
            var fileData = data.ReadBytes((int)compressedSize);
            if (fileData.Length < (long)compressedSize)
                return null;

            // Set the file data
            obj.FileData = fileData;

            #endregion

            #region Data Descriptor

            // Only attempt to read the descriptor if necessary
#if NET20 || NET35
            if ((localFileHeader.Flags & GeneralPurposeBitFlags.NoCRC) == 0)
#else
            if (!localFileHeader.Flags.HasFlag(GeneralPurposeBitFlags.NoCRC))
#endif
            {
                obj.DataDescriptor = new DataDescriptor();
                obj.ZIP64DataDescriptor = new DataDescriptor64();
                return obj;
            }

            // Read the signature
            long beforeSignature = data.Position;
            uint signature = data.ReadUInt32LittleEndian();
            data.SeekIfPossible(beforeSignature, SeekOrigin.Begin);

            // Don't fail if descriptor is missing
            if (signature != DataDescriptorSignature)
            {
                obj.DataDescriptor = new DataDescriptor();
                obj.ZIP64DataDescriptor = new DataDescriptor64();
                return obj;
            }

            // Determine if the entry is ZIP64
            bool zip64 = IsZip64Descriptor(data);

            // Try to parse the correct data descriptor
            if (zip64)
            {
                obj.DataDescriptor = new DataDescriptor();
                obj.ZIP64DataDescriptor = ParseDataDescriptor64(data);
                if (obj.ZIP64DataDescriptor is null)
                    return null;
            }
            else
            {
                obj.DataDescriptor = ParseDataDescriptor(data);
                obj.ZIP64DataDescriptor = new DataDescriptor64();
                if (obj.DataDescriptor is null)
                    return null;
            }

            #endregion

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a local file header
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="includeDebug">True to include debug data, false otherwise</param>
        /// <returns>Filled local file header on success, null on error</returns>
        public static LocalFileHeader? ParseLocalFileHeader(Stream data, bool includeDebug = false)
        {
            var obj = new LocalFileHeader();

            obj.Signature = data.ReadUInt32LittleEndian();
            if (obj.Signature != LocalFileHeaderSignature)
                return null;

            obj.Version = data.ReadUInt16LittleEndian();
            obj.Flags = (GeneralPurposeBitFlags)data.ReadUInt16LittleEndian();
            obj.CompressionMethod = (CompressionMethod)data.ReadUInt16LittleEndian();
            obj.LastModifedFileTime = data.ReadUInt16LittleEndian();
            obj.LastModifiedFileDate = data.ReadUInt16LittleEndian();
            obj.CRC32 = data.ReadUInt32LittleEndian();
            obj.CompressedSize = data.ReadUInt32LittleEndian();
            obj.UncompressedSize = data.ReadUInt32LittleEndian();
            obj.FileNameLength = data.ReadUInt16LittleEndian();
            obj.ExtraFieldLength = data.ReadUInt16LittleEndian();

#if NET20 || NET35
            bool utf8 = (obj.Flags & GeneralPurposeBitFlags.LanguageEncodingFlag) != 0;
#else
            bool utf8 = obj.Flags.HasFlag(GeneralPurposeBitFlags.LanguageEncodingFlag);
#endif

            if (obj.FileNameLength > 0 && data.Position + obj.FileNameLength <= data.Length)
            {
                byte[] filenameBytes = data.ReadBytes(obj.FileNameLength);
                if (filenameBytes.Length != obj.FileNameLength)
                    return null;

                if (utf8)
                    obj.FileName = Encoding.UTF8.GetString(filenameBytes);
                else
                    obj.FileName = Encoding.ASCII.GetString(filenameBytes);
            }

            if (obj.ExtraFieldLength > 0 && data.Position + obj.ExtraFieldLength <= data.Length)
            {
                byte[] extraBytes = data.ReadBytes(obj.ExtraFieldLength);
                if (extraBytes.Length != obj.ExtraFieldLength)
                    return null;

                obj.ExtraFields = ParseExtraFields(obj, extraBytes, includeDebug);
            }

            return obj;
        }

        #region Extras Fields

        /// <summary>
        /// Process all extensible data fields in a central directory file extras block
        /// </summary>
        /// <param name="header">Central directory file header</param>
        /// <param name="data">Byte array to parse</param>
        /// <param name="includeDebug">True to include debug data, false otherwise</param>
        /// <returns>Array of data fields on success, null otherwise</returns>
        public static ExtensibleDataField[]? ParseExtraFields(CentralDirectoryFileHeader header, byte[]? data, bool includeDebug = false)
        {
            if (data is null)
                return null;

            List<ExtensibleDataField> fields = [];

            int offset = 0;
            while (offset < data.Length)
            {
                // Peek at the next header ID
                HeaderID id = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
                offset -= 2;

                // Read based on the ID
                ExtensibleDataField? field = id switch
                {
                    HeaderID.Zip64ExtendedInformation => ParseZip64ExtendedInformationExtraField(data, ref offset, header),
                    HeaderID.AVInfo => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement model
                    HeaderID.ExtendedLanguageEncodingData => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement model
                    HeaderID.OS2 => ParseOS2ExtraField(data, ref offset),
                    HeaderID.NTFS => ParseNTFSExtraField(data, ref offset),
                    HeaderID.OpenVMS => ParseOpenVMSExtraField(data, ref offset),
                    HeaderID.UNIX => ParseUnixExtraField(data, ref offset),
                    HeaderID.FileStreamFork => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.PatchDescriptor => ParsePatchDescriptorExtraField(data, ref offset),
                    HeaderID.PKCSStore => ParsePKCS7Store(data, ref offset),
                    HeaderID.X509IndividualFile => ParseX509IndividualFile(data, ref offset),
                    HeaderID.X509CentralDirectory => ParseX509CentralDirectory(data, ref offset),
                    HeaderID.StrongEncryptionHeader => ParseStrongEncryptionHeader(data, ref offset),
                    HeaderID.RecordManagementControls => ParseRecordManagementControls(data, ref offset),
                    HeaderID.PKCSCertificateList => ParsePKCS7EncryptionRecipientCertificateList(data, ref offset),
                    HeaderID.Timestamp => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.PolicyDecryptionKey => ParsePolicyDecryptionKeyRecordExtraField(data, ref offset),
                    HeaderID.SmartcryptKeyProvider => ParseKeyProviderRecordExtraField(data, ref offset),
                    HeaderID.SmartcryptPolicyKeyData => ParsePolicyKeyDataRecordRecordExtraField(data, ref offset),
                    HeaderID.IBMS390AttributesUncompressed => ParseAS400ExtraFieldAttribute(data, ref offset),
                    HeaderID.IBMS390AttributesCompressed => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.POSZIP4690 => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.Macintosh => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.PixarUSD => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.ZipItMacintosh => ParseZipItMacintoshExtraField(data, ref offset),
                    HeaderID.ZipItMacintosh135Plus => ParseZipItMacintoshShortFileExtraField(data, ref offset),
                    HeaderID.ZipItMacintosh135PlusAlt => ParseZipItMacintoshShortDirectoryExtraField(data, ref offset),
                    HeaderID.InfoZIPMacintosh => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.AcornSparkFS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.WindowsNTSecurityDescriptor => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.VMCMS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.MVS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.THEOSold => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.FWKCSMD5 => ParseFWKCSMD5ExtraField(data, ref offset),
                    HeaderID.OS2AccessControlList => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPOpenVMS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.MacintoshSmartzip => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.XceedOriginalLocation => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.ADSVS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.ExtendedTimestamp => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.XceedUnicode => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPUNIX => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPUnicodeComment => ParseInfoZIPUnicodeCommentExtraField(data, ref offset),
                    HeaderID.BeOSBeBox => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.THEOS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPUnicodePath => ParseInfoZIPUnicodePathExtraField(data, ref offset),
                    HeaderID.AtheOSSyllable => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.ASiUNIX => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPUNIXNew => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPUNIXNewer => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.DataStreamAlignment => ParseDataStreamAlignment(data, ref offset),
                    HeaderID.MicrosoftOpenPackagingGrowthHint => ParseMicrosoftOpenPackagingGrowthHint(data, ref offset),
                    HeaderID.JavaJAR => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.AndroidZIPAlignment => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.KoreanZIPCodePage => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.SMSQDOS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.AExEncryptionStructure => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.Unknown => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement

                    _ => ParseUnknownExtraField(data, ref offset, includeDebug),
                };

                if (field is not null)
                    fields.Add(field);
            }

            return [.. fields];
        }

        /// <summary>
        /// Process all extensible data fields in a local file extras block
        /// </summary>
        /// <param name="header">Local file header</param>
        /// <param name="data">Byte array to parse</param>
        /// <param name="includeDebug">True to include debug data, false otherwise</param>
        /// <returns>Array of data fields on success, null otherwise</returns>
        public static ExtensibleDataField[]? ParseExtraFields(LocalFileHeader header, byte[]? data, bool includeDebug = false)
        {
            if (data is null)
                return null;

            List<ExtensibleDataField> fields = [];

            int offset = 0;
            while (offset < data.Length)
            {
                // Peek at the next header ID
                HeaderID id = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
                offset -= 2;

                // Read based on the ID
                ExtensibleDataField? field = id switch
                {
                    HeaderID.Zip64ExtendedInformation => ParseZip64ExtendedInformationExtraField(data, ref offset, header),
                    HeaderID.AVInfo => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement model
                    HeaderID.ExtendedLanguageEncodingData => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement model
                    HeaderID.OS2 => ParseOS2ExtraField(data, ref offset),
                    HeaderID.NTFS => ParseNTFSExtraField(data, ref offset),
                    HeaderID.OpenVMS => ParseOpenVMSExtraField(data, ref offset),
                    HeaderID.UNIX => ParseUnixExtraField(data, ref offset),
                    HeaderID.FileStreamFork => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.PatchDescriptor => ParsePatchDescriptorExtraField(data, ref offset),
                    HeaderID.PKCSStore => ParsePKCS7Store(data, ref offset),
                    HeaderID.X509IndividualFile => ParseX509IndividualFile(data, ref offset),
                    HeaderID.X509CentralDirectory => ParseX509CentralDirectory(data, ref offset),
                    HeaderID.StrongEncryptionHeader => ParseStrongEncryptionHeader(data, ref offset),
                    HeaderID.RecordManagementControls => ParseRecordManagementControls(data, ref offset),
                    HeaderID.PKCSCertificateList => ParsePKCS7EncryptionRecipientCertificateList(data, ref offset),
                    HeaderID.Timestamp => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.PolicyDecryptionKey => ParsePolicyDecryptionKeyRecordExtraField(data, ref offset),
                    HeaderID.SmartcryptKeyProvider => ParseKeyProviderRecordExtraField(data, ref offset),
                    HeaderID.SmartcryptPolicyKeyData => ParsePolicyKeyDataRecordRecordExtraField(data, ref offset),
                    HeaderID.IBMS390AttributesUncompressed => ParseAS400ExtraFieldAttribute(data, ref offset),
                    HeaderID.IBMS390AttributesCompressed => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.POSZIP4690 => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.Macintosh => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.PixarUSD => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.ZipItMacintosh => ParseZipItMacintoshExtraField(data, ref offset),
                    HeaderID.ZipItMacintosh135Plus => ParseZipItMacintoshShortFileExtraField(data, ref offset),
                    HeaderID.ZipItMacintosh135PlusAlt => ParseZipItMacintoshShortDirectoryExtraField(data, ref offset),
                    HeaderID.InfoZIPMacintosh => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.AcornSparkFS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.WindowsNTSecurityDescriptor => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.VMCMS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.MVS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.THEOSold => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.FWKCSMD5 => ParseFWKCSMD5ExtraField(data, ref offset),
                    HeaderID.OS2AccessControlList => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPOpenVMS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.MacintoshSmartzip => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.XceedOriginalLocation => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.ADSVS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.ExtendedTimestamp => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.XceedUnicode => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPUNIX => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPUnicodeComment => ParseInfoZIPUnicodeCommentExtraField(data, ref offset),
                    HeaderID.BeOSBeBox => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.THEOS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPUnicodePath => ParseInfoZIPUnicodePathExtraField(data, ref offset),
                    HeaderID.AtheOSSyllable => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.ASiUNIX => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPUNIXNew => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.InfoZIPUNIXNewer => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.DataStreamAlignment => ParseDataStreamAlignment(data, ref offset),
                    HeaderID.MicrosoftOpenPackagingGrowthHint => ParseMicrosoftOpenPackagingGrowthHint(data, ref offset),
                    HeaderID.JavaJAR => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.AndroidZIPAlignment => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.KoreanZIPCodePage => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.SMSQDOS => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.AExEncryptionStructure => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement
                    HeaderID.Unknown => ParseUnknownExtraField(data, ref offset, includeDebug), // TODO: Implement

                    _ => ParseUnknownExtraField(data, ref offset, includeDebug),
                };

                if (field is not null)
                    fields.Add(field);
            }

            return [.. fields];
        }

        /// <summary>
        /// Parse a Stream into an unknown extras field
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <param name="includeDebug">True to include debug data, false otherwise</param>
        /// <returns>Filled unknown extras field on success, null on error</returns>
        private static UnknownExtraField? ParseUnknownExtraField(byte[] data, ref int offset, bool includeDebug)
        {
            var obj = new UnknownExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            if (obj.DataSize > data.Length - offset)
            {
                ushort remainingSize = (ushort)(data.Length - offset);
                if (includeDebug) Console.WriteLine($"Extra field of type '{obj.HeaderID}' requested {obj.DataSize} bytes, but only {remainingSize} remain");
                obj.DataSize = remainingSize;
            }

            if (obj.DataSize > 0)
                obj.Data = data.ReadBytes(ref offset, obj.DataSize);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a Zip64ExtendedInformationExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled Zip64ExtendedInformationExtraField on success, null on error</returns>
        private static Zip64ExtendedInformationExtraField? ParseZip64ExtendedInformationExtraField(byte[] data, ref int offset, CentralDirectoryFileHeader header)
        {
            var obj = new Zip64ExtendedInformationExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.Zip64ExtendedInformation)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);

            int bytesRemaining = obj.DataSize;
            if (header.UncompressedSize == uint.MaxValue && bytesRemaining >= 8)
            {
                obj.OriginalSize = data.ReadUInt64LittleEndian(ref offset);
                bytesRemaining -= 8;
            }

            if (header.CompressedSize == uint.MaxValue && bytesRemaining >= 8)
            {
                obj.CompressedSize = data.ReadUInt64LittleEndian(ref offset);
                bytesRemaining -= 8;
            }

            if (header.RelativeOffsetOfLocalHeader == uint.MaxValue && bytesRemaining >= 8)
            {
                obj.RelativeHeaderOffset = data.ReadUInt64LittleEndian(ref offset);
                bytesRemaining -= 8;
            }

            if (header.DiskNumberStart == ushort.MaxValue && bytesRemaining >= 4)
            {
                obj.DiskStartNumber = data.ReadUInt32LittleEndian(ref offset);
                //bytesRemaining -= 4;
            }

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a Zip64ExtendedInformationExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled Zip64ExtendedInformationExtraField on success, null on error</returns>
        private static Zip64ExtendedInformationExtraField? ParseZip64ExtendedInformationExtraField(byte[] data, ref int offset, LocalFileHeader header)
        {
            var obj = new Zip64ExtendedInformationExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.Zip64ExtendedInformation)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);

            int bytesRemaining = obj.DataSize;
            if (header.UncompressedSize == uint.MaxValue)
            {
                obj.OriginalSize = data.ReadUInt64LittleEndian(ref offset);
                bytesRemaining -= 8;
            }

            if (header.CompressedSize == uint.MaxValue)
            {
                obj.CompressedSize = data.ReadUInt64LittleEndian(ref offset);
                bytesRemaining -= 8;
            }

            // TODO: These rely on values from the central directory
            if (bytesRemaining >= 8)
            {
                obj.RelativeHeaderOffset = data.ReadUInt64LittleEndian(ref offset);
                bytesRemaining -= 8;
            }

            if (bytesRemaining >= 4)
                obj.DiskStartNumber = data.ReadUInt32LittleEndian(ref offset);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into an OS2ExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled OS2ExtraField on success, null on error</returns>
        private static OS2ExtraField? ParseOS2ExtraField(byte[] data, ref int offset)
        {
            var obj = new OS2ExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.OS2)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.UncompressedBlockSize = data.ReadUInt32LittleEndian(ref offset);
            obj.CompressionType = data.ReadUInt16LittleEndian(ref offset);
            obj.CRC32 = data.ReadUInt32LittleEndian(ref offset);
            obj.Data = data.ReadBytes(ref offset, obj.DataSize - 10);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a NTFSExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled NTFSExtraField on success, null on error</returns>
        private static NTFSExtraField? ParseNTFSExtraField(byte[] data, ref int offset)
        {
            var obj = new NTFSExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.NTFS)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.Reserved = data.ReadUInt32LittleEndian(ref offset);

            List<TagSizeVar> entries = [];

            int bytesRemaining = obj.DataSize - 4;
            while (bytesRemaining > 0)
            {
                var entry = new TagSizeVar();

                entry.Tag = data.ReadUInt16LittleEndian(ref offset);
                entry.Size = data.ReadUInt16LittleEndian(ref offset);
                entry.Var = data.ReadBytes(ref offset, entry.Size);

                entries.Add(entry);

                bytesRemaining -= 4 + entry.Size;
            }

            obj.TagSizeVars = [.. entries];

            return obj;
        }

        /// <summary>
        /// Parse a Stream into an OpenVMSExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled OpenVMSExtraField on success, null on error</returns>
        private static OpenVMSExtraField? ParseOpenVMSExtraField(byte[] data, ref int offset)
        {
            var obj = new OpenVMSExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.OpenVMS)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.CRC = data.ReadUInt32LittleEndian(ref offset);

            List<TagSizeVar> entries = [];

            int bytesRemaining = obj.DataSize - 4;
            while (bytesRemaining > 0)
            {
                var entry = new TagSizeVar();

                entry.Tag = data.ReadUInt16LittleEndian(ref offset);
                entry.Size = data.ReadUInt16LittleEndian(ref offset);
                entry.Var = data.ReadBytes(ref offset, entry.Size);

                entries.Add(entry);

                bytesRemaining -= 4 + entry.Size;
            }

            obj.TagSizeVars = [.. entries];

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a UnixExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled UnixExtraField on success, null on error</returns>
        private static UnixExtraField? ParseUnixExtraField(byte[] data, ref int offset)
        {
            var obj = new UnixExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.UNIX)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.FileLastAccessTime = data.ReadUInt32LittleEndian(ref offset);
            obj.FileLastModificationTime = data.ReadUInt32LittleEndian(ref offset);
            obj.FileUserID = data.ReadUInt16LittleEndian(ref offset);
            obj.FileGroupID = data.ReadUInt16LittleEndian(ref offset);
            obj.Data = data.ReadBytes(ref offset, obj.DataSize - 12);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a PatchDescriptorExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled PatchDescriptorExtraField on success, null on error</returns>
        private static PatchDescriptorExtraField? ParsePatchDescriptorExtraField(byte[] data, ref int offset)
        {
            var obj = new PatchDescriptorExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.PatchDescriptor)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.Version = data.ReadUInt16LittleEndian(ref offset);
            obj.Flags = (ActionsReactions)data.ReadUInt32LittleEndian(ref offset);
            obj.OldSize = data.ReadUInt32LittleEndian(ref offset);
            obj.OldCRC = data.ReadUInt32LittleEndian(ref offset);
            obj.NewSize = data.ReadUInt32LittleEndian(ref offset);
            obj.NewCRC = data.ReadUInt32LittleEndian(ref offset);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a PKCS7Store
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled PKCS7Store on success, null on error</returns>
        private static PKCS7Store? ParsePKCS7Store(byte[] data, ref int offset)
        {
            var obj = new PKCS7Store();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.PKCSStore)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.TData = data.ReadBytes(ref offset, obj.DataSize);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a X509IndividualFile
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled X509IndividualFile on success, null on error</returns>
        private static X509IndividualFile? ParseX509IndividualFile(byte[] data, ref int offset)
        {
            var obj = new X509IndividualFile();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.X509IndividualFile)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.TData = data.ReadBytes(ref offset, obj.DataSize);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a X509CentralDirectory
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled X509CentralDirectory on success, null on error</returns>
        private static X509CentralDirectory? ParseX509CentralDirectory(byte[] data, ref int offset)
        {
            var obj = new X509CentralDirectory();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.X509CentralDirectory)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.TData = data.ReadBytes(ref offset, obj.DataSize);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a StrongEncryptionHeader
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled StrongEncryptionHeader on success, null on error</returns>
        private static StrongEncryptionHeader? ParseStrongEncryptionHeader(byte[] data, ref int offset)
        {
            var obj = new StrongEncryptionHeader();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.StrongEncryptionHeader)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.Format = data.ReadUInt16LittleEndian(ref offset);
            obj.AlgID = data.ReadUInt16LittleEndian(ref offset);
            obj.Bitlen = data.ReadUInt16LittleEndian(ref offset);
            obj.Flags = data.ReadUInt16LittleEndian(ref offset);
            obj.CertData = data.ReadBytes(ref offset, obj.DataSize - 8);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into an RecordManagementControls
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled RecordManagementControls on success, null on error</returns>
        private static RecordManagementControls? ParseRecordManagementControls(byte[] data, ref int offset)
        {
            var obj = new RecordManagementControls();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.RecordManagementControls)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);

            List<TagSizeVar> entries = [];

            int bytesRemaining = obj.DataSize - 4;
            while (bytesRemaining > 0)
            {
                var entry = new TagSizeVar();

                entry.Tag = data.ReadUInt16LittleEndian(ref offset);
                entry.Size = data.ReadUInt16LittleEndian(ref offset);
                entry.Var = data.ReadBytes(ref offset, entry.Size);

                entries.Add(entry);

                bytesRemaining -= 4 + entry.Size;
            }

            obj.TagSizeVars = [.. entries];

            return obj;
        }

        /// <summary>
        /// Parse a Stream into an PKCS7EncryptionRecipientCertificateList
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled PKCS7EncryptionRecipientCertificateList on success, null on error</returns>
        private static PKCS7EncryptionRecipientCertificateList? ParsePKCS7EncryptionRecipientCertificateList(byte[] data, ref int offset)
        {
            var obj = new PKCS7EncryptionRecipientCertificateList();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.PKCSCertificateList)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.Version = data.ReadUInt16LittleEndian(ref offset);
            obj.CStore = data.ReadBytes(ref offset, obj.DataSize - 2);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a PolicyDecryptionKeyRecordExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled PolicyDecryptionKeyRecordExtraField on success, null on error</returns>
        private static PolicyDecryptionKeyRecordExtraField? ParsePolicyDecryptionKeyRecordExtraField(byte[] data, ref int offset)
        {
            var obj = new PolicyDecryptionKeyRecordExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.PolicyDecryptionKey)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.TData = data.ReadBytes(ref offset, obj.DataSize);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a KeyProviderRecordExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled KeyProviderRecordExtraField on success, null on error</returns>
        private static KeyProviderRecordExtraField? ParseKeyProviderRecordExtraField(byte[] data, ref int offset)
        {
            var obj = new KeyProviderRecordExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.SmartcryptKeyProvider)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.TData = data.ReadBytes(ref offset, obj.DataSize);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a PolicyKeyDataRecordRecordExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled PolicyKeyDataRecordRecordExtraField on success, null on error</returns>
        private static PolicyKeyDataRecordRecordExtraField? ParsePolicyKeyDataRecordRecordExtraField(byte[] data, ref int offset)
        {
            var obj = new PolicyKeyDataRecordRecordExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.SmartcryptPolicyKeyData)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.TData = data.ReadBytes(ref offset, obj.DataSize);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a AS400ExtraFieldAttribute
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled AS400ExtraFieldAttribute on success, null on error</returns>
        /// <remarks>
        /// This header ID is shared with MVSExtraField, OS400ExtraField, and ZOSExtraFieldAttribute.
        /// This code makes an assumption that it's always AS400ExtraFieldAttribute.
        /// </remarks>
        private static AS400ExtraFieldAttribute? ParseAS400ExtraFieldAttribute(byte[] data, ref int offset)
        {
            var obj = new AS400ExtraFieldAttribute();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.IBMS390AttributesUncompressed)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.FieldLength = data.ReadUInt16BigEndian(ref offset);
            obj.FieldCode = (AS400ExtraFieldAttributeFieldCode)data.ReadUInt16LittleEndian(ref offset);
            obj.Data = data.ReadBytes(ref offset, obj.DataSize - 4);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a ZipItMacintoshExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled ZipItMacintoshExtraField on success, null on error</returns>
        private static ZipItMacintoshExtraField? ParseZipItMacintoshExtraField(byte[] data, ref int offset)
        {
            var obj = new ZipItMacintoshExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.ZipItMacintosh)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.ExtraFieldSignature = data.ReadUInt32LittleEndian(ref offset);
            obj.FnLen = data.ReadByteValue(ref offset);
            byte[] filenameBytes = data.ReadBytes(ref offset, obj.FnLen);
            obj.FileName = Encoding.ASCII.GetString(filenameBytes);
            obj.FileType = data.ReadBytes(ref offset, 4);
            obj.Creator = data.ReadBytes(ref offset, 4);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a ZipItMacintoshShortFileExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled ZipItMacintoshShortFileExtraField on success, null on error</returns>
        private static ZipItMacintoshShortFileExtraField? ParseZipItMacintoshShortFileExtraField(byte[] data, ref int offset)
        {
            var obj = new ZipItMacintoshShortFileExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.ZipItMacintosh135Plus)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.ExtraFieldSignature = data.ReadUInt32LittleEndian(ref offset);
            obj.FileType = data.ReadBytes(ref offset, 4);
            obj.Creator = data.ReadBytes(ref offset, 4);

            if (obj.DataSize > 12)
                obj.FdFlags = data.ReadUInt16LittleEndian(ref offset);

            if (obj.DataSize > 14)
                obj.Reserved = data.ReadUInt16LittleEndian(ref offset);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a ZipItMacintoshShortDirectoryExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled ZipItMacintoshShortDirectoryExtraField on success, null on error</returns>
        private static ZipItMacintoshShortDirectoryExtraField? ParseZipItMacintoshShortDirectoryExtraField(byte[] data, ref int offset)
        {
            var obj = new ZipItMacintoshShortDirectoryExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.ZipItMacintosh135PlusAlt)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.ExtraFieldSignature = data.ReadUInt32LittleEndian(ref offset);

            if (obj.DataSize > 4)
                obj.FrFlags = data.ReadUInt16LittleEndian(ref offset);

            if (obj.DataSize > 6)
                obj.View = (ZipItInternalSettings)data.ReadUInt16LittleEndian(ref offset);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a FWKCSMD5ExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled FWKCSMD5ExtraField on success, null on error</returns>
        private static FWKCSMD5ExtraField? ParseFWKCSMD5ExtraField(byte[] data, ref int offset)
        {
            var obj = new FWKCSMD5ExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.FWKCSMD5)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.Preface = data.ReadBytes(ref offset, 3);
            obj.MD5 = data.ReadBytes(ref offset, 16);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a InfoZIPUnicodeCommentExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled InfoZIPUnicodeCommentExtraField on success, null on error</returns>
        private static InfoZIPUnicodeCommentExtraField? ParseInfoZIPUnicodeCommentExtraField(byte[] data, ref int offset)
        {
            var obj = new InfoZIPUnicodeCommentExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.InfoZIPUnicodeComment)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.Version = data.ReadByteValue(ref offset);
            obj.ComCRC32 = data.ReadUInt32LittleEndian(ref offset);
            byte[] unicodeBytes = data.ReadBytes(ref offset, obj.DataSize - 5);
            obj.UnicodeCom = Encoding.UTF8.GetString(unicodeBytes);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a InfoZIPUnicodePathExtraField
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled InfoZIPUnicodePathExtraField on success, null on error</returns>
        private static InfoZIPUnicodePathExtraField? ParseInfoZIPUnicodePathExtraField(byte[] data, ref int offset)
        {
            var obj = new InfoZIPUnicodePathExtraField();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.InfoZIPUnicodePath)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.Version = data.ReadByteValue(ref offset);
            obj.NameCRC32 = data.ReadUInt32LittleEndian(ref offset);
            byte[] unicodeBytes = data.ReadBytes(ref offset, obj.DataSize - 5);
            obj.UnicodeName = Encoding.UTF8.GetString(unicodeBytes);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a DataStreamAlignment
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled DataStreamAlignment on success, null on error</returns>
        private static DataStreamAlignment? ParseDataStreamAlignment(byte[] data, ref int offset)
        {
            var obj = new DataStreamAlignment();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.DataStreamAlignment)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.Alignment = data.ReadUInt16LittleEndian(ref offset);
            obj.Padding = data.ReadBytes(ref offset, obj.DataSize - 2);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a MicrosoftOpenPackagingGrowthHint
        /// </summary>
        /// <param name="data">Byte array to parse</param>
        /// <param name="offset">Offset into the byte array</param>
        /// <returns>Filled MicrosoftOpenPackagingGrowthHint on success, null on error</returns>
        private static MicrosoftOpenPackagingGrowthHint? ParseMicrosoftOpenPackagingGrowthHint(byte[] data, ref int offset)
        {
            var obj = new MicrosoftOpenPackagingGrowthHint();

            obj.HeaderID = (HeaderID)data.ReadUInt16LittleEndian(ref offset);
            if (obj.HeaderID != HeaderID.MicrosoftOpenPackagingGrowthHint)
                return null;

            obj.DataSize = data.ReadUInt16LittleEndian(ref offset);
            obj.Sig = data.ReadUInt16LittleEndian(ref offset);
            obj.PadVal = data.ReadUInt16LittleEndian(ref offset);
            obj.Padding = data.ReadBytes(ref offset, obj.DataSize - 2);

            return obj;
        }

        #endregion

        #region Helpers

        /// <summary>
        /// Determine if a data descriptor is 64-bit
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>True if the descriptor is 64-bit, false otherwise</returns>
        private static bool IsZip64Descriptor(Stream data)
        {
            // Last item in the stream
            if (data.Position + 12 == data.Length) // Short 32-bit
                return false;
            else if (data.Position + 16 == data.Length) // Long 32-bit
                return false;
            else if (data.Position + 20 == data.Length) // Short 64-bit
                return true;
            else if (data.Position + 24 == data.Length) // Long 64-bit
                return true;

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

            // Short 32-bit
            data.SeekIfPossible(12, SeekOrigin.Current);
            byte[] nextBlock = data.ReadBytes(2);
            data.SeekIfPossible(currentPosition, SeekOrigin.Begin);
            if (nextBlock.EqualsExactly([0x50, 0x4B]))
                return false;

            // Long 32-bit
            data.SeekIfPossible(16, SeekOrigin.Current);
            nextBlock = data.ReadBytes(2);
            data.SeekIfPossible(currentPosition, SeekOrigin.Begin);
            if (nextBlock.EqualsExactly([0x50, 0x4B]))
                return false;

            // Assume 64-bit otherwise
            return true;
        }

        #endregion
    }
}
