import { bundled } from './bundled.js';
import { base64ToBytes } from './utils.js';
import yescryptWasm, { MainModule } from './yescrypt_wasm.js';

export * from './utils.js';

type yescrypt_kdf_wasm = (
    passwd: number,
    passwdLen: number,
    salt: number,
    saltLen: number,
    N: bigint,
    r: number,
    p: number,
    t: number,
) => number;

// Returns 0 on success, -1 on failure (output buffer is zeroed on failure).
type yescrypt_kdf_full = (
    flags: number,
    passwd: number,
    passwdLen: number,
    salt: number,
    saltLen: number,
    N: bigint,
    r: number,
    p: number,
    t: number,
    g: number,
    output: number,
    outputLen: number,
) => number;

// Returns a pointer to a freshly malloc'd, NUL-terminated C string, or 0 (NULL)
// on failure. The caller is responsible for _free()'ing a non-zero pointer.
type yescrypt_hash_full_ptr = (
    flags: number,
    passwd: number,
    passwdLen: number,
    salt: number,
    saltLen: number,
    N: bigint,
    r: number,
    p: number,
    t: number,
    g: number,
) => number;

/**
 * yescrypt flag constants (see yescrypt-c/yescrypt.h). OR these together to
 * build a custom `flags` value. YESCRYPT_DEFAULTS is what the yescrypt_* helpers
 * use by default; scrypt_* helpers use 0 (classic scrypt).
 */
export const YESCRYPT_WORM = 1;
export const YESCRYPT_RW = 0x002;
export const YESCRYPT_ROUNDS_3 = 0x000;
export const YESCRYPT_ROUNDS_6 = 0x004;
export const YESCRYPT_GATHER_1 = 0x000;
export const YESCRYPT_GATHER_2 = 0x008;
export const YESCRYPT_GATHER_4 = 0x010;
export const YESCRYPT_GATHER_8 = 0x018;
export const YESCRYPT_SIMPLE_1 = 0x000;
export const YESCRYPT_SIMPLE_2 = 0x020;
export const YESCRYPT_SIMPLE_4 = 0x040;
export const YESCRYPT_SIMPLE_8 = 0x060;
export const YESCRYPT_SBOX_6K = 0x000;
export const YESCRYPT_SBOX_12K = 0x080;
export const YESCRYPT_SBOX_24K = 0x100;
export const YESCRYPT_SBOX_48K = 0x180;
export const YESCRYPT_SBOX_96K = 0x200;
export const YESCRYPT_SBOX_192K = 0x280;
export const YESCRYPT_SBOX_384K = 0x300;
export const YESCRYPT_SBOX_768K = 0x380;
export const YESCRYPT_RW_DEFAULTS =
    YESCRYPT_RW | YESCRYPT_ROUNDS_6 | YESCRYPT_GATHER_4 | YESCRYPT_SIMPLE_2 | YESCRYPT_SBOX_12K;
export const YESCRYPT_DEFAULTS = YESCRYPT_RW_DEFAULTS;

const FULL_KDF_ARGS = [
    'number', // flags
    'number', // passwd
    'number', // passwdLen
    'number', // salt
    'number', // saltLen
    'number', // N (bigint)
    'number', // r
    'number', // p
    'number', // t
    'number', // g
    'number', // output
    'number', // outputLen
] as const;

const FULL_HASH_ARGS = [
    'number', // flags
    'number', // passwd
    'number', // passwdLen
    'number', // salt
    'number', // saltLen
    'number', // N (bigint)
    'number', // r
    'number', // p
    'number', // t
    'number', // g
] as const;

/**
 * Validate the cost parameters against yescrypt's documented constraints
 * (see crypto_scrypt / yescrypt_kdf in yescrypt-c/yescrypt.h). Throwing here
 * gives callers a clear error instead of an all-zero "hash".
 */
