import {
    getUint64BigEndian,
    setInt64BigEndian,
    setInt64LittleEndian,
} from "@yume-chan/no-data-view";

/**
 * Gets the `BigInt` value at the specified byte offset and length from the start of the view. There is
 * no alignment constraint; multi-byte values may be fetched from any offset.
 *
 * Only supports Big-Endian, because that's what ADB uses.
 * @param byteOffset The place in the buffer at which the value should be retrieved.
 */
export function getBigUint(
    array: Uint8Array,
    byteOffset: number,
    length: number,
): bigint {
    let result = 0n;

    // Currently `length` must be a multiplication of 8
    // Support for arbitrary length can be easily added

    for (let i = byteOffset; i < byteOffset + length; i += 8) {
        result <<= 64n;
        const value = getUint64BigEndian(array, i);
        result |= value;
    }

    return result;
}

/**
 * Stores an arbitrary-precision positive `BigInt` value at the specified byte offset from the start of the view.
 * @param byteOffset The place in the buffer at which the value should be set.
 * @param length The number of bytes to set.
 * @param value The value to set.
 * @param littleEndian If `false` or `undefined`, a big-endian value should be written,
 * otherwise a little-endian value should be written.
 */

// eslint-disable-next-line @typescript-eslint/max-params
export function setBigUint(
    array: Uint8Array,
    byteOffset: number,
    length: number,
    value: bigint,
    littleEndian?: boolean,
) {
    if (littleEndian) {
        while (value > 0n) {
            setInt64LittleEndian(array, byteOffset, value);
            byteOffset += 8;
            value >>= 64n;
        }
    } else {
        let position = byteOffset + length - 8;
        while (value > 0n) {
            setInt64BigEndian(array, position, value);
            position -= 8;
            value >>= 64n;
        }
    }
}

// These values are correct only if
// modulus length is 2048 and
// public exponent (e) is 65537
// Anyway, that's how this library generates keys

// To support other parameters,
// a proper ASN.1 parser can be used

// References:
//
//   https://tools.ietf.org/html/rfc8017#appendix-A.1.2
//   PKCS #1: RSA Cryptography Specifications Version 2.2
//     A.1.2.  RSA Private Key Syntax
//
//   https://lapo.it/asn1js/
//   https://github.com/lapo-luchini/asn1js
//   ASN.1 JavaScript decoder
//
//   https://www.itu.int/rec/T-REC-X.690-201508-I/en
//   X.690: Specification of Distinguished Encoding Rules (DER)

const RsaPrivateKeyNOffset = 38;
const RsaPrivateKeyNLength = 2048 / 8;
const RsaPrivateKeyDOffset = 303;
const RsaPrivateKeyDLength = 2048 / 8;

export function rsaParsePrivateKey(key: Uint8Array): [n: bigint, d: bigint] {
    const n = getBigUint(key, RsaPrivateKeyNOffset, RsaPrivateKeyNLength);
    const d = getBigUint(key, RsaPrivateKeyDOffset, RsaPrivateKeyDLength);
    return [n, d];
}

function nonNegativeMod(m: number, d: number) {
    const r = m % d;
    if (r > 0) {
        return r;
    }
    return r + (d > 0 ? d : -d);
}

// https://en.wikipedia.org/wiki/Modular_multiplicative_inverse
// Solve for the smallest positive `x` in the equation `a * x ≡ 1 (mod m)`,
// or in other words, `a * x % m = 1`
// Taken from https://stackoverflow.com/a/51562038
// Only used with numbers smaller than 2^32 so doesn't need BigInt
export function modInverse(a: number, m: number) {
    a = nonNegativeMod(a, m);
    if (!a || m < 2) {
        return NaN; // invalid input
    }
    // find the gcd
    const s = [];
    let b = m;
    while (b) {
        [a, b] = [b, a % b];
        s.push({ a, b });
    }
    /* istanbul ignore next */
    if (a !== 1) {
        return NaN; // inverse does not exists
    }
    // find the inverse
    let x = 1;
    let y = 0;
    for (let i = s.length - 2; i >= 0; i -= 1) {
        [x, y] = [y, x - y * Math.floor(s[i]!.a / s[i]!.b)];
    }
    return nonNegativeMod(y, m);
}

const ModulusLengthInBytes = 2048 / 8;
const ModulusLengthInWords = ModulusLengthInBytes / 4;

export function adbGetPublicKeySize() {
    return 4 + 4 + ModulusLengthInBytes + ModulusLengthInBytes + 4;
}

