import { ByteBuffer } from "peechy/bb";
import {
  Loader as BunLoader,
  TestKind,
  decodeGetTestsResponse,
  decodeScanResult,
  decodeTransformResponse,
  encodeGetTestsRequest,
  encodeScan,
  encodeTransform,
  type ScanResult,
  type TransformResponse,
} from "./schema.js";

export enum Loader {
  jsx = BunLoader.jsx,
  js = BunLoader.js,
  tsx = BunLoader.tsx,
  ts = BunLoader.ts,
}
export interface TestReference {
  name: string;
  byteOffset: number;
  kind: "test" | "describe";
}
export type { ScanResult, TransformResponse };

const testKindMap = {
  [TestKind.describe_fn]: "describe",
  [TestKind.test_fn]: "test",
};
const capturedErrors: string[] = [];
let captureErrors = false;

function normalizeLoader(file_name: string, loader?: keyof typeof Loader): BunLoader {
  return (
    (loader
      ? {
          jsx: BunLoader.jsx,
          tsx: BunLoader.tsx,
          ts: BunLoader.ts,
          js: BunLoader.js,
        }[loader]
      : null) ||
    {
      ".jsx": BunLoader.jsx,
      ".tsx": BunLoader.tsx,
      ".ts": BunLoader.ts,
      ".js": BunLoader.js,
    }[file_name.substring(file_name.lastIndexOf("."))] ||
    BunLoader.js
  );
}

interface WebAssemblyModule {
  init(heapSize: number): number;
  transform(a: bigint): bigint;
  bun_malloc(a: number | bigint): bigint;
  bun_free(a: bigint): void;
  scan(a: bigint): bigint;
  getTests(a: bigint): bigint;
}

const Wasi = {
  clock_time_get(clk_id: unknown, tp: unknown) {
    return Date.now();
  },
  environ_sizes_get() {
    return 0;
  },
  environ_get(__environ: unknown, environ_buf: unknown) {
    return 0;
  },

  fd_close(fd: number) {
    return 0;
  },
  proc_exit() {},

  fd_seek(fd: number, offset_bigint: bigint, whence: unknown, newOffset: unknown) {},
  fd_write(fd: unknown, iov: unknown, iovcnt: unknown, pnum: unknown) {},
};

const env = {
  console_log(slice: bigint) {
    // @ts-expect-error
    const text = Bun._wasmPtrLenToString(slice);
    if (captureErrors) {
      capturedErrors.push(text);
      return;
    }
    console.log(text);
  },
  console_error(slice: bigint) {
    // @ts-expect-error
    const text = Bun._wasmPtrLenToString(slice);
    if (captureErrors) {
      capturedErrors.push(text);
      return;
    }
    console.error(text);
  },
  console_warn(slice: bigint) {
    // @ts-expect-error
    console.warn(Bun._wasmPtrLenToString(slice));
  },
  console_info(slice: bigint) {
    // @ts-expect-error
    console.info(Bun._wasmPtrLenToString(slice));
  },
  __indirect_function_table: new WebAssembly.Table({
    initial: 0,
    element: "anyfunc",
  }),
  __stack_pointer: new WebAssembly.Global({
    mutable: true,
    value: "i32",
  }),
  __multi3(one: number, two: number) {
    return Math.imul(one | 0, two | 0);
  },
  fmod(one: number, two: number) {
    return one % two;
  },
  memset(ptr: number, value: number, len: number) {
    // @ts-expect-error
    Bun.memory_array.fill(value, ptr, ptr + len);
  },
  memcpy(ptr: number, value: number, len: number) {
    // @ts-expect-error
    Bun.memory_array.copyWithin(ptr, value, value + len);
  },
  // These functions convert a to an unsigned long long, rounding toward zero. Negative values all become zero.
  __fixunsdfti(a: number) {
    return Math.floor(a);
  },
  // These functions return the remainder of the unsigned division of a and b.
  __umodti3(a: number, b: number) {
    return (a | 0) % (b | 0);
  },
  // These functions return the quotient of the unsigned division of a and b.
  __udivti3(a: number, b: number) {
    return (a | 0) / (b | 0);
  },
  // These functions return the result of shifting a left by b bits.
  __ashlti3(a: number, b: number) {
    return (a | 0) >> (b | 0);
  },
  /* Returns: convert a to a double, rounding toward even. */
  __floatuntidf(a: number) {
    const mod = a % 2;
    if (mod === 0) {
      return Math.ceil(a);
    } else if (mod === 1) {
      return Math.floor(a);
    }
  },
  emscripten_notify_memory_growth() {},
};

