import { onWillUnmount, useScope } from "@odoo/owl";
import { useEnv, useSubEnv } from "../owl3_compatibility_layer";
import { LocalStoreConstructor, Store, StoreConstructor, StoreParams } from "../types/store_engine";
import { DependencyContainer } from "./dependency_container";

/**
 * This hook should be used at the root of your app to provide the store container.
 */
export function useStoreProvider() {
  const env = useEnv();
  if (env.__spreadsheet_stores__ instanceof DependencyContainer) {
    return env.__spreadsheet_stores__;
  }
  const container = new DependencyContainer();
  useSubEnv({
    __spreadsheet_stores__: container,
    getStore: <T extends StoreConstructor>(Store: T) => {
      const store = container.get(Store);
      return proxifyStoreMutation(store, () => container.trigger("store-updated"));
    },
  });
  onWillUnmount(() => container.dispose());
  return container;
}

/**
 * This create a new store dependency container. The store given in the extended store array will be owned by this
 * new container, the other stores will be owned by the parent container.
 */
export function useChildStoreProvider(extendedStores: StoreConstructor[]) {
  const env = useEnv();
  const parentContainer = env.__spreadsheet_stores__;
  if (!(parentContainer instanceof DependencyContainer)) {
    throw new Error("No parent store provider found.");
  }
  const container = new DependencyContainer(parentContainer, extendedStores);
  useSubEnv({
    __spreadsheet_stores__: container,
    getStore: <T extends StoreConstructor>(Store: T) => {
      const store = container.get(Store);
      return proxifyStoreMutation(store, () => container.trigger("store-updated"));
    },
  });
  onWillUnmount(() => container.dispose());
  return container;
}

type Env = ReturnType<typeof useEnv>;

/**
 * Get the instance of a store.
 */
export function useStore<T extends StoreConstructor>(Store: T): Store<InstanceType<T>> {
  const env: Env = useEnv();
  const container = getDependencyContainer(env);
  const store = container.get(Store);
  return useStoreRenderProxy(container, store);
}

export function useLocalStore<T extends LocalStoreConstructor<any>>(
  Store: T,
  ...args: StoreParams<T> extends never ? [] : StoreParams<T>
): Store<InstanceType<T>> {
  const env = useEnv();
  const container = getDependencyContainer(env);
  const store = container.instantiate(Store, ...args);
  onWillUnmount(() => store.dispose());
  return useStoreRenderProxy(container, store);
}

/**
 * Trigger an event to re-render the app (deep render) when
 * a store is mutated by invoking one of its mutator methods.
 */
function useStoreRenderProxy<S extends { mutators: readonly (keyof S)[] }>(
  container: DependencyContainer,
  store: S
): S {
  const scope = useScope();
  const proxy = proxifyStoreMutation(store, () => {
    //TODO OWL v50 introduces STATUS.MOUNTED with the value 1. This line should be uncommented when upgrading OWL v50 lands in odoo.
    // if (scope.status === STATUS.MOUNTED) {
    if (scope.status === 1) {
      container.trigger("store-updated");
    }
  });
  return proxy as S;
}

/**
 * Creates a proxied version of a store object with mutation tracking.
 * Whenever a mutator method of the store is called, the provided callback function is invoked.
 */
export function proxifyStoreMutation<S extends { mutators: readonly (keyof S)[] }>(
  store: S,
  callback: () => void
): S {
  const proxy = new Proxy(store as object, {
    get(target, property, receiver) {
      const thisStore = target;
      // The third argument is `thisStore` (target) instead of `receiver`.
      // The goal is to always have the same `this` value in getter functions
      // (when `target[property]` is an accessor property).
      // `thisStore` is always the same object reference. `receiver` however is the
      // object on which the property is called, which is the Proxy object which is different for each component.
      const value = Reflect.get(target, property, thisStore);
      if (store.mutators?.includes(property as keyof S)) {
        const functionProxy = new Proxy(value, {
          // trap the function call
          apply(target, thisArg, argArray) {
            const res = Reflect.apply(target, thisStore, argArray);
            if (res === "noStateChange") {
              return;
            }
            callback();
          },
        });
        return functionProxy;
      }
      return value;
    },
  });
  return proxy as S;
}

function getDependencyContainer(env: Env) {
  const container = env.__spreadsheet_stores__;
  if (!(container instanceof DependencyContainer)) {
    throw new Error("No store provider found. Did you forget to call useStoreProvider()?");
  }
  return container;
}
