'use strict';

// @ts-check

/**
 * @param {unknown} value
 */
const getType = (value) => {
  if (value === undefined) return 0
  if (value === null) return 1
  if (isBuffer(value)) return 10
  const t = typeof value;
  if (t === 'boolean') return 2
  if (t === 'number') return 3
  if (t === 'string') return 4
  if (t === 'object') return 6
  if (t === 'bigint') return 9
  return -1
};

/**
 * @param {unknown} value
 * @returns {value is Uint8Array}
 */
const isBuffer = (value) => {
  const BufferCtor = globalThis.Buffer;
  if (typeof BufferCtor?.isBuffer === 'function') {
    return BufferCtor.isBuffer(value)
  }
  return value instanceof Uint8Array && value.constructor?.name === 'Buffer'
};

const RESPONSE_HEADER_SIZE = 16;
const RESPONSE_PAYLOAD_SIZE = 10240;
const RESPONSE_BUFFER_SIZE = RESPONSE_HEADER_SIZE + RESPONSE_PAYLOAD_SIZE;

/**
 * @param {Int32Array} sab
 * @param {Uint8Array} payload
 */
const writeResponsePayload = (sab, payload) => {
  if (payload.length > RESPONSE_PAYLOAD_SIZE) {
    throw new RangeError('payload overflow')
  }
  new Uint8Array(sab.buffer).set(payload, RESPONSE_HEADER_SIZE);
};

/**
 * @param {import('memfs').IFs} memfs
 * @param {any} value
 * @param {ReturnType<typeof getType>} type
 * @returns {Uint8Array}
 */
const encodeValue = (memfs, value, type) => {
  switch (type) {
    case 0:
    case 1:
      return new Uint8Array(0)
    case 2: {
      const view = new Int32Array(1);
      view[0] = value ? 1 : 0;
      return new Uint8Array(view.buffer)
    }
    case 3: {
      const view = new Float64Array(1);
      view[0] = value;
      return new Uint8Array(view.buffer)
    }
    case 4: {
      const view = new TextEncoder().encode(value);
      return view
    }
    case 10:
      return new Uint8Array(value)
    case 6: {
      function storeConstructor(obj, memfs, processed = new WeakSet()) {
        if (!obj || typeof obj !== 'object') {
          return
        }

        if (processed.has(obj)) {
          return
        }

        processed.add(obj);

        const [entry] =
          Object.entries(memfs).filter(([_, v]) => v === obj.constructor)[0] ??
          [];
        if (entry) {
          Object.defineProperty(obj, '__constructor__', {
            configurable: true,
            writable: true,
            enumerable: true,
            value: entry,
          });
        }

        for (const value of Object.values(obj)) {
          storeConstructor(value, memfs, processed);
        }
      }

      storeConstructor(value, memfs);

      const json = JSON.stringify(value, (_, value) => {
        if (typeof value === 'bigint') {
          return `BigInt(${String(value)})`
        }
        if (value instanceof Error) {
          return {
            ...value,
            message: value.message,
            stack: value.stack,
            __error__: value.constructor.name,
          }
        }
        return value
      });
      const view = new TextEncoder().encode(json);
      return view
    }
    case 9: {
      return new TextEncoder().encode(String(value))
    }
    case -1:
    default:
      throw new Error('unsupported data')
  }
};

/**
 * @param {typeof import('memfs')} memfs
 * @param {Uint8Array} payload
 * @param {number} type
 * @returns {any}
 */