export class Bun {
  private static has_initialized = false;
  private static wasm_source: WebAssembly.WebAssemblyInstantiatedSource;
  private static get wasm_exports(): WebAssemblyModule {
    return Bun.wasm_source.instance.exports as unknown as WebAssemblyModule;
  }

  private static get memory(): WebAssembly.Memory {
    return Bun.wasm_source.instance.exports.memory as WebAssembly.Memory;
  }

  private static scratch: Uint8Array = new Uint8Array(8192);
  private static memory_array: Uint8Array;

  private static _decoder: TextDecoder;
  private static _encoder: TextEncoder = new TextEncoder();

  private static ptr_converter = new ArrayBuffer(16);
  private static ptr_float = new BigUint64Array(Bun.ptr_converter);
  private static ptr_slice = new Uint32Array(Bun.ptr_converter);

  private static _wasmPtrToSlice(offset: bigint) {
    Bun.ptr_float[0] = typeof offset === "number" ? BigInt(offset) : offset;
    return new Uint8Array(Bun.memory.buffer, Bun.ptr_slice[0], Bun.ptr_slice[1]);
  }

  private static _wasmPtrLenToString(slice: bigint) {
    if (!Bun._decoder) {
      Bun._decoder = new TextDecoder("utf8");
    }

    const region = Bun._wasmPtrToSlice(slice);
    return Bun._decoder.decode(region);
  }

  static async init(url?: URL | string | null, heapSize = 64_000_000, fetch = globalThis.fetch) {
    if (Bun.has_initialized) return;
    url ??= new URL("./bun.wasm", import.meta.url);

    if (typeof process === "undefined") {
      if (globalThis.WebAssembly.instantiateStreaming) {
        Bun.wasm_source = await globalThis.WebAssembly.instantiateStreaming(fetch(url), {
          env: env,
          wasi_snapshot_preview1: Wasi,
        });
      } else if (typeof window !== "undefined") {
        const resp = await fetch(url);
        Bun.wasm_source = await globalThis.WebAssembly.instantiate(await resp.arrayBuffer(), {
          env: env,
          wasi_snapshot_preview1: Wasi,
        });
        // is it node?
      }
    } else {
      const fs = await import("fs");

      if (typeof url === "string" && url.startsWith("file://")) {
        url = new URL(url); // fs.readFileSync cannot consume URL strings, only URL objects
      }

      Bun.wasm_source = await globalThis.WebAssembly.instantiate(fs.readFileSync(url), {
        env: env,
        wasi_snapshot_preview1: Wasi,
      });
    }

    const res = Bun.wasm_exports.init(heapSize);

    if (res < 0) {
      throw new Error(`[Bun] Failed to initialize WASM module: code ${res}`);
    }

    Bun.has_initialized = true;
  }

