<?php


namespace CubiStore\Web\Utils;

use RuntimeException;

class ApkSignUtils
{
    const ZIP_END_OF_CENTRAL_DIRECTORY_RECORD = "PK\x05\x06";
    const ZIP_CENTRAL_DIRECTORY_RECORD = "PK\x01\x02";

    const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_LOCATOR = "PK\x07\x06";
    const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD = "PK\x06\x06";

    public static function findSignature($file)
    {
        self::tryAPKSignatureV2($file);
    }

    public static function tryAPKSignatureV2($file)
    {
        // APK Signature Block is located -before- the Central Directory Record
        // Try finding offset of that
        $offset = static::findStartOfCentralDirectoryRecord($file);

        $fh = fopen($file, 'r');
        if ($fh === false) {
            throw new RuntimeException("Couldn't open file '$file'");
        }

        try {
            if (fseek($fh, $offset - 24) !== 0) {
                throw new RuntimeException("Couldn't seek in file '$file'");
            }

            $data = fread($fh, 24);

            if ($data === false) {
                throw new RuntimeException("Couldn't read file '$file'");
            }

            if (substr($data, 8) === "APK Sig Block 42") {
                $blockSize = Bytes::readUint8LE($data);
                static::readAPKSignatureInfo($fh, $offset - $blockSize, $blockSize);
            }
        } finally {
            fclose($fh);
        }
    }

    public static function findStartOfCentralDirectoryRecord($file): int
    {
        $size = filesize($file);
        if ($size === false) {
            throw new RuntimeException("Couldn't determine file size of file '$file'");
        }

        $chunkSize = 4096;
        // Make sure we keep above 0
        $offset = max(0, $size - $chunkSize);

        $fh = fopen($file, 'r');
        if ($fh === false) {
            throw new RuntimeException("Couldn't open file '$file'");
        }

        try {
            while (true) {
                if (fseek($fh, $offset) !== 0) {
                    throw new RuntimeException("Couldn't seek in file '$file'");
                }

                // Read chunk of data
                $data = fread($fh, $chunkSize);

                if ($data === false) {
                    throw new RuntimeException("Couldn't read file '$file'");
                }

                $signatureOffset = 0;
                // Try resolving central directory offset from this chunk
                if (static::tryResolveCentralDirectoryOffset($fh, $file, $data, $chunkSize, $signatureOffset)) {
                    // If found seek to offset
                    if (fseek($fh, $signatureOffset) !== 0) {
                        throw new RuntimeException("Couldn't seek in file '$file'");
                    }

                    // Read header of block
                    $centralDirectorySignature = fread($fh, 4);

                    if ($centralDirectorySignature === false) {
                        throw new RuntimeException("Couldn't read file '$file'");
                    }

                    // Check if block is Central Directory Record
                    if ($centralDirectorySignature === static::ZIP_CENTRAL_DIRECTORY_RECORD) {
                        return $signatureOffset;
                    }
                }

                $offset -= $chunkSize;

                // Offset is set to 0 if it's negative
                // but 0 - chunkSize = -chunkSize
                // which means that we couldn't find it
                if ($offset <= -$chunkSize) {
                    throw new RuntimeException("Can't EndOfCentralDirectoryRecord in '$file");
                }

                if ($offset < 0) {
                    $offset = 0;
                }
            }
        } finally {
            fclose($fh);
        }

        throw new RuntimeException("Unreachable code reached");
    }

    protected static function tryResolveCentralDirectoryOffset($fh, string $file, string $data, int $chunkSize, int &$signatureOffset): bool
    {
        // Try to find signature
        $sigOffset = strrpos($data, static::ZIP_END_OF_CENTRAL_DIRECTORY_RECORD);

        // Found signature, check if valid, or part of comment
        if ($sigOffset === false) {
            return false;
        }

        // Minimal size of EOCD is 22 bytes.
        if ((strlen($data) - $sigOffset) < 22) {
            $moreData = fread($fh, $chunkSize);

            if ($moreData === false) {
                throw new RuntimeException("Couldn't read file '$file'");
            }

            $data .= $moreData;

            if ((strlen($data) - $sigOffset) < 22) {
                // This can't be it, there's no space.
                return false;
            }
        }

        // read offset of Central Directory Record
        $offset = Bytes::readUint4LE($data, $sigOffset + 16);

        //
        // - Fetch zip64 end of central directory record locator
        //

        // if offset is less than 20, we need more data to fetch the zip64 end of central directory record locator
        if ($sigOffset < 20) {
            if (fseek($fh, $sigOffset - 20) !== 0) {
                throw new RuntimeException("Couldn't seek in file '$file'");
            }

            $zip64Locator = fread($fh, 20);

            if ($zip64Locator === false) {
                throw new RuntimeException("Couldn't read file '$file'");
            }
        } else {
            $zip64Locator = substr($data, $sigOffset - 20, 20);
        }

        // Check if block is zip64 end of central directory record locator if so, read that.
        if (strlen($zip64Locator) === 20 && substr($zip64Locator, 0, 4) === static::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_LOCATOR) {
            $zip64EOCDOffset = Bytes::readUint8LE($zip64Locator, 8);
            return static::readZip64EOCD($fh, $file, $zip64EOCDOffset, $signatureOffset, $offset);
        }

        // Set offset into reference
        $signatureOffset = $offset;
        return true;
    }

    private static function readZip64EOCD($fh, string $file, int $zip64EOCDOffset, int &$signatureOffset, int $legacyOffset): bool
    {
        if (fseek($fh, $zip64EOCDOffset) !== 0) {
            throw new RuntimeException("Couldn't seek in file '$file'");
        }

        $zip64EOCD = fread($fh, 56);

        if ($zip64EOCD === false) {
            throw new RuntimeException("Couldn't read file '$file'");
        }

        // Check if offset from locator is zip64 end of central directory record
        if (substr($zip64EOCD, 0, 4) !== static::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD) {
            return false;
        }

        $zip64Offset = Bytes::readUint8LE($zip64EOCD, 48);

        if ($legacyOffset !== 2 * 32 && $legacyOffset != $zip64Offset) {
            throw new RuntimeException("Corrupted ZIP, ZIP64 offset and normal offset don't match up.");
        }

        // Read central directory record offset
        $signatureOffset = $zip64Offset;
        return true;
    }

    private static function readAPKSignatureInfo($fh, int $offset, int $size)
    {
    }
}
