'use strict';
const { target: tv, unwrap } = require('proxy-target/array');
const { create: createGCHook } = require('gc-hook');

const {
  ARRAY,
  OBJECT,
  FUNCTION,
  NUMBER,
  STRING,
  SYMBOL,
  UNDEFINED
} = require('proxy-target/types');

const {
  TypedArray,
  defineProperty,
  deleteProperty,
  getOwnPropertyDescriptor,
  getPrototypeOf,
  isExtensible,
  ownKeys,
  preventExtensions,
  set,
  setPrototypeOf,
  assign,
  create,
  augment,
  asEntry,
  symbol,
  transform
} = require('./utils.js');

const {
  APPLY,
  CONSTRUCT,
  DEFINE_PROPERTY,
  DELETE_PROPERTY,
  GET,
  GET_OWN_PROPERTY_DESCRIPTOR,
  GET_PROTOTYPE_OF,
  HAS,
  IS_EXTENSIBLE,
  OWN_KEYS,
  PREVENT_EXTENSION,
  SET,
  SET_PROTOTYPE_OF,
  DELETE
} = require('./traps.js');

module.exports = (name, patch) => {
  const eventsHandler = patch && new WeakMap;

  // patch once main UI tread
  if (patch) {
    const { addEventListener } = EventTarget.prototype;
    // this should never be on the way as it's extremely light and fast
    // but it's necessary to allow "preventDefault" or other event invokes at distance
    defineProperty(EventTarget.prototype, 'addEventListener', {
      value(type, listener, ...options) {
        if (options.at(0)?.invoke) {
          if (!eventsHandler.has(this))
            eventsHandler.set(this, new Map);
          eventsHandler.get(this).set(type, [].concat(options[0].invoke));
          delete options[0].invoke;
        }
        return addEventListener.call(this, type, listener, ...options);
      }
    });
  }

  const handleEvent = patch && (event => {
    const {currentTarget, target, type} = event;
    for (const method of eventsHandler.get(currentTarget || target)?.get(type) || [])
      event[method]();
  });

  return function (thread, MAIN, THREAD, ...args) {
    let id = 0, $ = this?.transform || transform;
    const ids = new Map;
    const values = new Map;

    const {[THREAD]: __thread__} = thread;

    const global = args.length ? assign(create(globalThis), ...args) : globalThis;

    const result = asEntry((type, value) => {
      if (!ids.has(value)) {
        let sid;
        // a bit apocalyptic scenario but if this main runs forever
        // and the id does a whole int32 roundtrip we might have still
        // some reference dangling around
        while (values.has(sid = id++));
        ids.set(value, sid);
        values.set(sid, type === FUNCTION ? value : $(value));
      }
      return tv(type, ids.get(value));
    });

    const onGarbageCollected = value => {
      __thread__(DELETE, tv(STRING, value));
    };

    const asValue = (type, value) => {
      switch (type) {
        case OBJECT:
          if (value == null) return global;
        case ARRAY:
          if (typeof value === NUMBER) return values.get(value);
          if (!(value instanceof TypedArray)) {
            for (const key in value)
              value[key] = target(value[key]);
          }
          return value;
        case FUNCTION:
          if (typeof value === STRING) {
            const retained = values.get(value)?.deref();
            if (retained) return retained;
            const cb = function (...args) {
              if (patch && args.at(0) instanceof Event) handleEvent(...args);
              return __thread__(
                APPLY,
                tv(FUNCTION, value),
                result(this),
                args.map(result)
              );
            };
            values.set(value, new WeakRef(cb));
            return createGCHook(value, onGarbageCollected, {
              return: cb,
              token: false,
            });
          }
          return values.get(value);
        case SYMBOL:
          return symbol(value);
      }
      return value;
    };

    const target = entry => unwrap(entry, asValue);

    const trapsHandler = {
      [APPLY]: (target, thisArg, args) => result(target.apply(thisArg, args)),
      [CONSTRUCT]: (target, args) => result(new target(...args)),
      [DEFINE_PROPERTY]: (target, name, descriptor) => result(defineProperty(target, name, descriptor)),
      [DELETE_PROPERTY]: (target, name) => result(deleteProperty(target, name)),
      [GET_PROTOTYPE_OF]: target => result(getPrototypeOf(target)),
      [GET]: (target, name) => result(target[name]),
      [GET_OWN_PROPERTY_DESCRIPTOR]: (target, name) => {
        const descriptor = getOwnPropertyDescriptor(target, name);
        return descriptor ? tv(OBJECT, augment(descriptor, result)) : tv(UNDEFINED, descriptor);
      },
      [HAS]: (target, name) => result(name in target),
      [IS_EXTENSIBLE]: target => result(isExtensible(target)),
      [OWN_KEYS]: target => tv(ARRAY, ownKeys(target).map(result)),
      [PREVENT_EXTENSION]: target => result(preventExtensions(target)),
      [SET]: (target, name, value) => result(set(target, name, value)),
      [SET_PROTOTYPE_OF]: (target, proto) => result(setPrototypeOf(target, proto)),
      [DELETE](id) {
        ids.delete(values.get(id));
        values.delete(id);
      }
    };

    thread[MAIN] = (trap, entry, ...args) => {
      switch (trap) {
        case APPLY:
          args[0] = target(args[0]);
          args[1] = args[1].map(target);
          break;
        case CONSTRUCT:
          args[0] = args[0].map(target);
          break;
        case DEFINE_PROPERTY: {
          const [name, descriptor] = args;
          args[0] = target(name);
          const {get, set, value} = descriptor;
          if (get) descriptor.get = target(get);
          if (set) descriptor.set = target(set);
          if (value) descriptor.value = target(value);
          break;
        }
        default:
          args = args.map(target);
          break;
      }
      return trapsHandler[trap](target(entry), ...args);
    };

    return {
      proxy: thread,
      [name.toLowerCase()]: global,
      [`is${name}Proxy`]: () => false
    };
  };
};
