import { fileURLToPath } from './deps.ts';
import { orderedBinary, setNativeFunctions } from './external.js';
orderedBinary.enableNullTermination();
// probably use Deno.build.os
let version = import.meta.url.match(/@([^/]+)\//)?.[1];
//console.log({version});
let libPath = import.meta.url.startsWith('file:') && fileURLToPath(new URL('build/Release/lmdbx.node', import.meta.url));
if (!libPath || !exists(libPath)) {
    //console.log({ libPath }, 'does not exist')
    libPath = (Deno.env.get('LMDBX_LIB_PATH') || (tmpdir() + '/lmdbx-js-' + (version || '') + '.lib')) as string;
    const ARCH = { x86_64: 'x64', aarch64: 'arm64' }
    if (!exists(libPath)) {
        let os: string = Deno.build.os;
        os = os == 'windows' ? 'win32' : os;
        os += '-' + ARCH[Deno.build.arch];
        let libraryUrl = 'https://cdn.jsdelivr.net/npm/lmdbs@' + (version || 'latest') +
            '/prebuilds/' + os + '/node.abi93' + (os == 'win32' ? '' : '.glibc') + '.node';
        console.log('Download', libraryUrl);
        let response = await fetch(libraryUrl);
        if (response.status == 200) {
            let binaryLibraryBuffer = await response.arrayBuffer();
            Deno.writeFileSync(libPath, new Uint8Array(binaryLibraryBuffer));            
        } else {
            throw new Error('Unable to fetch ' + libraryUrl + ', HTTP response: ' + response.status);
        }
    }
}
let lmdbxLib = Deno.dlopen(libPath, {
    // const char* path, char* keyBuffer, Compression* compression, int jsFlags, int flags, int maxDbs,
    // int maxReaders, mdb_size_t mapSize, int pageSize
	envOpen: { parameters: ['u32', 'u32', 'pointer', 'pointer', 'f64', 'u32', 'u32', 'f64', 'u32'], result: 'i64'},
    closeEnv: { parameters: ['f64'], result: 'void'},
    freeData: { parameters: ['f64'], result: 'void'},
    getAddress: { parameters: ['pointer'], result: 'usize'},
    getMaxKeySize: { parameters: ['f64'], result: 'u32'},
    openDbi: { parameters: ['f64', 'u32', 'pointer', 'u32', 'f64'], result: 'i64'},
    getDbi: { parameters: ['f64'], result: 'u32'},
    readerCheck: { parameters: ['f64'], result: 'i32'},
    beginTxn: { parameters: ['f64', 'u32'], result: 'i64'},
    resetTxn: { parameters: ['f64'], result: 'void'},
    renewTxn: { parameters: ['f64'], result: 'i32'},
    abortTxn: { parameters: ['f64'], result: 'void'},
    commitTxn: { parameters: ['f64'], result: 'i32'},
    commitEnvTxn: { parameters: ['f64'], result: 'i32'},
    abortEnvTxn: { parameters: ['f64'], result: 'void'},
    getError: { parameters: ['i32', 'pointer'], result: 'void'},
    dbiGetByBinary: { parameters: ['f64', 'u32'], result: 'u32'},    
    openCursor: { parameters: ['f64'], result: 'i64'},
    cursorRenew: { parameters: ['f64'], result: 'i32'},
    cursorClose: { parameters: ['f64'], result: 'i32'},
    cursorIterate: { parameters: ['f64'], result: 'i32'},
    cursorPosition: { parameters: ['f64', 'u32', 'u32', 'u32', 'f64'], result: 'i32'},
    cursorCurrentValue: { parameters: ['f64'], result: 'i32'},
    startWriting: { parameters: ['f64', 'f64'], nonblocking: true, result: 'i32'},
    compress: { parameters: ['f64', 'f64'], nonblocking: true, result: 'void'},
    envWrite: { parameters: ['f64', 'f64'], result: 'i32'},
    setGlobalBuffer: { parameters: ['pointer', 'usize'], result: 'void'},
    setCompressionBuffer: { parameters: ['f64', 'pointer', 'usize', 'u32'], result: 'void'},
    newCompression: { parameters: ['pointer', 'usize', 'u32'], result: 'u64'},
    prefetch: { parameters: ['f64', 'f64'], nonblocking: true, result: 'i32'},
});

let { envOpen, closeEnv, getAddress, freeData, getMaxKeySize, openDbi, getDbi, readerCheck,
    commitEnvTxn, abortEnvTxn, beginTxn, resetTxn, renewTxn, abortTxn, commitTxn, dbiGetByBinary, startWriting, compress, envWrite, openCursor, cursorRenew, cursorClose, cursorIterate, cursorPosition, cursorCurrentValue, setGlobalBuffer: setGlobalBuffer2, setCompressionBuffer, getError, newCompression, prefetch } = lmdbxLib.symbols;
let registry = new FinalizationRegistry<number>(address => {
    // when an object is GC'ed, free it in C.
    freeData(address);
});

class CBridge {
    address: number;
    constructor(address: number) {
        this.address = address || 0;
        if (address) {
            registry.register(this, address);
        }
    }
  /*  static addMethods(...methods: ) {
        for (let method of methods) {
            this.prototype[method] = function() {
                return symbols[method](this.address, ...arguments);
            };
        }
    }*/
}
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const MAX_ERROR = 1000;
function checkError(rc: number): number {
    if (rc && rc < MAX_ERROR) {
        // TODO: Look up error and throw
        lmdbxError(rc);
    }
    return rc;
}
function lmdbxError(rc: number) {
    getError(rc, keyBytes);
    let message = textDecoder.decode(keyBytes.subarray(0, keyBytes.indexOf(0))) || ('Error code: ' + rc);
    throw new Error(message);
}
let keyBytes: Uint8Array;
class Env extends CBridge {
    open(options: any, flags: number, jsFlags: number) {
        let rc = envOpen(flags, jsFlags, toCString(options.path), keyBytes = options.keyBytes, 0,
            options.maxDbs || 12, options.maxReaders || 126, options.mapSize, options.pageSize) as number;
        this.address = checkError(rc);
        registry.register(this, this.address);
        return 0;
    }
    openDbi(flags: number, name: string, keyType: number, compression: Compression) {
        let rc: number = openDbi(this.address, flags, toCString(name), keyType, compression?.address || 0) as number;
        if (rc == -30798) { // MDB_NOTFOUND
            return;
        }
        return new Dbi(checkError(rc),
            getDbi(rc) as number);
    }
    close() {
        closeEnv(this.address);
    }
    getMaxKeySize() {
        return getMaxKeySize(this.address);
    }
    readerCheck() {
        return readerCheck(this.address);
    }
    beginTxn(flags: number) {
        let rc: number = beginTxn(this.address, flags) as number;
        return new Transaction(checkError(rc), flags);
    }
    commitTxn() {
        checkError(commitEnvTxn(this.address) as number);
    }
    abortTxn() {
        abortEnvTxn(this.address);
    }
    startWriting(instructions: number, callback: (value: number) => number) {
        (startWriting(this.address, instructions) as Promise<number>).then(callback);
    }
    compress(compressionPointer: number, callback: (value: void) => void) {
        return (compress(this.address, compressionPointer) as  Promise<void>).then(callback);
    }
    write(instructions: number) {
        return checkError(envWrite(this.address, instructions) as number);
    }
}
//Env.addMethods('startWriting', 'write', 'openDB');
class Dbi extends CBridge {
    dbi: number;
    constructor(address: number, dbi: number) {
        super(address);
        this.dbi = dbi;
    }
    getByBinary(keySize: number): number {
        return dbiGetByBinary(this.address, keySize) as number;
    }
    prefetch(keys: number, callback: () => void): void {
        (prefetch(this.address, keys) as Promise<number>).then(() => callback());
    }
}
class Transaction extends CBridge {
    flags: number;
    constructor(address: number, flags: number) {
        super(address);
        this.flags = flags;
    }
    reset() {
        resetTxn(this.address);
    }
    renew() {
        let rc = renewTxn(this.address) as number;
        if (rc)
            lmdbxError(rc);
    }
    abort() {
        abortTxn(this.address);
    }
    commit() {
        commitTxn(this.address);
    }
}


class Compression extends CBridge {
    constructor(options: { dictionary: Uint8Array, threshold: number }) {
        let dictionary = options.dictionary || new Uint8Array(0);
        super(newCompression(dictionary, dictionary.length, options.threshold || 1000) as number);
    }
    setBuffer(bytes: Uint8Array, dictLength: number) {
        setCompressionBuffer(this.address, bytes, bytes.length, dictLength);
    }
}
class Cursor extends CBridge {
    constructor(dbi: Dbi) {
        super(openCursor(dbi.address) as number);
    }
    renew() {
        cursorRenew(this.address);
    }
    position(flags: number, offset: number, keySize: number, endKeyAddress: number) {
        return cursorPosition(this.address, flags, offset, keySize, endKeyAddress);
    }
    iterate() {
        return cursorIterate(this.address);
    }
    getCurrentValue() {
        return cursorCurrentValue(this.address);
    }
    close() {
        return cursorClose(this.address);
    }
}
function toCString(str: string): Uint8Array {
    return str == null ? new Uint8Array(0) : textEncoder.encode(str + '\x00');
}
function setGlobalBuffer(buffer: Uint8Array) {
    setGlobalBuffer2(buffer, buffer.length);
}

setNativeFunctions({ Env, Compression, Cursor, getAddress, lmdbxError, setGlobalBuffer });
export const { toBufferKey: keyValueToBuffer, compareKeys, compareKeys: compareKey, fromBufferKey: bufferToKeyValue } = orderedBinary;
export { ABORT, asBinary, IF_EXISTS } from './write.js';
export { levelup } from './level.js';
export { open, getLastVersion } from './open.js';

// inlined from https://github.com/denoland/deno_std/blob/main/node/os.ts
function tmpdir(): string | null {
    /* This follows the node js implementation, but has a few
       differences:
       * On windows, if none of the environment variables are defined,
         we return null.
       * On unix we use a plain Deno.env.get, instead of safeGetenv,
         which special cases setuid binaries.
       * Node removes a single trailing / or \, we remove all.
    */
    if (Deno.build.os == 'windows') {
      const temp = Deno.env.get("TEMP") || Deno.env.get("TMP");
      if (temp) {
        return temp.replace(/(?<!:)[/\\]*$/, "");
      }
      const base = Deno.env.get("SYSTEMROOT") || Deno.env.get("WINDIR");
      if (base) {
        return base + "\\temp";
      }
      return null;
    } else { // !isWindows
      const temp = Deno.env.get("TMPDIR") || Deno.env.get("TMP") ||
        Deno.env.get("TEMP") || "/tmp";
      return temp.replace(/(?<!^)\/*$/, "");
    }
}
function exists(path: string): boolean {
    try {
        return Boolean(Deno.statSync(path));
    } catch (error) {
        if (error.name == 'NotFound')
			return false
        throw error
    }
}