function validateParams(N: number, r: number, p: number): void {
    if (!Number.isInteger(N) || N < 2 || (N & (N - 1)) !== 0) {
        throw new RangeError(`yescrypt: N must be a power of two greater than 1 (got ${N})`);
    }
    if (!Number.isInteger(r) || r < 1) {
        throw new RangeError(`yescrypt: r must be a positive integer (got ${r})`);
    }
    if (!Number.isInteger(p) || p < 1) {
        throw new RangeError(`yescrypt: p must be a positive integer (got ${p})`);
    }
    if (r * p >= 1 << 30) {
        throw new RangeError(`yescrypt: r * p must be < 2^30 (got ${r * p})`);
    }
}

export class Yescrypt {
    nByte: number;
    // The generated MainModule typings omit the heap views that Emscripten
    // exposes at runtime, so widen with the ones we touch directly.
    Module: MainModule & { HEAPU8: Uint8Array };
    scrypt_kdf_wasm: yescrypt_kdf_wasm;
    yescrypt_kdf_wasm: yescrypt_kdf_wasm;
    yescrypt_kdf_full_: yescrypt_kdf_full;
    yescrypt_hash_full_: yescrypt_hash_full_ptr;
    scrypt_hash_full_: yescrypt_hash_full_ptr;

    constructor(Module: MainModule & { HEAPU8: Uint8Array }) {
        this.nByte = 1;
        this.Module = Module;
        // Legacy fixed-flag KDF entry points kept for backward compatibility.
        this.scrypt_kdf_wasm = this.Module.cwrap('scrypt_kdf_wasm', 'number', [
            'number',
            'number',
            'number',
            'number',
            'number',
            'number',
            'number',
            'number',
        ]) as yescrypt_kdf_wasm;
        this.yescrypt_kdf_wasm = this.Module.cwrap('yescrypt_kdf_wasm', 'number', [
            'number',
            'number',
            'number',
            'number',
            'number',
            'number',
            'number',
            'number',
        ]) as yescrypt_kdf_wasm;
        // Full-control entry points (return status / pointer so we can surface errors).
        this.yescrypt_kdf_full_ = this.Module.cwrap('yescrypt_kdf_full', 'number', [
            ...FULL_KDF_ARGS,
        ]) as yescrypt_kdf_full;
        this.yescrypt_hash_full_ = this.Module.cwrap('yescrypt_hash_full_str', 'number', [
            ...FULL_HASH_ARGS,
        ]) as yescrypt_hash_full_ptr;
        this.scrypt_hash_full_ = this.Module.cwrap('scrypt_hash_full', 'number', [
            ...FULL_HASH_ARGS,
        ]) as yescrypt_hash_full_ptr;
    }

    static async init() {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        if (typeof (globalThis as any).WebAssembly === 'undefined') {
            throw new Error('WebAssembly is not enabled with this browser');
        }

        const wasmBinary = base64ToBytes(bundled);

        const module = await yescryptWasm({
            wasmBinary,
            locateFile: (file: string) => file,
        });

        return new Yescrypt(module as MainModule & { HEAPU8: Uint8Array });
    }

    // https://stackoverflow.com/questions/41875728/pass-a-javascript-array-as-argument-to-a-webassembly-function
    // Takes an Uint8Array, copies it to the heap and returns a pointer.
    // Allocates one extra byte and NUL-terminates it so that any strlen-based
    // consumer in the C code is safe even though the wrappers pass explicit
    // lengths.
    arrayToPtr(array: Uint8Array): number {
        const ptr = this.Module._malloc((array.length + 1) * this.nByte);
        if (!ptr) {
            throw new Error('yescrypt: out of WASM memory (malloc failed)');
        }
        // Re-read HEAPU8 *after* _malloc: with ALLOW_MEMORY_GROWTH the heap may
        // have grown and the previous typed-array view become detached.
        const heap = this.Module.HEAPU8;
        const base = ptr / this.nByte;
        heap.set(array, base);
        heap[base + array.length] = 0;
        return ptr;
    }

    // Takes a pointer and  array length, and returns a Uint8Array from the heap
    ptrToArray(ptr: number, length: number): Uint8Array {
        const array = new Uint8Array(length);
        const pos = ptr / this.nByte;
        array.set(this.Module.HEAPU8.subarray(pos, pos + length));
        return array;
    }