export function adbGeneratePublicKey(privateKey: Uint8Array): Uint8Array;
export function adbGeneratePublicKey(
    privateKey: Uint8Array,
    output: Uint8Array,
): number;
export function adbGeneratePublicKey(
    privateKey: Uint8Array,
    output?: Uint8Array,
): Uint8Array | number {
    // cspell: ignore: mincrypt
    // Android 6 and earlier has its own encryption library called mincrypt
    // This is the RSA public key format used by mincrypt:
    // https://android.googlesource.com/platform/system/core/+/bb0c180e62703c2068a1b2c9f8ba6d634bf1553c/include/mincrypt/rsa.h#46
    // `n0inv` and `rr` are pre-calculated to speed up RSA operations

    // Android 7 switched its encryption library to BoringSSL, but still keeps the key format:
    // https://android.googlesource.com/platform/system/core.git/+/91784040db2b9273687f88d8b95f729d4a61ecc2/libcrypto_utils/android_pubkey.cpp#38
    // Except when reading a key, `n0inv` and `rr` are ignored (they are still populated when generating a key):
    // https://android.googlesource.com/platform/system/core.git/+/91784040db2b9273687f88d8b95f729d4a61ecc2/libcrypto_utils/android_pubkey.cpp#55

    // The public key is a struct (in little endian) of:
    //
    // [
    //   modulusLengthInWords, // 32-bit integer, a "word" is 32-bit so it must be 2048 / 8 / 4
    //                         // (the comment in Android source code is incorrect saying "This must be ANDROID_PUBKEY_MODULUS_SIZE")
    //   n0inv,                // 32-bit integer, the modular inverse of (lower 32 bits of `n`)
    //   modulus,              // `n`
    //   rr,                   // Montgomery parameter R^2
    //   exponent,             // 32-bit integer, must be 3 or 65537
    // ]

    let outputType: "Uint8Array" | "number";
    const outputLength = adbGetPublicKeySize();
    if (!output) {
        output = new Uint8Array(outputLength);
        outputType = "Uint8Array";
    } else {
        if (output.length < outputLength) {
            throw new TypeError("output buffer is too small");
        }

        outputType = "number";
    }

    const outputView = new DataView(
        output.buffer,
        output.byteOffset,
        output.length,
    );
    let outputOffset = 0;

    // modulusLengthInWords
    outputView.setUint32(outputOffset, ModulusLengthInWords, true);
    outputOffset += 4;

    // extract `n` from private key
    const [n] = rsaParsePrivateKey(privateKey);

    // Calculate `n0inv`
    const n0inv = -modInverse(Number(n % 2n ** 32n), 2 ** 32);
    outputView.setInt32(outputOffset, n0inv, true);
    outputOffset += 4;

    // Write `n` (a.k.a. `modulus`)
    setBigUint(output, outputOffset, ModulusLengthInBytes, n, true);
    outputOffset += ModulusLengthInBytes;

    // Calculate rr = (2 ** (rsa_size)) ** 2 % n
    const rr = 2n ** 4096n % n;
    setBigUint(output, outputOffset, ModulusLengthInBytes, rr, true);
    outputOffset += ModulusLengthInBytes;

    // exponent
    outputView.setUint32(outputOffset, 65537, true);
    outputOffset += 4;

    if (outputType === "Uint8Array") {
        return output;
    } else {
        return outputLength;
    }
}

/**
 * Modular exponentiation.
 *
 * Calculate `(base ** exponent) % modulus` without actually calculating `(base ** exponent)`.
 *
 * See https://en.wikipedia.org/wiki/Modular_exponentiation#Implementation_in_Lua
 */
export function powMod(
    base: bigint,
    exponent: bigint,
    modulus: bigint,
): bigint {
    if (modulus === 1n) {
        return 0n;
    }

    let r = 1n;
    base = base % modulus;

    while (exponent > 0n) {
        if (BigInt.asUintN(1, exponent) === 1n) {
            r = (r * base) % modulus;
        }

        base = (base * base) % modulus;
        exponent >>= 1n;
    }

    return r;
}

export const SHA1_DIGEST_LENGTH = 20;

export const ASN1_SEQUENCE = 0x30;
export const ASN1_OCTET_STRING = 0x04;
export const ASN1_NULL = 0x05;
export const ASN1_OID = 0x06;

// PKCS#1 SHA-1 hash digest info
export const SHA1_DIGEST_INFO = new Uint8Array([
    ASN1_SEQUENCE,
    0x0d + SHA1_DIGEST_LENGTH,
    ASN1_SEQUENCE,
    0x09,
    // SHA-1 (1 3 14 3 2 26)
    ASN1_OID,
    0x05,
    1 * 40 + 3,
    14,
    3,
    2,
    26,
    ASN1_NULL,
    0x00,
    ASN1_OCTET_STRING,
    SHA1_DIGEST_LENGTH,
]);

// SubtleCrypto.sign() will hash the given data and sign the hash
// But we don't need the hashing step
// (In another word, ADB just requires the client to
// encrypt the given data with its private key)
// However SubtileCrypto.encrypt() doesn't accept 'RSASSA-PKCS1-v1_5' algorithm
// So we need to implement the encryption by ourself
export function rsaSign(privateKey: Uint8Array, data: Uint8Array): Uint8Array {
    const [n, d] = rsaParsePrivateKey(privateKey);

    // PKCS#1 padding
    const padded = new Uint8Array(256);
    let index = 0;

    padded[index] = 0;
    index += 1;

    padded[index] = 1;
    index += 1;

    const fillLength =
        padded.length - SHA1_DIGEST_INFO.length - data.length - 1;
    while (index < fillLength) {
        padded[index] = 0xff;
        index += 1;
    }

    padded[index] = 0;
    index += 1;

    padded.set(SHA1_DIGEST_INFO, index);
    index += SHA1_DIGEST_INFO.length;

    padded.set(data, index);

    // Encryption
    // signature = padded ** d % n
    const signature = powMod(getBigUint(padded, 0, padded.length), d, n);

    // `padded` is not used anymore,
    // re-use the buffer to store the result
    setBigUint(padded, 0, padded.length, signature, false);

    return padded;
}
