import { AnyFun } from "queenjs/typing";
import { isProxy, toRaw } from "vue";

const stateWatchers = new WeakMap<object, Set<AnyFun>>();

export const createProxy = <T extends object>(
  obj: T,
  paths: PropertyKey[] = [],
  root?: any
): T => {
  if (!(obj instanceof Object) || isProxy(obj)) return obj;
  root = root || obj;
  for (const key in obj) {
    const objItem = obj[key];
    if (objItem instanceof Object) {
      obj[key] = createProxy(objItem, [...paths, key], root);
    }
  }

  return new Proxy(obj, {
    get(target: any, key: PropertyKey) {
      if (key === "__origin__") {
        return obj;
      }
      return Reflect.get(target, key);
    },
    set(target: any, key: PropertyKey, value: any, receiver: any) {
      const oldValue = Reflect.get(target, key, receiver);
      const nextValue = createProxy(value, [...paths, key], root);
      const result = Reflect.set(target, key, nextValue, receiver);
      if (value !== oldValue) {
        emit("set", root, [...paths, key], nextValue, oldValue);
      }
      return result;
    },
    deleteProperty(target: any, key: PropertyKey) {
      const oldValue = Reflect.get(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (result && oldValue !== undefined) {
        emit("delete", root, [...paths, key], undefined, oldValue);
      }
      return result;
    },
  });
};

export function toOriginRaw(obj: any) {
  if (isProxy(obj)) {
    return toRaw(obj)["__origin__"];
  } else {
    return obj["__origin__"] || obj;
  }
}

function emit(
  type: "set" | "delete",
  root: any,
  paths: PropertyKey[],
  value: any,
  oldValue: any
) {
  const watchers = stateWatchers.get(toOriginRaw(root));
  watchers?.forEach((watch) => watch(type, paths, value, oldValue));
}

export function createProxyEffect(
  target: any,
  handler: (type: "set" | "delete", paths: string[], val: any, old: any) => void
) {
  target = toOriginRaw(target);
  const handlers = stateWatchers.get(target);
  if (handlers) {
    handlers.add(handler);
  } else {
    stateWatchers.set(target, new Set([handler]));
  }
  return {
    stop() {
      const handlers = stateWatchers.get(target);
      handlers?.delete(handler);
    },
  };
}