    freePtr(ptr: number) {
        if (ptr) {
            this.Module._free(ptr);
        }
    }

    private runKdf(
        flags: number,
        passwd: Uint8Array,
        salt: Uint8Array,
        N: number,
        r: number,
        p: number,
        t: number,
        g: number,
        dklen: number,
    ): Uint8Array {
        validateParams(N, r, p);
        if (!Number.isInteger(dklen) || dklen < 1) {
            throw new RangeError(`yescrypt: dklen must be a positive integer (got ${dklen})`);
        }

        let passwdPtr = 0;
        let saltPtr = 0;
        let outPtr = 0;
        try {
            passwdPtr = this.arrayToPtr(passwd);
            saltPtr = this.arrayToPtr(salt);
            outPtr = this.Module._malloc(dklen);
            if (!outPtr) {
                throw new Error('yescrypt: out of WASM memory (malloc failed)');
            }

            const status = this.yescrypt_kdf_full_(
                flags,
                passwdPtr,
                passwd.length,
                saltPtr,
                salt.length,
                BigInt(N),
                r,
                p,
                t,
                g,
                outPtr,
                dklen,
            );
            if (status !== 0) {
                throw new Error(
                    `yescrypt: KDF failed (invalid parameters or out of memory; N=${N}, r=${r}, p=${p})`,
                );
            }

            return this.ptrToArray(outPtr, dklen);
        } finally {
            this.freePtr(passwdPtr);
            this.freePtr(saltPtr);
            this.freePtr(outPtr);
        }
    }

    private runHash(
        impl: yescrypt_hash_full_ptr,
        label: string,
        flags: number,
        passwd: Uint8Array,
        salt: Uint8Array,
        N: number,
        r: number,
        p: number,
        t: number,
        g: number,
    ): string {
        validateParams(N, r, p);

        let passwdPtr = 0;
        let saltPtr = 0;
        let resultPtr = 0;
        try {
            passwdPtr = this.arrayToPtr(passwd);
            saltPtr = this.arrayToPtr(salt);
            resultPtr = impl(flags, passwdPtr, passwd.length, saltPtr, salt.length, BigInt(N), r, p, t, g);
            if (!resultPtr) {
                throw new Error(
                    `yescrypt: ${label} failed (invalid parameters or out of memory; N=${N}, r=${r}, p=${p})`,
                );
            }
            return this.Module.UTF8ToString(resultPtr);
        } finally {
            this.freePtr(passwdPtr);
            this.freePtr(saltPtr);
            // The C helper malloc'd the result string; free it here.
            this.freePtr(resultPtr);
        }
    }

    scrypt_kdf(
        passwd: Uint8Array,
        salt: Uint8Array,
        N = 4096,
        r = 32,
        p = 1,
        t = 0,
        flags = 0,
        g = 0,
        dklen = 64,
    ): Uint8Array {
        return this.runKdf(flags, passwd, salt, N, r, p, t, g, dklen);
    }

    yescrypt_kdf(
        passwd: Uint8Array,
        salt: Uint8Array,
        N = 4096,
        r = 32,
        p = 1,
        t = 0,
        flags = YESCRYPT_DEFAULTS,
        g = 0,
        dklen = 64,
    ): Uint8Array {
        return this.runKdf(flags, passwd, salt, N, r, p, t, g, dklen);
    }

    scrypt_hash(
        passwd: Uint8Array,
        salt: Uint8Array,
        N = 4096,
        r = 32,
        p = 1,
        t = 0,
        flags = 0,
        g = 0,
    ): string {
        return this.runHash(this.scrypt_hash_full_, 'scrypt_hash', flags, passwd, salt, N, r, p, t, g);
    }

    yescrypt_hash(
        passwd: Uint8Array,
        salt: Uint8Array,
        N = 4096,
        r = 32,
        p = 1,
        t = 0,
        flags = YESCRYPT_DEFAULTS,
        g = 0,
    ): string {
        return this.runHash(this.yescrypt_hash_full_, 'yescrypt_hash', flags, passwd, salt, N, r, p, t, g);
    }
}
