using System.IO;
using System.Text;
using SabreTools.Data.Models.N3DS;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;

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

            try
            {
                // Create a new CIA archive to fill
                var cia = new Data.Models.N3DS.CIA();

                #region CIA Header

                // Try to parse the header
                var header = ParseCIAHeader(data);
                if (header.CertificateChainSize > data.Length)
                    return null;
                if (header.TicketSize > data.Length)
                    return null;
                if (header.TMDFileSize > data.Length)
                    return null;
                if (header.MetaSize > data.Length)
                    return null;
                if ((long)header.ContentSize > data.Length)
                    return null;

                // Set the CIA archive header
                cia.Header = header;

                #endregion

                // Align to 64-byte boundary, if needed
                if (!data.AlignToBoundary(64))
                    return null;

                #region Certificate Chain

                // Create the certificate chain
                cia.CertificateChain = new Certificate[3];

                // Try to parse the certificates
                for (int i = 0; i < 3; i++)
                {
                    var certificate = ParseCertificate(data);
                    if (certificate is null)
                        return null;

                    cia.CertificateChain[i] = certificate;
                }

                #endregion

                // Align to 64-byte boundary, if needed
                if (!data.AlignToBoundary(64))
                    return null;

                #region Ticket

                // Try to parse the ticket
                var ticket = ParseTicket(data);
                if (ticket is null)
                    return null;

                // Set the ticket
                cia.Ticket = ticket;

                #endregion

                // Align to 64-byte boundary, if needed
                if (!data.AlignToBoundary(64))
                    return null;

                #region Title Metadata

                // Try to parse the title metadata
                var titleMetadata = ParseTitleMetadata(data);
                if (titleMetadata is null)
                    return null;

                // Set the title metadata
                cia.TMDFileData = titleMetadata;

                #endregion

                // Align to 64-byte boundary, if needed
                if (!data.AlignToBoundary(64))
                    return null;

                #region Content File Data

                // Create the partition table
                cia.Partitions = new NCCHHeader[8];

                // Iterate and build the partitions
                for (int i = 0; i < 8; i++)
                {
                    cia.Partitions[i] = N3DS.ParseNCCHHeader(data);
                }

                #endregion

                // Align to 64-byte boundary, if needed
                if (!data.AlignToBoundary(64))
                    return null;

                #region Meta Data

                // If we have a meta data
                if (header.MetaSize > 0)
                    cia.MetaData = ParseMetaData(data);

                #endregion

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

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

            obj.SignatureType = (SignatureType)data.ReadUInt32LittleEndian();
            switch (obj.SignatureType)
            {
                case SignatureType.RSA_4096_SHA1:
                case SignatureType.RSA_4096_SHA256:
                    obj.SignatureSize = 0x200;
                    obj.PaddingSize = 0x3C;
                    break;

                case SignatureType.RSA_2048_SHA1:
                case SignatureType.RSA_2048_SHA256:
                    obj.SignatureSize = 0x100;
                    obj.PaddingSize = 0x3C;
                    break;

                case SignatureType.ECDSA_SHA1:
                case SignatureType.ECDSA_SHA256:
                    obj.SignatureSize = 0x3C;
                    obj.PaddingSize = 0x40;
                    break;

                default:
                    return null;
            }

            obj.Signature = data.ReadBytes(obj.SignatureSize);
            obj.Padding = data.ReadBytes(obj.PaddingSize);
            byte[] issuer = data.ReadBytes(0x40);
            obj.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0');
            obj.KeyType = (PublicKeyType)data.ReadUInt32LittleEndian();
            byte[] name = data.ReadBytes(0x40);
            obj.Name = Encoding.ASCII.GetString(name).TrimEnd('\0');
            obj.ExpirationTime = data.ReadUInt32LittleEndian();

            switch (obj.KeyType)
            {
                case PublicKeyType.RSA_4096:
                    obj.RSAModulus = data.ReadBytes(0x200);
                    obj.RSAPublicExponent = data.ReadUInt32LittleEndian();
                    obj.RSAPadding = data.ReadBytes(0x34);
                    break;
                case PublicKeyType.RSA_2048:
                    obj.RSAModulus = data.ReadBytes(0x100);
                    obj.RSAPublicExponent = data.ReadUInt32LittleEndian();
                    obj.RSAPadding = data.ReadBytes(0x34);
                    break;
                case PublicKeyType.EllipticCurve:
                    obj.ECCPublicKey = data.ReadBytes(0x3C);
                    obj.ECCPadding = data.ReadBytes(0x3C);
                    break;
                default:
                    return null;
            }

            return obj;
        }

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

            obj.HeaderSize = data.ReadUInt32LittleEndian();
            obj.Type = data.ReadUInt16LittleEndian();
            obj.Version = data.ReadUInt16LittleEndian();
            obj.CertificateChainSize = data.ReadUInt32LittleEndian();
            obj.TicketSize = data.ReadUInt32LittleEndian();
            obj.TMDFileSize = data.ReadUInt32LittleEndian();
            obj.MetaSize = data.ReadUInt32LittleEndian();
            obj.ContentSize = data.ReadUInt64LittleEndian();
            obj.ContentIndex = data.ReadBytes(0x2000);

            return obj;
        }

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

            obj.ContentId = data.ReadUInt32LittleEndian();
            obj.ContentIndex = (ContentIndex)data.ReadUInt16LittleEndian();
            obj.ContentType = (TMDContentType)data.ReadUInt16LittleEndian();
            obj.ContentSize = data.ReadUInt64LittleEndian();
            obj.SHA256Hash = data.ReadBytes(0x20);

            return obj;
        }

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

            obj.ContentIndexOffset = data.ReadUInt16LittleEndian();
            obj.ContentCommandCount = data.ReadUInt16LittleEndian();
            obj.UnhashedContentRecordsSHA256Hash = data.ReadBytes(0x20);

            return obj;
        }

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

            obj.TitleIDDependencyList = data.ReadBytes(0x180);
            obj.Reserved1 = data.ReadBytes(0x180);
            obj.CoreVersion = data.ReadUInt32LittleEndian();
            obj.Reserved2 = data.ReadBytes(0xFC);
            obj.IconData = data.ReadBytes(0x36C0);

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a Ticket
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="fromCdn">Indicates if the ticket is from CDN</param>
        /// <returns>Filled Ticket on success, null on error</returns>
        public static Ticket? ParseTicket(Stream data, bool fromCdn = false)
        {
            var obj = new Ticket();

            obj.SignatureType = (SignatureType)data.ReadUInt32LittleEndian();
            switch (obj.SignatureType)
            {
                case SignatureType.RSA_4096_SHA1:
                case SignatureType.RSA_4096_SHA256:
                    obj.SignatureSize = 0x200;
                    obj.PaddingSize = 0x3C;
                    break;

                case SignatureType.RSA_2048_SHA1:
                case SignatureType.RSA_2048_SHA256:
                    obj.SignatureSize = 0x100;
                    obj.PaddingSize = 0x3C;
                    break;

                case SignatureType.ECDSA_SHA1:
                case SignatureType.ECDSA_SHA256:
                    obj.SignatureSize = 0x3C;
                    obj.PaddingSize = 0x40;
                    break;

                default:
                    return null;
            }

            obj.Signature = data.ReadBytes(obj.SignatureSize);
            obj.Padding = data.ReadBytes(obj.PaddingSize);
            byte[] issuer = data.ReadBytes(0x40);
            obj.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0');
            obj.ECCPublicKey = data.ReadBytes(0x3C);
            obj.Version = data.ReadByteValue();
            obj.CaCrlVersion = data.ReadByteValue();
            obj.SignerCrlVersion = data.ReadByteValue();
            obj.TitleKey = data.ReadBytes(0x10);
            obj.Reserved1 = data.ReadByteValue();
            obj.TicketID = data.ReadUInt64LittleEndian();
            obj.ConsoleID = data.ReadUInt32LittleEndian();
            obj.TitleID = data.ReadUInt64LittleEndian();
            obj.Reserved2 = data.ReadBytes(2);
            obj.TicketTitleVersion = data.ReadUInt16LittleEndian();
            obj.Reserved3 = data.ReadBytes(8);
            obj.LicenseType = data.ReadByteValue();
            obj.CommonKeyYIndex = data.ReadByteValue();
            obj.Reserved4 = data.ReadBytes(0x2A);
            obj.eShopAccountID = data.ReadUInt32LittleEndian();
            obj.Reserved5 = data.ReadByteValue();
            obj.Audit = data.ReadByteValue();
            obj.Reserved6 = data.ReadBytes(0x42);
            obj.Limits = new uint[0x10];
            for (int i = 0; i < obj.Limits.Length; i++)
            {
                obj.Limits[i] = data.ReadUInt32LittleEndian();
            }

            // Seek to the content index size
            data.SeekIfPossible(4, SeekOrigin.Current);

            // Read the size (big-endian)
            obj.ContentIndexSize = data.ReadUInt32BigEndian();

            // Seek back to the start of the content index
            data.SeekIfPossible(-8, SeekOrigin.Current);

            obj.ContentIndex = data.ReadBytes((int)obj.ContentIndexSize);

            // Certificates only exist in standalone CETK files
            if (fromCdn)
            {
                obj.CertificateChain = new Certificate[2];
                for (int i = 0; i < 2; i++)
                {
                    var certificate = ParseCertificate(data);
                    if (certificate is null)
                        return null;

                    obj.CertificateChain[i] = certificate;
                }
            }

            return obj;
        }

        /// <summary>
        /// Parse a Stream into a title metadata
        /// </summary>
        /// <param name="data">Stream to parse</param>
        /// <param name="fromCdn">Indicates if the ticket is from CDN</param>
        /// <returns>Filled title metadata on success, null on error</returns>
        public static TitleMetadata? ParseTitleMetadata(Stream data, bool fromCdn = false)
        {
            var obj = new TitleMetadata();

            obj.SignatureType = (SignatureType)data.ReadUInt32LittleEndian();
            switch (obj.SignatureType)
            {
                case SignatureType.RSA_4096_SHA1:
                case SignatureType.RSA_4096_SHA256:
                    obj.SignatureSize = 0x200;
                    obj.PaddingSize = 0x3C;
                    break;

                case SignatureType.RSA_2048_SHA1:
                case SignatureType.RSA_2048_SHA256:
                    obj.SignatureSize = 0x100;
                    obj.PaddingSize = 0x3C;
                    break;

                case SignatureType.ECDSA_SHA1:
                case SignatureType.ECDSA_SHA256:
                    obj.SignatureSize = 0x3C;
                    obj.PaddingSize = 0x40;
                    break;

                default:
                    return null;
            }

            obj.Signature = data.ReadBytes(obj.SignatureSize);
            obj.Padding1 = data.ReadBytes(obj.PaddingSize);
            byte[] issuer = data.ReadBytes(0x40);
            obj.Issuer = Encoding.ASCII.GetString(issuer).TrimEnd('\0');
            obj.Version = data.ReadByteValue();
            obj.CaCrlVersion = data.ReadByteValue();
            obj.SignerCrlVersion = data.ReadByteValue();
            obj.Reserved1 = data.ReadByteValue();
            obj.SystemVersion = data.ReadUInt64LittleEndian();
            obj.TitleID = data.ReadUInt64LittleEndian();
            obj.TitleType = data.ReadUInt32LittleEndian();
            obj.GroupID = data.ReadUInt16LittleEndian();
            obj.SaveDataSize = data.ReadUInt32LittleEndian();
            obj.SRLPrivateSaveDataSize = data.ReadUInt32LittleEndian();
            obj.Reserved2 = data.ReadBytes(4);
            obj.SRLFlag = data.ReadByteValue();
            obj.Reserved3 = data.ReadBytes(0x31);
            obj.AccessRights = data.ReadUInt32LittleEndian();
            obj.TitleVersion = data.ReadUInt16LittleEndian();
            obj.ContentCount = data.ReadUInt16BigEndian();
            obj.BootContent = data.ReadUInt16LittleEndian();
            obj.Padding2 = data.ReadBytes(2);
            obj.SHA256HashContentInfoRecords = data.ReadBytes(0x20);
            obj.ContentInfoRecords = new ContentInfoRecord[64];
            for (int i = 0; i < 64; i++)
            {
                var contentInfoRecord = ParseContentInfoRecord(data);
                obj.ContentInfoRecords[i] = contentInfoRecord;
            }

            obj.ContentChunkRecords = new ContentChunkRecord[obj.ContentCount];
            for (int i = 0; i < obj.ContentCount; i++)
            {
                var contentChunkRecord = ParseContentChunkRecord(data);
                obj.ContentChunkRecords[i] = contentChunkRecord;
            }

            // Certificates only exist in standalone TMD files
            if (fromCdn)
            {
                obj.CertificateChain = new Certificate[2];
                for (int i = 0; i < 2; i++)
                {
                    var certificate = ParseCertificate(data);
                    if (certificate is null)
                        return null;

                    obj.CertificateChain[i] = certificate;
                }
            }

            return obj;
        }
    }
}
