import { createElement, Ref, VNode } from "preact";
import { MutableRef, useEffect, useRef } from "preact/hooks";

export type StateFunc<S> = (p: S) => VNode;

export type StateViewMap<StateType extends { status: string }> = {
  [S in StateType as S["status"]]: StateFunc<S>;
};

export type RecursiveState<S extends object> = S | (() => RecursiveState<S>);

export function compose<SType extends { status: string }, PType>(
  hook: (p: PType) => RecursiveState<SType>,
  viewMap: StateViewMap<SType>,
): (p: PType) => VNode {
  function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
    function ComposedComponent(): VNode {
      const state = stateHook();

      if (typeof state === "function") {
        const subComponent = withHook(state);
        return createElement(subComponent, {});
      }

      const statusName = state.status as unknown as SType["status"];
      const viewComponent = viewMap[statusName] as unknown as StateFunc<SType>;
      return createElement(viewComponent, state);
    }

    return ComposedComponent;
  }

  return (p: PType) => {
    const h = withHook(() => hook(p));
    return h();
  };
}

export function recursive<PType>(
  hook: (p: PType) => RecursiveState<VNode>,
): (p: PType) => VNode {
  function withHook(stateHook: () => RecursiveState<VNode>): () => VNode {
    function ComposedComponent(): VNode {
      const state = stateHook();

      if (typeof state === "function") {
        const subComponent = withHook(state);
        return createElement(subComponent, {});
      }

      return state;
    }

    return ComposedComponent;
  }

  return (p: PType) => {
    const h = withHook(() => hook(p));
    return h();
  };
}

/**
 * Call `callback` only once.
 *
 * Callback can be a closure with binding to the current caller context. This helper
 * will always take the latest `callback`
 *
 * @param callback
 */
export function onComponentUnload(callback: () => void) {
  /**
   * we use a ref to avoid evaluating the effect function
   * on every render and so the unload is called only once
   */
  const ref = useRef<typeof callback>();
  ref.current = callback;

  useEffect(() => {
    return () => {
      ref.current!();
    };
  }, []);
}

const ownerDocument = typeof document === "undefined" ? null : document;
const preconnectsSet: Set<string> = new Set();

export type Preconnect = {
  rel: "preconnect" | "dns-prefetch";
  href: string;
  crossOrigin: string;
};

export function preconnectAs(pre: Preconnect[]) {
  if (ownerDocument) {
    pre.forEach(({ rel, href, crossOrigin }) => {
      const key = `${rel}${href}${crossOrigin}`;
      if (preconnectsSet.has(key)) return;
      preconnectsSet.add(key);
      const instance = ownerDocument.createElement("link");
      instance.setAttribute("rel", rel);
      instance.setAttribute("crossOrigin", crossOrigin);
      instance.setAttribute("href", href);
      ownerDocument.head.appendChild(instance);
    });
  }
}

export function composeRef<T>(...fn: ((e: T | null) => void)[]) {
  return (element: T | null) => {
    fn.forEach((handler) => {
      handler(element);
    });
  };
}

export function saveRef<T>(ref: MutableRef<T>) {
  return (element: T | null) => {
    if (element) {
      ref.current = element;
    }
  };
}
/**
 * Show the element when the load ended
 * @param element
 */
export function doAutoFocus<T extends HTMLElement>(
  element: T | null | undefined,
) {
  if (element) {
    setTimeout(() => {
      element.focus({ preventScroll: true });
    }, 100);
  }
}

/**
 * Show the element when the load ended
 * @param element
 */
export function doAutoFocusWithScroll(element: HTMLElement | null) {
  if (element) {
    setTimeout(() => {
      element.focus({ preventScroll: true });
      element.scrollIntoView({
        behavior: "smooth",
        block: "center",
        inline: "center",
      });
    }, 100);
  }
}

/**
 *
 * @param obj VNode
 * @returns
 */
export function saveVNodeForInspection<T>(obj: T): T {
  // @ts-ignore
  window["showVNodeInfo"] = function showVNodeInfo() {
    inspect(obj);
  };
  return obj;
}
function inspect(obj: any) {
  if (!obj) return;
  if (obj.__c && obj.__c.__H) {
    const componentName = obj.__c.constructor.name;
    const hookState = obj.__c.__H;
    const stateList = hookState.__ as Array<any>;
    console.log("==============", componentName);
    stateList.forEach((hook) => {
      const { __: value, c: context, __h: factory, __H: args } = hook;
      if (typeof context !== "undefined") {
        const { __c: contextId } = context;
        console.log("context:", contextId, hook);
      } else if (typeof factory === "function") {
        console.log("memo:", value, "deps:", args);
      } else if (typeof value === "function") {
        const effectName = value.name;
        console.log("effect:", effectName, "deps:", args);
      } else if (typeof value.current !== "undefined") {
        const ref = value.current;
        console.log("ref:", ref instanceof Element ? ref.outerHTML : ref);
      } else if (value instanceof Array) {
        console.log("state:", value[0]);
      } else {
        console.log(hook);
      }
    });
  }
  const children = obj.__k;
  if (children instanceof Array) {
    children.forEach((e) => inspect(e));
  } else {
    inspect(children);
  }
}
