import { StaticRouter, type RouterProps } from "@solidjs/router";
import {
  Component,
  createContext,
  createEffect,
  createRenderEffect,
  createUniqueId,
  Index,
  onMount,
  Show,
  untrack,
  useContext,
  type Accessor,
} from "solid-js";
import { createStore, unwrap } from "solid-js/store";
import { insert, render } from "solid-js/web";
import "./StackedRouter.css";
import { animateSlideInFromRight } from "./anim";
import { ANIM_CURVE_DECELERATION, ANIM_CURVE_STD } from "../material/theme";

export type StackedRouterProps = Omit<RouterProps, "url">;

export type StackFrame = {
  path: string;
  rootId: string;
  state: unknown;
  beforeShow?: (element: HTMLElement) => void;
};

export type NewFrameOptions<T> = (T extends undefined
  ? {
      state?: T;
    }
  : { state: T }) & {
  replace?: boolean;
};

export type FramePusher<T, K extends keyof T = keyof T> = T[K] extends
  | undefined
  | any
  ? (path: K, state?: Readonly<NewFrameOptions<T[K]>>) => Readonly<StackFrame>
  : (path: K, state: Readonly<NewFrameOptions<T[K]>>) => Readonly<StackFrame>;

export type Navigator<PushGuide = Record<string, any>> = {
  frames: readonly StackFrame[];
  push: FramePusher<PushGuide>;
  pop: (depth?: number) => void;
};

const NavigatorContext = /* @__PURE__ */ createContext<Navigator>();

/**
 * Get the navigator of the {@link StackedRouter}.
 *
 * This function returns a {@link Navigator} without available
 * push guide. Push guide is a record type contains available
 * path and its state. If you need push guide, you may want to
 * define your own function (like `useAppNavigator`) and cast the
 * navigator to the type you need.
 */
export function useNavigator() {
  const navigator = useContext(NavigatorContext);

  if (!navigator) {
    throw new TypeError("not in available scope of StackedRouter");
  }

  return navigator;
}

export type CurrentFrame = {
  index: number;
  frame: Readonly<StackFrame>;
};

const CurrentFrameContext =
  /* @__PURE__ */ createContext<Accessor<Readonly<CurrentFrame>>>();

export function useCurrentFrame() {
  const frame = useContext(CurrentFrameContext);

  if (!frame) {
    throw new TypeError("not in available scope of StackedRouter");
  }

  return frame;
}

function onDialogClick(
  onClose: () => void,
  event: MouseEvent & { currentTarget: HTMLDialogElement },
) {
  if (event.target !== event.currentTarget) return;
  const rect = event.currentTarget.getBoundingClientRect();
  const isNotInDialog =
    event.clientY < rect.top ||
    event.clientY > rect.bottom ||
    event.clientX < rect.left ||
    event.clientX > rect.right;
  if (isNotInDialog) {
    onClose();
  }
}

/**
 * The router that stacks the pages.
 */
const StackedRouter: Component<StackedRouterProps> = (oprops) => {
  const [stack, mutStack] = createStore([] as StackFrame[], { name: "stack" });

  const pushFrame = (path: string, opts?: Readonly<NewFrameOptions<any>>) =>
    untrack(() => {
      const frame = {
        path,
        state: opts?.state,
        rootId: createUniqueId(),
      };
      mutStack(opts?.replace ? stack.length - 1 : stack.length, frame);
      return frame;
    });

  const popFrame = (depth: number = 1) =>
    untrack(() => {
      if (import.meta.env.DEV) {
        if (depth < 0) {
          console.warn("the depth to pop should not < 0, now is", depth);
        }
      }
      if (stack.length > 1) {
        const lastFrame = stack[stack.length - 1];
        const element = document.getElementById(lastFrame.rootId)!;
        requestAnimationFrame(() => {
          const animation = element.animate(
            {
              opacity: [0.5, 0],
            },
            { easing: ANIM_CURVE_STD, duration: 220 },
          );
          animation.addEventListener("finish", () =>
            mutStack((o) => o.toSpliced(o.length - depth, depth)),
          );
        });
      } else {
        mutStack((o) => {
          return o.toSpliced(o.length - depth, depth);
        });
      }
    });

  /* createEffect(() => {
    const length = stack.length;
    console.debug("stack is changed", length, unwrap(stack));
  }); */

  createRenderEffect(() => {
    if (stack.length === 0) {
      pushFrame("/", undefined);
    }
  });

  const onBeforeDialogMount = (element: HTMLDialogElement) => {
    createEffect(() => {
      requestAnimationFrame(() => {
        element.showModal();
        if (window.innerWidth <= 560) {
          animateSlideInFromRight(element, { easing: ANIM_CURVE_DECELERATION });
        } else {
          element.animate(
            {
              opacity: [0.5, 1],
            },
            { easing: ANIM_CURVE_STD, duration: 220 },
          );
        }
      });
    });
  };

  return (
    <NavigatorContext.Provider
      value={{
        push: pushFrame,
        pop: popFrame,
        frames: stack,
      }}
    >
      <Index each={stack}>
        {(frame, index) => {
          const currentFrame = () => {
            return {
              index,
              frame: frame(),
            };
          };

          return (
            <CurrentFrameContext.Provider value={currentFrame}>
              <Show
                when={index !== 0}
                fallback={
                  <div
                    class="StackedPage"
                    id={frame().rootId}
                    role="presentation"
                  >
                    <StaticRouter url={frame().path} {...oprops} />
                  </div>
                }
              >
                <dialog
                  ref={onBeforeDialogMount}
                  class="StackedPage"
                  onCancel={[popFrame, 1]}
                  onClick={[onDialogClick, popFrame]}
                  id={frame().rootId}
                >
                  <StaticRouter url={frame().path} {...oprops} />
                </dialog>
              </Show>
            </CurrentFrameContext.Provider>
          );
        }}
      </Index>
    </NavigatorContext.Provider>
  );
};

export default StackedRouter;
