using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Data.Models.AACS;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;

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

            try
            {
                #region Records

                // Create the records list
                var records = new List<Record>();

                // Try to parse the records
                while (data.Position < data.Length)
                {
                    // Try to parse the record
                    var record = ParseRecord(data);
                    if (record is null)
                        continue;

                    // Add the record
                    records.Add(record);

                    // If we have an end of media key block record
                    if (record.RecordType == RecordType.EndOfMediaKeyBlock)
                        break;

                    // Align to the 4-byte boundary if we're not at the end
                    if (!data.AlignToBoundary(4))
                        break;
                }

                #endregion

                // Set the records
                if (records.Count > 0)
                    return new MediaKeyBlock { Records = [.. records] };

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

        /// <summary>
        /// Parse a Stream into a Record
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled Record on success, null on error</returns>
        private static Record? ParseRecord(Stream data)
        {
            // The first 4 bytes are the type and length
            RecordType type = (RecordType)data.PeekByteValue();

            // Create a record based on the type
            return type switch
            {
                // Known record types
                RecordType.EndOfMediaKeyBlock => ParseEndOfMediaKeyBlockRecord(data),
                RecordType.ExplicitSubsetDifference => ParseExplicitSubsetDifferenceRecord(data),
                RecordType.MediaKeyData => ParseMediaKeyDataRecord(data),
                RecordType.SubsetDifferenceIndex => ParseSubsetDifferenceIndexRecord(data),
                RecordType.TypeAndVersion => ParseTypeAndVersionRecord(data),
                RecordType.DriveRevocationList => ParseDriveRevocationListRecord(data),
                RecordType.HostRevocationList => ParseHostRevocationListRecord(data),
                RecordType.VerifyMediaKey => ParseVerifyMediaKeyRecord(data),
                RecordType.Copyright => ParseCopyrightRecord(data),

                // Unimplemented
                RecordType.MediaKeyVariantData => ParseGenericRecord(data),
                RecordType.Unknown0x28_AACS2 => ParseGenericRecord(data),
                RecordType.DriveRevocationList_AACS2 => ParseGenericRecord(data),
                RecordType.HostRevocationList_AACS2 => ParseGenericRecord(data),
                RecordType.VerifyMediaKey_AACS2 => ParseGenericRecord(data),
                RecordType.EmptyRecord0xF8_AACS2 => ParseGenericRecord(data),

                // Unknown record type
                _ => ParseGenericRecord(data),
            };
        }

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

            obj.RecordType = (RecordType)data.ReadByteValue();
            obj.RecordLength = data.ReadUInt24LittleEndian();
            if ((uint)obj.RecordLength > 4)
            {
                byte[] copyright = data.ReadBytes((int)((uint)obj.RecordLength - 4));
                obj.Copyright = Encoding.ASCII.GetString(copyright).TrimEnd('\0');
            }

            return obj;
        }

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

            obj.Range = data.ReadUInt16BigEndian();
            obj.DriveID = data.ReadBytes(6);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a DriveRevocationListRecord
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled DriveRevocationListRecord on success, null on error</returns>
        public static DriveRevocationListRecord ParseDriveRevocationListRecord(Stream data)
        {
            // Cache the current offset
            long initialOffset = data.Position;

            var obj = new DriveRevocationListRecord();

            obj.RecordType = (RecordType)data.ReadByteValue();
            obj.RecordLength = data.ReadUInt24LittleEndian();
            obj.TotalNumberOfEntries = data.ReadUInt32BigEndian();

            // Try to parse the signature blocks
            var blocks = new List<DriveRevocationSignatureBlock>();
            uint entryCount = 0;
            while (entryCount < obj.TotalNumberOfEntries && data.Position < initialOffset + (uint)obj.RecordLength)
            {
                var block = ParseDriveRevocationSignatureBlock(data);
                entryCount += block.NumberOfEntries;
                blocks.Add(block);

                // If we have an empty block
                if (block.NumberOfEntries == 0)
                    break;
            }

            // Set the signature blocks
            obj.SignatureBlocks = [.. blocks];

            // If there's any data left, discard it
            if (data.Position < initialOffset + (uint)obj.RecordLength)
                _ = data.ReadBytes((int)(initialOffset + (uint)obj.RecordLength - data.Position));

            return obj;
        }

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

            obj.NumberOfEntries = data.ReadUInt32BigEndian();
            obj.EntryFields = new DriveRevocationListEntry[obj.NumberOfEntries];
            for (int i = 0; i < obj.EntryFields.Length; i++)
            {
                obj.EntryFields[i] = ParseDriveRevocationListEntry(data);
            }

            return obj;
        }

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

            obj.RecordType = (RecordType)data.ReadByteValue();
            obj.RecordLength = data.ReadUInt24LittleEndian();
            if ((uint)obj.RecordLength > 4)
                obj.SignatureData = data.ReadBytes((int)((uint)obj.RecordLength - 4));

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a ExplicitSubsetDifferenceRecord
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled ExplicitSubsetDifferenceRecord on success, null on error</returns>
        public static ExplicitSubsetDifferenceRecord ParseExplicitSubsetDifferenceRecord(Stream data)
        {
            // Cache the current offset
            long initialOffset = data.Position;

            var obj = new ExplicitSubsetDifferenceRecord();

            obj.RecordType = (RecordType)data.ReadByteValue();
            obj.RecordLength = data.ReadUInt24LittleEndian();

            // Try to parse the subset differences
            var subsetDifferences = new List<SubsetDifference>();
            while (data.Position < initialOffset + (uint)obj.RecordLength - 5)
            {
                var subsetDifference = ParseSubsetDifference(data);
                subsetDifferences.Add(subsetDifference);
            }

            // Set the subset differences
            obj.SubsetDifferences = [.. subsetDifferences];

            // If there's any data left, discard it
            if (data.Position < initialOffset + (uint)obj.RecordLength)
                _ = data.ReadBytes((int)(initialOffset + (uint)obj.RecordLength - data.Position));

            return obj;
        }

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

            obj.RecordType = (RecordType)data.ReadByteValue();
            obj.RecordLength = data.ReadUInt24LittleEndian();
            obj.Data = data.ReadBytes(0x10);

            return obj;
        }

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

            obj.Range = data.ReadUInt16BigEndian();
            obj.HostID = data.ReadBytes(6);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a HostRevocationListRecord
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled HostRevocationListRecord on success, null on error</returns>
        public static HostRevocationListRecord ParseHostRevocationListRecord(Stream data)
        {
            // Cache the current offset
            long initialOffset = data.Position;

            var obj = new HostRevocationListRecord();

            obj.RecordType = (RecordType)data.ReadByteValue();
            obj.RecordLength = data.ReadUInt24LittleEndian();
            obj.TotalNumberOfEntries = data.ReadUInt32BigEndian();

            // Try to parse the signature blocks
            var blocks = new List<HostRevocationSignatureBlock>();
            for (uint entryCount = 0; entryCount < obj.TotalNumberOfEntries && data.Position < initialOffset + (uint)obj.RecordLength;)
            {
                var block = ParseHostRevocationSignatureBlock(data);
                entryCount += block.NumberOfEntries;
                blocks.Add(block);

                // If we have an empty block
                if (block.NumberOfEntries == 0)
                    break;
            }

            // Set the signature blocks
            obj.SignatureBlocks = [.. blocks];

            // If there's any data left, discard it
            if (data.Position < initialOffset + (uint)obj.RecordLength)
                _ = data.ReadBytes((int)(initialOffset + (uint)obj.RecordLength - data.Position));

            return obj;
        }

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

            obj.NumberOfEntries = data.ReadUInt32BigEndian();
            obj.EntryFields = new HostRevocationListEntry[obj.NumberOfEntries];
            for (int i = 0; i < obj.EntryFields.Length; i++)
            {
                obj.EntryFields[i] = ParseHostRevocationListEntry(data);
            }

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a MediaKeyDataRecord
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled MediaKeyDataRecord on success, null on error</returns>
        public static MediaKeyDataRecord ParseMediaKeyDataRecord(Stream data)
        {
            // Cache the current offset
            long initialOffset = data.Position;

            var obj = new MediaKeyDataRecord();

            obj.RecordType = (RecordType)data.ReadByteValue();
            obj.RecordLength = data.ReadUInt24LittleEndian();

            // Try to parse the media keys
            var mediaKeys = new List<byte[]>();
            while (data.Position < initialOffset + (uint)obj.RecordLength)
            {
                byte[] mediaKey = data.ReadBytes(0x10);
                mediaKeys.Add(mediaKey);
            }

            // Set the media keys
            obj.MediaKeyData = [.. mediaKeys];

            return obj;
        }

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

            obj.Mask = data.ReadByteValue();
            obj.Number = data.ReadUInt32BigEndian();

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a SubsetDifferenceIndexRecord
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <returns>Filled SubsetDifferenceIndexRecord on success, null on error</returns>
        public static SubsetDifferenceIndexRecord ParseSubsetDifferenceIndexRecord(Stream data)
        {
            // Cache the current offset
            long initialOffset = data.Position;

            var obj = new SubsetDifferenceIndexRecord();

            obj.RecordType = (RecordType)data.ReadByteValue();
            obj.RecordLength = data.ReadUInt24LittleEndian();
            obj.Span = data.ReadUInt32BigEndian();

            // Try to parse the offsets
            var offsets = new List<uint>();
            while (data.Position < initialOffset + (uint)obj.RecordLength)
            {
                uint offset = data.ReadUInt32BigEndian();
                offsets.Add(offset);
            }

            // Set the offsets
            obj.Offsets = [.. offsets];

            return obj;
        }

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

            obj.RecordType = (RecordType)data.ReadByteValue();
            obj.RecordLength = data.ReadUInt24LittleEndian();
            obj.MediaKeyBlockType = (MediaKeyBlockType)data.ReadUInt32BigEndian();
            obj.VersionNumber = data.ReadUInt32BigEndian();

            return obj;
        }

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

            obj.RecordType = (RecordType)data.ReadByteValue();
            obj.RecordLength = data.ReadUInt24LittleEndian();
            obj.CiphertextValue = data.ReadBytes(0x10);

            return obj;
        }
    }
}
