param(
    [string]$ExePath = "",
    [string]$OutPath = ""
)

$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$PayloadPath = Join-Path $ScriptDir "patch_payload.bin"
$InfoPath = Join-Path $ScriptDir "PATCH_PACKAGE_INFO.json"
$Password = "iikarachoko-cn-fullapp-v9-xiaolai-forced"

function Write-Header {
    Write-Host "=============================================================="
    Write-Host "iikarachoko CN Patch Installer v9 - forced xiaolai font"
    Write-Host "=============================================================="
    Write-Host "No Python. No 7-Zip. No runtime extraction/repacking."
    Write-Host "Default input: iikarachoko.exe in this folder or parent folder."
    Write-Host "Default output: iikarachoko_cn.exe next to the original exe."
    Write-Host ""
}

function Add-RequiredTypes {
    $code = @"
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;

public static class TyranoCnFullPatchV9
{
    private static byte[] Magic = new byte[] { 0x54, 0x59, 0x52, 0x41, 0x4E, 0x4F, 0x5F, 0x43, 0x4E, 0x5F, 0x46, 0x55, 0x4C, 0x4C, 0x41, 0x50, 0x50, 0x5F, 0x56, 0x39, 0x5F, 0x41, 0x45, 0x53, 0x00 };

    private static byte[] Concat(params byte[][] arrays)
    {
        int len = 0;
        foreach (byte[] a in arrays) len += a.Length;
        byte[] r = new byte[len];
        int pos = 0;
        foreach (byte[] a in arrays)
        {
            Buffer.BlockCopy(a, 0, r, pos, a.Length);
            pos += a.Length;
        }
        return r;
    }

    private static bool ConstantTimeEquals(byte[] a, byte[] b)
    {
        if (a == null || b == null || a.Length != b.Length) return false;
        int diff = 0;
        for (int i = 0; i < a.Length; i++) diff |= a[i] ^ b[i];
        return diff == 0;
    }

    private static byte[] Sha256(byte[] data)
    {
        using (SHA256 sha = SHA256.Create()) return sha.ComputeHash(data);
    }

    public static string Sha256Hex(byte[] data)
    {
        byte[] hash = Sha256(data);
        StringBuilder sb = new StringBuilder(hash.Length * 2);
        foreach (byte b in hash) sb.Append(b.ToString("x2"));
        return sb.ToString();
    }

    public static string Sha256File(string filePath)
    {
        using (FileStream fs = File.OpenRead(filePath))
        using (SHA256 sha = SHA256.Create())
        {
            byte[] hash = sha.ComputeHash(fs);
            StringBuilder sb = new StringBuilder(hash.Length * 2);
            foreach (byte b in hash) sb.Append(b.ToString("x2"));
            return sb.ToString();
        }
    }

    private static UInt32 ReadUInt32LE(byte[] b, int p)
    {
        return (UInt32)(b[p] | (b[p+1] << 8) | (b[p+2] << 16) | (b[p+3] << 24));
    }

    private static UInt16 ReadUInt16LE(byte[] b, int p)
    {
        return (UInt16)(b[p] | (b[p+1] << 8));
    }

    public static long FindZipStart(string filePath)
    {
        FileInfo fi = new FileInfo(filePath);
        long len = fi.Length;
        if (len < 22) throw new Exception("The exe is too small.");

        int tailLen = (int)Math.Min(len, 22 + 65535 + 4096);
        byte[] tail = new byte[tailLen];
        using (FileStream fs = File.OpenRead(filePath))
        {
            fs.Seek(len - tailLen, SeekOrigin.Begin);
            int read = 0;
            while (read < tailLen)
            {
                int n = fs.Read(tail, read, tailLen - read);
                if (n <= 0) break;
                read += n;
            }
        }

        for (int i = tailLen - 22; i >= 0; i--)
        {
            if (tail[i] == 0x50 && tail[i+1] == 0x4B && tail[i+2] == 0x05 && tail[i+3] == 0x06)
            {
                int commentLen = ReadUInt16LE(tail, i + 20);
                if (i + 22 + commentLen != tailLen) continue;
                UInt32 cdSize = ReadUInt32LE(tail, i + 12);
                UInt32 cdOffset = ReadUInt32LE(tail, i + 16);
                long eocdAbs = len - tailLen + i;
                long zipStart = eocdAbs - ((long)cdOffset + (long)cdSize);
                if (zipStart < 0 || zipStart >= len) continue;
                using (FileStream fs = File.OpenRead(filePath))
                {
                    fs.Seek(zipStart, SeekOrigin.Begin);
                    int b0 = fs.ReadByte();
                    int b1 = fs.ReadByte();
                    int b2 = fs.ReadByte();
                    int b3 = fs.ReadByte();
                    if (b0 == 0x50 && b1 == 0x4B && b2 == 0x03 && b3 == 0x04) return zipStart;
                }
            }
        }
        throw new Exception("No embedded ZIP resource was found in the exe.");
    }

    public static byte[] DecryptPayload(string payloadPath, string password)
    {
        byte[] data = File.ReadAllBytes(payloadPath);
        if (data.Length < Magic.Length + 16 + 16 + 32) throw new Exception("patch_payload.bin is too small or damaged.");
        for (int i = 0; i < Magic.Length; i++)
        {
            if (data[i] != Magic[i]) throw new Exception("patch_payload.bin has an invalid AES payload format.");
        }

        int pos = Magic.Length;
        byte[] salt = new byte[16];
        Buffer.BlockCopy(data, pos, salt, 0, 16); pos += 16;
        byte[] iv = new byte[16];
        Buffer.BlockCopy(data, pos, iv, 0, 16); pos += 16;
        byte[] tag = new byte[32];
        Buffer.BlockCopy(data, pos, tag, 0, 32); pos += 32;
        byte[] cipher = new byte[data.Length - pos];
        Buffer.BlockCopy(data, pos, cipher, 0, cipher.Length);

        byte[] key = Sha256(Concat(Encoding.UTF8.GetBytes(password), salt));
        byte[] expected;
        using (HMACSHA256 h = new HMACSHA256(key)) expected = h.ComputeHash(Concat(Magic, salt, iv, cipher));
        if (!ConstantTimeEquals(tag, expected)) throw new Exception("patch_payload.bin verification failed.");

        using (Aes aes = Aes.Create())
        {
            aes.Key = key;
            aes.IV = iv;
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
            using (MemoryStream ms = new MemoryStream())
            using (CryptoStream cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
            {
                cs.Write(cipher, 0, cipher.Length);
                cs.FlushFinalBlock();
                return ms.ToArray();
            }
        }
    }

    public static void WritePatchedExe(string originalExe, string payloadPath, string password, string expectedAppSha256, string outputExe)
    {
        long zipStart = FindZipStart(originalExe);
        byte[] appZip = DecryptPayload(payloadPath, password);
        string actualSha = Sha256Hex(appZip);
        if (!actualSha.Equals(expectedAppSha256, StringComparison.OrdinalIgnoreCase))
            throw new Exception("Decrypted app package hash mismatch.");
        if (appZip.Length < 4 || appZip[0] != 0x50 || appZip[1] != 0x4B || appZip[2] != 0x03 || appZip[3] != 0x04)
            throw new Exception("Decrypted app package is not a ZIP file.");

        string parent = Path.GetDirectoryName(Path.GetFullPath(outputExe));
        if (!Directory.Exists(parent)) Directory.CreateDirectory(parent);

        byte[] buffer = new byte[1024 * 1024];
        using (FileStream output = new FileStream(outputExe, FileMode.Create, FileAccess.Write, FileShare.None))
        {
            using (FileStream src = File.OpenRead(originalExe))
            {
                long left = zipStart;
                while (left > 0)
                {
                    int want = (int)Math.Min(buffer.Length, left);
                    int n = src.Read(buffer, 0, want);
                    if (n <= 0) throw new EndOfStreamException("Unexpected end while copying exe stub.");
                    output.Write(buffer, 0, n);
                    left -= n;
                }
            }
            output.Write(appZip, 0, appZip.Length);
        }
    }
}
"@
    Add-Type -TypeDefinition $code -Language CSharp
}

function Load-Info {
    if (-not (Test-Path -LiteralPath $InfoPath)) { throw "PATCH_PACKAGE_INFO.json was not found." }
    $text = [System.IO.File]::ReadAllText($InfoPath, [System.Text.Encoding]::UTF8)
    return $text | ConvertFrom-Json
}

function Get-ExeCandidatesInDir([string]$DirPath) {
    if (-not (Test-Path -LiteralPath $DirPath)) { return @() }
    return @(Get-ChildItem -LiteralPath $DirPath -Filter "*.exe" | Where-Object {
        (-not $_.PSIsContainer) -and
        $_.Name -notmatch "_cn\.exe$" -and
        $_.Name -notmatch "_patched\.exe$" -and
        $_.Name -notmatch "^install_" -and
        $_.Name -notmatch "^setup"
    })
}

function Resolve-OriginalExe([string]$GivenPath) {
    if (-not [string]::IsNullOrWhiteSpace($GivenPath)) {
        $p = $GivenPath.Trim('"')
        if (-not [System.IO.Path]::IsPathRooted($p)) { $p = Join-Path (Get-Location).Path $p }
        if (-not (Test-Path -LiteralPath $p)) { throw "Specified exe was not found: $p" }
        return [System.IO.Path]::GetFullPath($p)
    }

    $searchDirs = New-Object System.Collections.Generic.List[string]
    $searchDirs.Add([System.IO.Path]::GetFullPath($ScriptDir))
    $parent = Split-Path -Parent $ScriptDir
    if (-not [string]::IsNullOrWhiteSpace($parent)) { $searchDirs.Add([System.IO.Path]::GetFullPath($parent)) }
    $cwd = [System.IO.Path]::GetFullPath((Get-Location).Path)
    if (-not $searchDirs.Contains($cwd)) { $searchDirs.Add($cwd) }

    foreach ($d in $searchDirs) {
        $preferred = Join-Path $d "iikarachoko.exe"
        if (Test-Path -LiteralPath $preferred) {
            Write-Host "Found original exe: $preferred"
            return [System.IO.Path]::GetFullPath($preferred)
        }
    }

    $all = @()
    foreach ($d in $searchDirs) { foreach ($item in (Get-ExeCandidatesInDir $d)) { $all += $item } }
    $unique = @()
    $seen = @{}
    foreach ($item in $all) {
        $full = [System.IO.Path]::GetFullPath($item.FullName)
        if (-not $seen.ContainsKey($full)) { $seen[$full] = $true; $unique += $item }
    }
    if ($unique.Count -eq 1) {
        Write-Host "Found the only candidate exe: $($unique[0].FullName)"
        return [System.IO.Path]::GetFullPath($unique[0].FullName)
    }
    if ($unique.Count -gt 1) {
        Write-Host "Multiple exe files were found:"
        for ($i = 0; $i -lt $unique.Count; $i = $i + 1) { Write-Host ("  " + ($i + 1) + ". " + $unique[$i].FullName) }
        throw "Drag the original exe onto install_cn_patch.bat or run: install_cn_patch.bat iikarachoko.exe"
    }
    throw "No original iikarachoko.exe was found. Put the patch files in the original game folder."
}

try {
    Write-Header
    if (-not (Test-Path -LiteralPath $PayloadPath)) { throw "patch_payload.bin was not found. Extract the patch zip first." }
    Add-RequiredTypes
    $Info = Load-Info
    $OriginalExe = Resolve-OriginalExe $ExePath

    $origHash = [TyranoCnFullPatchV9]::Sha256File($OriginalExe)
    if ($origHash -ne $Info.original_exe_sha256) {
        Write-Host "[WARN] The original exe hash is different from the patch source."
        Write-Host "       The installer will continue because only the NW.js stub is reused."
        Write-Host "       If the generated exe cannot run, use the matching original version."
    }

    if ([string]::IsNullOrWhiteSpace($OutPath)) {
        $folder = Split-Path -Parent $OriginalExe
        $stem = [System.IO.Path]::GetFileNameWithoutExtension($OriginalExe)
        $OutputExe = Join-Path $folder ($stem + "_cn.exe")
    } else {
        if ([System.IO.Path]::IsPathRooted($OutPath)) { $OutputExe = $OutPath } else { $OutputExe = Join-Path (Get-Location).Path $OutPath }
    }

    if ([System.IO.Path]::GetFullPath($OutputExe).Equals([System.IO.Path]::GetFullPath($OriginalExe), [System.StringComparison]::OrdinalIgnoreCase)) {
        throw "Output path cannot be the same as original exe."
    }

    Write-Host "Creating patched exe with forced xiaolai font..."
    [TyranoCnFullPatchV9]::WritePatchedExe($OriginalExe, $PayloadPath, $Password, [string]$Info.patched_app_zip_sha256, $OutputExe)
    Write-Host ""
    Write-Host "[DONE] Created: $OutputExe"
    Write-Host "[KEEP] Original exe was not modified: $OriginalExe"
    Write-Host "Run the CN exe from the same folder as the original DLL/nw.pak/locales files."
    exit 0
} catch {
    Write-Host ""
    Write-Host ("[ERROR] " + $_.Exception.Message)
    Write-Host ""
    Write-Host "Check these points:"
    Write-Host "1. Extract the patch zip before running it."
    Write-Host "2. Put the patch files next to iikarachoko.exe."
    Write-Host "3. Run install_cn_patch.bat, not iikarachoko_cn.exe before it is created."
    Write-Host "4. Keep the generated exe in the original game folder with DLL/nw.pak/locales."
    exit 1
}
