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

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

const {
  TypedArray,
  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 => {
  let id = 0;
  const ids = new Map;
  const values = new Map;

  const __proxy__ = Symbol();

  return function (main, MAIN, THREAD) {
    const $ = this?.transform || transform;
    const { [MAIN]: __main__ } = main;

    const proxies = new Map;

    const onGarbageCollected = value => {
      proxies.delete(value);
      __main__(DELETE, argument(value));
    };

    const argument = asEntry(
      (type, value) => {
        if (__proxy__ in value)
          return unbound(value[__proxy__]);
        if (type === FUNCTION) {
          value = $(value);
          if (!values.has(value)) {
            let sid;
            // a bit apocalyptic scenario but if this thread runs forever
            // and the id does a whole int32 roundtrip we might have still
            // some reference dangling around
            while (values.has(sid = String(id++)));
            ids.set(value, sid);
            values.set(sid, value);
          }
          return tv(type, ids.get(value));
        }
        if (!(value instanceof TypedArray)) {
          value = $(value);
          for(const key in value)
            value[key] = argument(value[key]);
        }
        return tv(type, value);
      }
    );

    const register = (entry, type, value) => {
      const retained = proxies.get(value)?.deref();
      if (retained) return retained;
      const target = type === FUNCTION ? bound(entry) : entry;
      const proxy = new Proxy(target, proxyHandler);
      proxies.set(value, new WeakRef(proxy));
      return createGCHook(value, onGarbageCollected, {
        return: proxy,
        token: false,
      });
    };

    const fromEntry = entry => unwrap(entry, (type, value) => {
      switch (type) {
        case OBJECT:
          if (value === null) return globalThis;
        case ARRAY:
          return typeof value === NUMBER ? register(entry, type, value) : value;
        case FUNCTION:
          return typeof value === STRING ? values.get(value) : register(entry, type, value);
        case SYMBOL:
          return symbol(value);
      }
      return value;
    });

    const result = (TRAP, target, ...args) => fromEntry(__main__(TRAP, unbound(target), ...args));

    const proxyHandler = {
      [APPLY]: (target, thisArg, args) => result(APPLY, target, argument(thisArg), args.map(argument)),
      [CONSTRUCT]: (target, args) => result(CONSTRUCT, target, args.map(argument)),
      [DEFINE_PROPERTY]: (target, name, descriptor) => {
        const { get, set, value } = descriptor;
        if (typeof get === FUNCTION) descriptor.get = argument(get);
        if (typeof set === FUNCTION) descriptor.set = argument(set);
        if (typeof value === FUNCTION) descriptor.value = argument(value);
        return result(DEFINE_PROPERTY, target, argument(name), descriptor);
      },
      [DELETE_PROPERTY]: (target, name) => result(DELETE_PROPERTY, target, argument(name)),
      [GET_PROTOTYPE_OF]: target => result(GET_PROTOTYPE_OF, target),
      [GET]: (target, name) => name === __proxy__ ? target : result(GET, target, argument(name)),
      [GET_OWN_PROPERTY_DESCRIPTOR]: (target, name) => {
        const descriptor = result(GET_OWN_PROPERTY_DESCRIPTOR, target, argument(name));
        return descriptor && augment(descriptor, fromEntry);
      },
      [HAS]: (target, name) => name === __proxy__ || result(HAS, target, argument(name)),
      [IS_EXTENSIBLE]: target => result(IS_EXTENSIBLE, target),
      [OWN_KEYS]: target => result(OWN_KEYS, target).map(fromEntry),
      [PREVENT_EXTENSION]: target => result(PREVENT_EXTENSION, target),
      [SET]: (target, name, value) => result(SET, target, argument(name), argument(value)),
      [SET_PROTOTYPE_OF]: (target, proto) => result(SET_PROTOTYPE_OF, target, argument(proto)),
    };

    main[THREAD] = (trap, entry, ctx, args) => {
      switch (trap) {
        case APPLY:
          return fromEntry(entry).apply(fromEntry(ctx), args.map(fromEntry));
        case DELETE: {
          const id = fromEntry(entry);
          ids.delete(values.get(id));
          values.delete(id);
        }
      }
    };

    const global = new Proxy(tv(OBJECT, null), proxyHandler);

    return {
      [name.toLowerCase()]: global,
      [`is${name}Proxy`]: value => typeof value === OBJECT && !!value && __proxy__ in value,
      proxy: main
    };
  };
};
