import {
  useEffect,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
} from "react";
import type { Resource, ResourceFiberRoot } from "../core/types";
import {
  commitResourceFiber,
  createResourceFiber,
  renderResourceFiber,
  unmountResourceFiber,
} from "../core/ResourceFiber";
import {
  commitRoot,
  createResourceFiberRoot,
  setRootVersion,
} from "../core/helpers/root";
import { peekResourceFiber } from "../core/helpers/execution-context";
import { isDevelopment } from "../core/helpers/env";
import { resource } from "../core/resource";
import { tapResource } from "../hooks/tap-resource";
import { tapResources } from "../hooks/tap-resources";
import { tapResourceRoot } from "../tapResourceRoot";

const useDevStrictMode = () => {
  if (!isDevelopment) return null;

  const count = useRef(0);
  const isFirstRender = count.current === 0;
  useState(() => count.current++);
  if (count.current !== 2) return null;
  return isFirstRender ? ("child" as const) : ("root" as const);
};

const HostResource = resource(function HostResource<T>(callback: () => T) {
  return callback();
});

const useResourceHost = <T>(callback: () => T): T => {
  const root = useMemo<ResourceFiberRoot>(() => {
    return createResourceFiberRoot((cb) => dispatch(cb));
  }, []);

  const [version, dispatch] = useReducer((v: number, cb: () => boolean) => {
    setRootVersion(root, v);
    return v + (cb() ? 1 : 0);
  }, 0);
  setRootVersion(root, version);

  const devStrictMode = useDevStrictMode();
  const fiber = useMemo(() => {
    return createResourceFiber(
      HostResource as unknown as Resource<T, () => T>,
      root,
      undefined,
      devStrictMode,
    );
  }, [root, devStrictMode]);

  const result = renderResourceFiber(fiber, callback);

  useLayoutEffect(() => {
    return () => unmountResourceFiber(fiber);
  }, [fiber]);

  useLayoutEffect(() => {
    commitRoot(root);
    commitResourceFiber(fiber, result);
  });

  return result.output;
};

const makeHook = <F extends (...args: any[]) => any>(hook: F): F =>
  ((...args: any[]) => {
    if (peekResourceFiber()) return hook(...args);
    return useResourceHost(() => hook(...args));
  }) as F;

export const useResource: typeof tapResource = makeHook(tapResource);

export const useResources: typeof tapResources = makeHook(tapResources);

const tapRootFromRender = <R>(render: () => R) =>
  tapResourceRoot(HostResource(render));

export const useTapRoot: typeof tapRootFromRender = makeHook(tapRootFromRender);

export const useResourceRoot = useTapRoot;

export namespace useTapHost {
  export interface Result<R> {
    value: R;
    effects: () => void;
  }
}

export const useTapHost = <R>(callback: () => R): useTapHost.Result<R> => {
  const root = useMemo<ResourceFiberRoot>(() => {
    return createResourceFiberRoot((cb) => dispatch(cb));
  }, []);

  const [version, dispatch] = useReducer((v: number, cb: () => boolean) => {
    setRootVersion(root, v);
    return v + (cb() ? 1 : 0);
  }, 0);
  setRootVersion(root, version);

  const devStrictMode = useDevStrictMode();
  const fiber = useMemo(() => {
    return createResourceFiber(
      HostResource as unknown as Resource<R, () => R>,
      root,
      undefined,
      devStrictMode,
    );
  }, [root, devStrictMode]);

  const result = renderResourceFiber(fiber, callback);

  useEffect(() => {
    return () => unmountResourceFiber(fiber);
  }, [fiber]);

  let renderCommitted = false;
  const effects = () => {
    if (renderCommitted && fiber.isMounted) return;
    renderCommitted = true;

    commitRoot(root);
    commitResourceFiber(fiber, result);
  };

  useEffect(effects);

  return {
    value: result.output,
    effects,
  };
};
