using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
using System;
using System.IO;

namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
{
    public class AmiiboBinReader
    {
        private static byte CalculateBCC0(byte[] uid)
        {
            return (byte)(uid[0] ^ uid[1] ^ uid[2] ^ 0x88);
        }

        private static byte CalculateBCC1(byte[] uid)
        {
            return (byte)(uid[3] ^ uid[4] ^ uid[5] ^ uid[6]);
        }

        public static VirtualAmiiboFile ReadBinFile(byte[] fileBytes)
        {
            string keyRetailBinPath = GetKeyRetailBinPath();
            if (string.IsNullOrEmpty(keyRetailBinPath))
            {
                return new VirtualAmiiboFile();
            }

            if (fileBytes.Length == 532)
            {
                int totalPages = 135;
                int pageSize = 4;
                int totalBytes = totalPages * pageSize;

                // add 8 bytes to the end of the file
                byte[] newFileBytes = new byte[totalBytes];
                Array.Copy(fileBytes, newFileBytes, fileBytes.Length);
                fileBytes = newFileBytes;
            }

            AmiiboDecryptor amiiboDecryptor = new(keyRetailBinPath);
            AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(fileBytes);

            byte[] titleId = new byte[8];
            byte[] usedCharacter = new byte[2];
            byte[] variation = new byte[2];
            byte[] amiiboID = new byte[2];
            byte[] setID = new byte[1];
            byte[] initDate = new byte[2];
            byte[] writeDate = new byte[2];
            byte[] writeCounter = new byte[2];
            byte[] appId = new byte[8];
            byte[] settingsBytes = new byte[2];
            byte[] applicationAreas = new byte[216];
            byte[] dataFull = amiiboDump.GetData();
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Data Full Length: {dataFull.Length}");
            byte[] uid = new byte[7];
            Array.Copy(dataFull, 0, uid, 0, 7);

            byte bcc0 = CalculateBCC0(uid);
            byte bcc1 = CalculateBCC1(uid);
            LogDebugData(uid, bcc0, bcc1);
            for (int page = 0; page < 128; page++) // NTAG215 has 128 pages
            {
                int pageStartIdx = page * 4; // Each page is 4 bytes
                byte[] pageData = new byte[4];
                byte[] sourceBytes = dataFull;
                Array.Copy(sourceBytes, pageStartIdx, pageData, 0, 4);
                // Special handling for specific pages
                switch (page)
                {
                    case 0: // Page 0 (UID + BCC0)
                        Logger.Debug?.Print(LogClass.ServiceNfp, "Page 0: UID and BCC0.");
                        break;
                    case 2: // Page 2 (BCC1 + Internal Value)
                        byte internalValue = pageData[1];
                        Logger.Debug?.Print(LogClass.ServiceNfp, $"Page 2: BCC1 + Internal Value 0x{internalValue:X2} (Expected 0x48).");
                        break;
                    case 6:
                        // Bytes 0 and 1 are init date, bytes 2 and 3 are write date
                        Array.Copy(pageData, 0, initDate, 0, 2);
                        Array.Copy(pageData, 2, writeDate, 0, 2);
                        break;
                    case 21:
                        // Bytes 0 and 1 are used character, bytes 2 and 3 are variation
                        Array.Copy(pageData, 0, usedCharacter, 0, 2);
                        Array.Copy(pageData, 2, variation, 0, 2);
                        break;
                    case 22:
                        // Bytes 0 and 1 are amiibo ID, byte 2 is set ID, byte 3 is form data
                        Array.Copy(pageData, 0, amiiboID, 0, 2);
                        setID[0] = pageData[2];
                        break;
                    case 64:
                    case 65:
                        // Extract title ID
                        int titleIdOffset = (page - 64) * 4;
                        Array.Copy(pageData, 0, titleId, titleIdOffset, 4);
                        break;
                    case 66:
                        // Bytes 0 and 1 are write counter
                        Array.Copy(pageData, 0, writeCounter, 0, 2);
                        break;
                    // Pages 76 to 127 are application areas
                    case >= 76 and <= 127:
                        int appAreaOffset = (page - 76) * 4;
                        Array.Copy(pageData, 0, applicationAreas, appAreaOffset, 4);
                        break;
                }
            }

            string usedCharacterStr = Convert.ToHexString(usedCharacter);
            string variationStr = Convert.ToHexString(variation);
            string amiiboIDStr = Convert.ToHexString(amiiboID);
            string setIDStr = Convert.ToHexString(setID);
            string head = usedCharacterStr + variationStr;
            string tail = amiiboIDStr + setIDStr + "02";
            string finalID = head + tail;

            ushort settingsValue = BitConverter.ToUInt16(settingsBytes, 0);
            ushort initDateValue = BitConverter.ToUInt16(initDate, 0);
            ushort writeDateValue = BitConverter.ToUInt16(writeDate, 0);
            DateTime initDateTime = DateTimeFromTag(initDateValue);
            DateTime writeDateTime = DateTimeFromTag(writeDateValue);
            ushort writeCounterValue = BitConverter.ToUInt16(writeCounter, 0);
            string nickName = amiiboDump.AmiiboNickname;
            LogFinalData(titleId, appId, head, tail, finalID, nickName, initDateTime, writeDateTime, settingsValue, writeCounterValue, applicationAreas);

            VirtualAmiiboFile virtualAmiiboFile = new()
            {
                FileVersion = 1,
                TagUuid = uid,
                AmiiboId = finalID,
                NickName = nickName,
                FirstWriteDate = initDateTime,
                LastWriteDate = writeDateTime,
                WriteCounter = writeCounterValue,
            };
            if (writeCounterValue > 0)
            {
                VirtualAmiibo.ApplicationBytes = applicationAreas;
            }

            VirtualAmiibo.NickName = nickName;
            return virtualAmiiboFile;
        }
        public static bool SaveBinFile(string inputFile, byte[] appData)
        {
            Logger.Info?.Print(LogClass.ServiceNfp, "Saving bin file.");
            byte[] readBytes;
            try
            {
                readBytes = File.ReadAllBytes(inputFile);
            }
            catch (Exception ex)
            {
                Logger.Error?.Print(LogClass.ServiceNfp, $"Error reading file: {ex.Message}");
                return false;
            }

            string keyRetailBinPath = GetKeyRetailBinPath();
            if (string.IsNullOrEmpty(keyRetailBinPath))
            {
                Logger.Error?.Print(LogClass.ServiceNfp, "Key retail path is empty.");
                return false;
            }

            if (appData.Length != 216) // Ensure application area size is valid
            {
                Logger.Error?.Print(LogClass.ServiceNfp, "Invalid application data length. Expected 216 bytes.");
                return false;
            }

            if (readBytes.Length == 532)
            {
                // add 8 bytes to the end of the file
                byte[] newFileBytes = new byte[540];
                Array.Copy(readBytes, newFileBytes, readBytes.Length);
                readBytes = newFileBytes;
            }

            AmiiboDecryptor amiiboDecryptor = new(keyRetailBinPath);
            AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(readBytes);

            byte[] oldData = amiiboDump.GetData();
            if (oldData.Length != 540) // Verify the expected length for NTAG215 tags
            {
                Logger.Error?.Print(LogClass.ServiceNfp, "Invalid tag data length. Expected 540 bytes.");
                return false;
            }

            byte[] newData = new byte[oldData.Length];
            Array.Copy(oldData, newData, oldData.Length);

            // Replace application area with appData
            int appAreaOffset = 76 * 4; // Starting page (76) times 4 bytes per page
            Array.Copy(appData, 0, newData, appAreaOffset, appData.Length);

            AmiiboDump encryptedDump = amiiboDecryptor.EncryptAmiiboDump(newData);
            byte[] encryptedData = encryptedDump.GetData();

            if (encryptedData == null || encryptedData.Length != readBytes.Length)
            {
                Logger.Error?.Print(LogClass.ServiceNfp, "Failed to encrypt data correctly.");
                return false;
            }

            inputFile = inputFile.Replace("_modified", string.Empty);
            // Save the encrypted data to file or return it for saving externally
            string outputFilePath = Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_modified.bin");
            try
            {
                File.WriteAllBytes(outputFilePath, encryptedData);
                Logger.Info?.Print(LogClass.ServiceNfp, $"Modified Amiibo data saved to {outputFilePath}.");
                return true;
            }
            catch (Exception ex)
            {
                Logger.Error?.Print(LogClass.ServiceNfp, $"Error saving file: {ex.Message}");
                return false;
            }
        }
        public static bool SaveBinFile(string inputFile, string newNickName)
        {
            Logger.Info?.Print(LogClass.ServiceNfp, "Saving bin file.");
            byte[] readBytes;
            try
            {
                readBytes = File.ReadAllBytes(inputFile);
            }
            catch (Exception ex)
            {
                Logger.Error?.Print(LogClass.ServiceNfp, $"Error reading file: {ex.Message}");
                return false;
            }

            string keyRetailBinPath = GetKeyRetailBinPath();
            if (string.IsNullOrEmpty(keyRetailBinPath))
            {
                Logger.Error?.Print(LogClass.ServiceNfp, "Key retail path is empty.");
                return false;
            }

            if (readBytes.Length == 532)
            {
                // add 8 bytes to the end of the file
                byte[] newFileBytes = new byte[540];
                Array.Copy(readBytes, newFileBytes, readBytes.Length);
                readBytes = newFileBytes;
            }

            AmiiboDecryptor amiiboDecryptor = new(keyRetailBinPath);
            AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(readBytes);
            amiiboDump.AmiiboNickname = newNickName;
            byte[] oldData = amiiboDump.GetData();
            if (oldData.Length != 540) // Verify the expected length for NTAG215 tags
            {
                Logger.Error?.Print(LogClass.ServiceNfp, "Invalid tag data length. Expected 540 bytes.");
                return false;
            }

            byte[] encryptedData = amiiboDecryptor.EncryptAmiiboDump(oldData).GetData();

            if (encryptedData == null || encryptedData.Length != readBytes.Length)
            {
                Logger.Error?.Print(LogClass.ServiceNfp, "Failed to encrypt data correctly.");
                return false;
            }

            inputFile = inputFile.Replace("_modified", string.Empty);
            // Save the encrypted data to file or return it for saving externally
            string outputFilePath = Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_modified.bin");
            try
            {
                File.WriteAllBytes(outputFilePath, encryptedData);
                Logger.Info?.Print(LogClass.ServiceNfp, $"Modified Amiibo data saved to {outputFilePath}.");
                return true;
            }
            catch (Exception ex)
            {
                Logger.Error?.Print(LogClass.ServiceNfp, $"Error saving file: {ex.Message}");
                return false;
            }
        }
        private static void LogDebugData(byte[] uid, byte bcc0, byte bcc1)
        {
            Logger.Debug?.Print(LogClass.ServiceNfp, $"UID: {BitConverter.ToString(uid)}");
            Logger.Debug?.Print(LogClass.ServiceNfp, $"BCC0: 0x{bcc0:X2}, BCC1: 0x{bcc1:X2}");
        }

        private static void LogFinalData(byte[] titleId, byte[] appId, string head, string tail, string finalID, string nickName, DateTime initDateTime, DateTime writeDateTime, ushort settingsValue, ushort writeCounterValue, byte[] applicationAreas)
        {
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Title ID: 0x{Convert.ToHexString(titleId)}");
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Application Program ID: 0x{Convert.ToHexString(appId)}");
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Head: {head}");
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Tail: {tail}");
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Final ID: {finalID}");
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Nickname: {nickName}");
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Init Date: {initDateTime}");
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Write Date: {writeDateTime}");
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Settings: 0x{settingsValue:X4}");
            Logger.Debug?.Print(LogClass.ServiceNfp, $"Write Counter: {writeCounterValue}");
            Logger.Debug?.Print(LogClass.ServiceNfp, "Length of Application Areas: " + applicationAreas.Length);
        }

        private static uint CalculateCRC32(byte[] input)
        {
            uint[] table = new uint[256];
            uint polynomial = 0xEDB88320;
            for (uint i = 0; i < table.Length; ++i)
            {
                uint crc = i;
                for (int j = 0; j < 8; ++j)
                {
                    if ((crc & 1) != 0)
                        crc = (crc >> 1) ^ polynomial;
                    else
                        crc >>= 1;
                }

                table[i] = crc;
            }

            uint result = 0xFFFFFFFF;
            foreach (byte b in input)
            {
                byte index = (byte)((result & 0xFF) ^ b);
                result = (result >> 8) ^ table[index];
            }

            return ~result;
        }

        private static string GetKeyRetailBinPath()
        {
            return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
        }

        public static bool HasAmiiboKeyFile => File.Exists(GetKeyRetailBinPath());


        public static DateTime DateTimeFromTag(ushort dateTimeTag)
        {
            try
            {
                int day = dateTimeTag & 0x1F;
                int month = (dateTimeTag >> 5) & 0x0F;
                int year = (dateTimeTag >> 9) & 0x7F;

                if (day == 0 || month == 0 || month > 12 || day > DateTime.DaysInMonth(2000 + year, month))
                    throw new ArgumentOutOfRangeException(nameof(dateTimeTag), "Invalid date in tag.");

                return new DateTime(2000 + year, month, day);
            }
            catch
            {
                return DateTime.Now;
            }
        }
    }
}
