import type { mastodon } from "masto";
import {
  For,
  type Component,
  type ParentComponent,
  Switch,
  Match,
  createEffect,
  createSignal,
  type JSX,
  onMount,
  Index,
  mergeProps,
  requestCallback,
  untrack,
} from "solid-js";
import { css } from "solid-styled";
import { createStore } from "solid-js/store";
import { IconButton, Toolbar } from "@suid/material";
import { ArrowLeft, ArrowRight, Close } from "@suid/icons-material";

type MediaViewerProps = {
  show: boolean;
  index: number;
  media: mastodon.v1.MediaAttachment[];
  onIndexUpdated?: (newIndex: number) => void;
  onClose?: () => void;
};

export const MEDIA_VIEWER_HEROSRC = Symbol("mediaViewerHeroSrc");

function within(n: number, target: number, range: number) {
  return n >= target - range || n <= target + range;
}

function clamp(input: number, min: number, max: number) {
  return Math.min(Math.max(input, min), max);
}

const MediaViewer: ParentComponent<MediaViewerProps> = (props) => {
  let rootRef: HTMLDialogElement;

  type State = {
    ref?: HTMLElement;
    media: mastodon.v1.MediaAttachment;
    top: number;
    left: number;
    scale: number;
    osize: [number, number]; // width, height
  };
  const [state, setState] = createStore<State[]>(
    props.media.map(
      (media) =>
        ({
          top: 0,
          left: 0,
          ref: undefined,
          media,
          scale: 1,
          osize: [0, 9],
        }) as State,
    ),
  );
  const [showControls, setShowControls] = createSignal(true);
  const [dragging, setDragging] = createSignal(false);

  const hasPrev = () => state.length > 1 && props.index !== 0;
  const hasNext = () => state.length > 1 && props.index < state.length - 1;

  css`
    .media-viewer--root {
      background: none;
      border: none;
      overflow: hidden;
      margin: 0;
      padding: 0;
      outline: none;
      max-width: 100%;
      max-height: 100%;
      height: 100%;
      width: 100%;

      &[open] {
        display: block;
      }
    }

    .media-viewer {
      display: grid;
      grid-auto-flow: column;
      grid-auto-columns: 100%;
      width: 100%;
      height: 100%;
      overflow: auto;
      background-color: ${showControls()
        ? "var(--tutu-color-surface)"
        : "var(--tutu-color-on-surface)"};
      transition: background-color 0.2s var(--tutu-anim-curve-std);
      scroll-behavior: smooth;

      > .media {
        height: 100%;
      }
    }

    .media {
      overflow: hidden;
      position: relative;

      > img {
        position: absolute;
        object-fit: contain;
        top: 0;
        left: 0;
        transform-origin: center;
      }
    }

    .media-ctrls {
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 1;
      cursor: ${dragging() ? "grabbing" : "grab"};
      padding-left: var(--safe-area-inset-left, 0);
      padding-right: var(--safe-area-inset-right, 0);
      padding-bottom: var(--safe-area-inset-bottom, 0);

      :global(> .MuiToolbar-root) {
        padding-top: var(--safe-area-inset-top, 0);
      }
    }

    .left-dock {
      position: absolute;
      left: 24px;
      top: 50%;
      transform: translateY(-50%)
        ${showControls() && hasPrev()
          ? ""
          : "translateX(-100%) translateX(-24px)"};
      display: inline-block;
      transition: transform 0.2s var(--tutu-anim-curve-std);
    }

    .right-dock {
      position: absolute;
      right: 24px;
      top: 50%;
      transform: translateY(-50%)
        ${showControls() && hasNext()
          ? ""
          : "translateX(100%) translateX(24px)"};
      display: inline-block;
      transition: transform 0.2s var(--tutu-anim-curve-std);
    }
  `;

  createEffect(() => {
    if (props.show) {
      rootRef.showModal();
      untrack(() => {
        for (let i = 0; i < state.length; i++) {
          centre(state[i], i);
        }
      });
    } else {
      rootRef.close();
    }
  });

  createEffect(() => {
    const viewer = rootRef.children.item(0)!;
    const targetPageE = viewer.children.item(props.index + 1);
    if (!targetPageE) return;
    targetPageE.scrollIntoView();
  });

  const minScaleOf = (state: State) => {
    const {
      ref,
      osize: [width, height],
    } = state;
    const { width: parentWidth, height: parentHeight } =
      ref!.parentElement!.getBoundingClientRect();
    if (height <= parentHeight && width <= parentWidth) {
      return 1;
    }
    return Math.min(parentHeight / height, parentWidth / width);
  };

  // Position medias to the centre.
  // This function is only available when the elements are layout.
  const centre = ({ ref, osize: [width, height] }: State, idx: number) => {
    const { width: parentWidth, height: parentHeight } =
      ref!.parentElement!.getBoundingClientRect();
    const scale =
      height <= parentHeight && width <= parentWidth
        ? 1
        : Math.min(parentHeight / height, parentWidth / width);
    const top = parentHeight / 2 - height / 2;
    const left = parentWidth / 2 - width / 2;
    setState(idx, { top, left, scale });
  };

  const scale = (
    center: readonly [number, number], // left, top
    move: number,
    idx: number,
  ) => {
    const {
      ref,
      top: otop,
      left: oleft,
      scale: oscale,
      osize: [owidth, oheight],
    } = state[idx];
    const [cx, cy] = center;
    const iy = clamp(cy - otop, 0, oheight),
      ix = clamp(cx - oleft, 0, owidth); // in image coordinate system
    const scale = move + oscale;
    const oix = ix / oscale,
      oiy = iy / oscale;
    const nix = oix * scale,
      niy = oiy * scale;
    // Now we can calculate the center's move

    const { width: vw, height: vh } =
      ref!.parentElement!.getBoundingClientRect();
    const top = vh / 2 - niy;
    const left = vw / 2 - nix;
    setState(idx, { top, left, scale });
  };

  const movePrev = () => {
    props.onIndexUpdated?.(Math.max(props.index - 1, 0));
  };

  const moveNext = () => {
    props.onIndexUpdated?.(Math.min(props.index + 1, state.length - 1));
  };

  const ctrlWheel = (event: WheelEvent) => {
    if (event.ctrlKey && event.deltaY !== 0) {
      event.preventDefault();
      const center = [event.clientX, event.clientY] as const;
      scale(center, -event.deltaY / event.clientY, props.index);
    } else {
      if (event.deltaX !== 0) {
        event.preventDefault();
        if (event.deltaX > 0) {
          moveNext();
        } else {
          movePrev();
        }
      }
    }
  };

  let lastMousedown: [number, number, number] | null = null; // time, left, top

  const ctrlMouseDown = (event: MouseEvent) => {
    if (event.buttons !== 1) return;
    event.preventDefault();
    lastMousedown = [Date.now(), event.clientX, event.clientY];
    setDragging(true);
  };

  const ctrlMouseMove = (event: MouseEvent) => {
    if (!lastMousedown) return;
    event.preventDefault();
    const { movementX: mleft, movementY: mtop } = event;
    setState(props.index, (o) => ({ left: o.left + mleft, top: o.top + mtop }));
  };

  const ctrlMouseUp = (event: MouseEvent) => {
    if (lastMousedown !== null) {
      event.preventDefault();
      const [time, left, top] = lastMousedown;
      const { clientX: nleft, clientY: ntop } = event;
      const now = Date.now();
      const target = event.target;
      checkControls: {
        if (
          target instanceof Element &&
          !target.classList.contains("media-ctrls")
        ) {
          // It's dispatched from sub controls, exits
          break checkControls;
        }
        if (
          now - time < 250 &&
          within(left, nleft, 4) &&
          within(top, ntop, 4)
        ) {
          setShowControls((x) => !x);
        }
      }
      lastMousedown = null;
      setDragging(false);
    }
  };

  return (
    <dialog ref={rootRef!} class="media-viewer--root">
      <div class="media-viewer">
        <div
          class="media-ctrls"
          onWheel={ctrlWheel}
          onMouseDown={ctrlMouseDown}
          onMouseUp={ctrlMouseUp}
          onMouseMove={ctrlMouseMove}
        >
          <Toolbar
            variant="dense"
            sx={{
              backgroundColor: "var(--tutu-color-surface)",
              transform: !showControls() ? "translateY(-100%)" : undefined,
              transition:
                "transform 0.2s var(--tutu-anim-curve-std), box-shadow 0.2s var(--tutu-anim-curve-std)",
              boxShadow: showControls() ? "var(--tutu-shadow-e6)" : undefined,
            }}
          >
            <IconButton onClick={props.onClose} disableRipple>
              <Close />
            </IconButton>
          </Toolbar>
          <div class="left-dock">
            <IconButton
              size="large"
              onClick={(e) => (movePrev(), e.stopPropagation())}
            >
              <ArrowLeft />
            </IconButton>
          </div>
          <div class="right-dock">
            <IconButton
              size="large"
              onClick={(e) => (moveNext(), e.stopPropagation())}
            >
              <ArrowRight />
            </IconButton>
          </div>
        </div>
        <Index each={state}>
          {(item, index) => {
            return (
              <div class="media" data-index={index}>
                <Switch
                  fallback={
                    <pre>{JSON.stringify(item().media, undefined, 2)}</pre>
                  }
                >
                  <Match when={item().media.type === "image"}>
                    <img
                      ref={(r) => {
                        setState(index, { ref: r });
                      }}
                      onLoad={(e) => {
                        const { naturalWidth: width, naturalHeight: height } =
                          e.currentTarget;
                        setState(index, {
                          osize: [width, height],
                        });
                      }}
                      src={item().media.url || undefined}
                      style={{
                        left: `${item().left}px`,
                        top: `${item().top}px`,
                        transform: `scale(${item().scale})`,
                      }}
                      alt={item().media.description || undefined}
                    ></img>
                  </Match>
                </Switch>
              </div>
            );
          }}
        </Index>
      </div>
    </dialog>
  );
};

export default MediaViewer;