  static getTests(content: Uint8Array, filename = "my.test.tsx") {
    const bb = new ByteBuffer(Bun.scratch);
    bb.length = 0;
    bb.index = 0;
    const contents_buffer = content;

    encodeGetTestsRequest(
      {
        contents: contents_buffer,
        path: filename,
      },
      bb,
    );

    const data = bb.toUint8Array();

    const input_ptr = Bun.wasm_exports.bun_malloc(data.length);
    var buffer = Bun._wasmPtrToSlice(input_ptr);
    buffer.set(data);
    captureErrors = true;
    try {
      var resp_ptr = Bun.wasm_exports.getTests(input_ptr);
    } catch (e) {
      throw e;
    } finally {
      captureErrors = false;
      Bun.wasm_exports.bun_free(input_ptr);
    }

    if (Number(resp_ptr) === 0) {
      if (capturedErrors.length) {
        const err = capturedErrors.slice();
        capturedErrors.length = 0;
        throw new Error(err.join("\n").trim());
      }

      throw new Error("Failed to parse");
    }

    if (capturedErrors.length) {
      Bun.wasm_exports.bun_free(resp_ptr);
      const err = capturedErrors.slice();
      capturedErrors.length = 0;
      throw new Error(err.join("\n").trim());
    }

    var _bb = new ByteBuffer(Bun._wasmPtrToSlice(resp_ptr));

    const response = decodeGetTestsResponse(_bb);
    var tests: TestReference[] = new Array(response.tests.length);

    for (var i = 0; i < response.tests.length; i++) {
      tests[i] = {
        name: new TextDecoder().decode(
          response.contents.subarray(
            response.tests[i].label.offset,
            response.tests[i].label.offset + response.tests[i].label.length,
          ),
        ),
        byteOffset: response.tests[i].byteOffset,
        kind: testKindMap[response.tests[i].kind] as "test" | "describe",
      };
    }

    Bun.wasm_exports.bun_free(resp_ptr);

    return tests;
  }

  static transformSync(
    content: Uint8Array | string,
    file_name: string,
    loader?: keyof typeof Loader,
  ): TransformResponse {
    const bb = new ByteBuffer(Bun.scratch);
    bb.length = 0;
    bb.index = 0;
    var contents_buffer;
    if (typeof content === "string") {
      contents_buffer = Bun._encoder.encode(content);
    } else {
      contents_buffer = content;
    }
    encodeTransform(
      {
        contents: contents_buffer,
        path: file_name,
        loader: normalizeLoader(file_name, loader),
      },
      bb,
    );
    const data = bb.toUint8Array();

    const input_ptr = Bun.wasm_exports.bun_malloc(data.length);
    var buffer = Bun._wasmPtrToSlice(input_ptr);
    buffer.set(data);

    const resp_ptr = Bun.wasm_exports.transform(input_ptr);
    var _bb = new ByteBuffer(Bun._wasmPtrToSlice(resp_ptr));
    const response = decodeTransformResponse(_bb);
    Bun.wasm_exports.bun_free(input_ptr);
    Bun.scratch = bb.data;
    return response;
  }

  static scan(content: Uint8Array | string, file_name: string, loader?: keyof typeof Loader): ScanResult {
    const bb = new ByteBuffer(Bun.scratch);
    bb.length = 0;
    bb.index = 0;
    var contents_buffer;
    if (typeof content === "string") {
      contents_buffer = Bun._encoder.encode(content);
    } else {
      contents_buffer = content;
    }

    encodeScan(
      {
        contents: contents_buffer,
        path: file_name,
        loader: normalizeLoader(file_name, loader),
      },
      bb,
    );
    const data = bb.toUint8Array();

    const input_ptr = Bun.wasm_exports.bun_malloc(data.length);
    var buffer = Bun._wasmPtrToSlice(input_ptr);
    buffer.set(data);

    const resp_ptr = Bun.wasm_exports.scan(input_ptr);
    var _bb = new ByteBuffer(Bun._wasmPtrToSlice(resp_ptr));
    //console.log(resp_ptr, Bun.ptr_slice[0], Bun.ptr_slice[1], new Uint8Array(Bun.memory.buffer, Bun.ptr_slice[0], Bun.ptr_slice[1] + 82));
    //console.log(_bb);
    const response = decodeScanResult(_bb);
    Bun.wasm_exports.bun_free(input_ptr);
    Bun.scratch = bb.data;
    return response;
  }
}

export const transformSync = Bun.transformSync;
export const scan = Bun.scan;
export const init = Bun.init;
export const getTests = Bun.getTests;
export default Bun;