const decodeValue = (memfs, payload, type) => {
  if (type === 0) return undefined
  if (type === 1) return null
  if (type === 2)
    return Boolean(new Int32Array(payload.buffer, payload.byteOffset, 1)[0])
  if (type === 3)
    return new Float64Array(payload.buffer, payload.byteOffset, 1)[0]
  if (type === 4) return new TextDecoder().decode(payload.slice())
  if (type === 10) {
    const BufferCtor = globalThis.Buffer;
    if (typeof BufferCtor?.from === 'function') {
      return BufferCtor.from(payload)
    }
    return payload.slice()
  }
  if (type === 6) {
    const obj = JSON.parse(
      new TextDecoder().decode(payload.slice()),
      (_key, value) => {
        if (typeof value === 'string') {
          const matched = value.match(/^BigInt\((-?\d+)\)$/);
          if (matched && matched[1]) {
            return BigInt(matched[1])
          }
        }
        return value
      },
    );

    function loadConstructor(obj, memfs, processed = new WeakSet()) {
      if (!obj || typeof obj !== 'object') {
        return
      }

      if (processed.has(obj)) {
        return
      }

      processed.add(obj);

      if (obj.__constructor__) {
        const ctor = obj.__constructor__;
        delete obj.__constructor__;
        Object.setPrototypeOf(obj, memfs[ctor].prototype);
      }

      for (const value of Object.values(obj)) {
        loadConstructor(value, memfs, processed);
      }
    }

    loadConstructor(obj, memfs);

    if (obj.__error__) {
      const name = obj.__error__;
      const ErrorConstructor = globalThis[name] || Error;
      delete obj.__error__;
      const err = new ErrorConstructor(obj.message);
      Object.defineProperty(err, 'stack', {
        configurable: true,
        enumerable: false,
        writable: true,
        value: err.stack,
      });
      Object.defineProperty(err, Symbol.toStringTag, {
        configurable: true,
        enumerable: false,
        writable: true,
        value: name,
      });
      for (const [k, v] of Object.entries(obj)) {
        if (k === 'message' || k === 'stack') continue
        err[k] = v;
      }
      return err
    }
    return obj
  }
  if (type === 9) {
    return BigInt(new TextDecoder().decode(payload.slice()))
  }
  throw new Error('unsupported data')
};

/**
 * @param {import('memfs').IFs} fs
 * @returns {(e: { data: { __fs__: { sab: Int32Array, type: keyof import('memfs').IFs, payload: any[] } } }) => void}
 */
// oxlint-disable-next-line no-unused-vars -- fixed in an upcoming release
const createOnMessage = (fs) =>
  function onMessage(e) {
    if (e.data.__fs__) {
      /**
       * 0..4                    status(int32_t):        21(waiting) 0(success) 1(error)
       * 5..8                    type(napi_valuetype):   0(undefined) 1(null) 2(boolean) 3(number) 4(string) 6(jsonstring) 9(bigint) 10(buffer) -1(unsupported)
       * 9..16                   payload_size(uint32_t)  <= 10240
       * 16..16 + payload_size   payload_content
       */
      const { sab, type, payload } = e.data.__fs__;
      const fn = fs[type];
      try {
        const ret = fn.apply(fs, payload);
        const t = getType(ret);
        Atomics.store(sab, 1, t);
        const v = encodeValue(fs, ret, t);
        Atomics.store(sab, 2, v.length);
        writeResponsePayload(sab, v);
        Atomics.store(sab, 0, 0); // success
      } catch (/** @type {any} */ err) {
        let t = getType(err);
        let v;
        try {
          if (t === -1) {
            throw new Error('unsupported data')
          }
          v = encodeValue(fs, err, t);
        } catch {
          const fallback = (() => {
            try {
              return String(err)
            } catch {
              return 'Unserializable thrown value'
            }
          })();
          t = getType(fallback);
          v = encodeValue(fs, fallback, t);
        }
        if (v.length > RESPONSE_PAYLOAD_SIZE) {
          const overflowErr = new RangeError('payload overflow');
          t = getType(overflowErr);
          v = encodeValue(fs, overflowErr, t);
        }
        Atomics.store(sab, 1, t);
        Atomics.store(sab, 2, v.length);
        writeResponsePayload(sab, v);
        Atomics.store(sab, 0, 1); // error
      } finally {
        Atomics.notify(sab, 0);
      }
    }
  };

/**
 * @param {typeof import('memfs')} memfs
 */
const createFsProxy = (memfs) =>
  new Proxy(
    {},
    {
      get(_target, p, _receiver) {
        /**
         * @param {any[]} args
         */
        return function (...args) {
          const sab = new SharedArrayBuffer(RESPONSE_BUFFER_SIZE);
          const i32arr = new Int32Array(sab);
          Atomics.store(i32arr, 0, 21);

          postMessage({
            __fs__: {
              sab: i32arr,
              type: p,
              payload: args,
            },
          });

          Atomics.wait(i32arr, 0, 21);

          const status = Atomics.load(i32arr, 0);
          const type = Atomics.load(i32arr, 1);
          const size = Atomics.load(i32arr, 2);
          const content = new Uint8Array(sab, RESPONSE_HEADER_SIZE, size);
          const value = decodeValue(memfs, content, type);
          if (status === 1) {
            throw value
          }
          return value
        }
      },
    },
  );

exports.createFsProxy = createFsProxy;
exports.createOnMessage = createOnMessage;
