import React, {
  CSSProperties,
  Component,
  ImgHTMLAttributes,
  KeyboardEvent,
  MouseEvent,
  ReactElement,
  ReactNode,
  SyntheticEvent,
  createRef,
} from "react";
import { createPortal } from "react-dom";
import { VisuallyHidden } from "reakit";
import type { SupportedImage } from "./types";
import {
  getImgAlt,
  getImgSrc,
  getStyleModalImg,
  testDiv,
  testImg,
  testSvg,
} from "./utils";

let elDialogContainer: HTMLDivElement;

if (typeof document !== "undefined") {
  elDialogContainer = document.createElement("div");
  elDialogContainer.setAttribute("data-rmiz-portal", "");
  document.body.appendChild(elDialogContainer);
}

const enum ModalState {
  LOADED = "LOADED",
  LOADING = "LOADING",
  UNLOADED = "UNLOADED",
  UNLOADING = "UNLOADING",
}

interface BodyAttrs {
  overflow: string;
  width: string;
}

const defaultBodyAttrs: BodyAttrs = {
  overflow: "",
  width: "",
};

export interface ControlledProps {
  children: ReactNode;
  classDialog?: string;
  isZoomed: boolean;
  onZoomChange?: (value: boolean) => void;
  wrapElement?: "div" | "span";
  ZoomContent?: (data: {
    img: ReactElement | null;
    modalState: ModalState;
    onUnzoom: () => void;
  }) => ReactElement;
  zoomImg?: ImgHTMLAttributes<HTMLImageElement>;
  zoomMargin?: number;
}

export function Controlled(props: ControlledProps) {
  return <ControlledBase {...props} />;
}

interface ControlledDefaultProps {
  wrapElement: "div" | "span";
  zoomMargin: number;
}

type ControlledPropsWithDefaults = ControlledDefaultProps & ControlledProps;

interface ControlledState {
  id: string;
  isZoomImgLoaded: boolean;
  loadedImgEl: HTMLImageElement | undefined;
  modalState: ModalState;
  shouldRefresh: boolean;
}

class ControlledBase extends Component<
  ControlledPropsWithDefaults,
  ControlledState
> {
  static defaultProps: ControlledDefaultProps = {
    wrapElement: "div",
    zoomMargin: 0,
  };

  state: ControlledState = {
    id: "",
    isZoomImgLoaded: false,
    loadedImgEl: undefined,
    modalState: ModalState.UNLOADED,
    shouldRefresh: false,
  };

  private refContent = createRef<HTMLDivElement>();
  private refDialog = createRef<HTMLDialogElement>();
  private refModalContent = createRef<HTMLDivElement>();
  private refModalImg = createRef<HTMLImageElement>();
  private refWrap = createRef<HTMLDivElement>();

  private changeObserver: MutationObserver | undefined;
  private imgEl: SupportedImage | null = null;
  private imgElObserver: ResizeObserver | undefined;
  private prevBodyAttrs: BodyAttrs = defaultBodyAttrs;
  private styleModalImg: CSSProperties = {};
  private touchYStart?: number;
  private touchYEnd?: number;

  render() {
    const {
      handleDialogCancel,
      handleDialogClick,
      handleDialogKeyDown,
      handleUnzoom,
      imgEl,
      props: {
        children,
        classDialog,
        isZoomed,
        wrapElement: WrapElement,
        ZoomContent,
        zoomImg,
        zoomMargin,
      },
      refContent,
      refDialog,
      refModalContent,
      refModalImg,
      refWrap,
      state: { id, isZoomImgLoaded, loadedImgEl, modalState, shouldRefresh },
    } = this;

    const idModal = `rmiz-modal-${id}`;
    const idModalImg = `rmiz-modal-img-${id}`;

    // =========================================================================

    const isDiv = testDiv(imgEl);
    const isImg = testImg(imgEl);
    const isSvg = testSvg(imgEl);

    const imgAlt = getImgAlt(imgEl);
    const imgSrc = getImgSrc(imgEl);
    const imgSizes = isImg ? imgEl.sizes : undefined;
    const imgSrcSet = isImg ? imgEl.srcset : undefined;

    const hasZoomImg = !!zoomImg?.src;

    const hasImage =
      imgEl &&
      (loadedImgEl || isSvg) &&
      window.getComputedStyle(imgEl).display !== "none";

    const isModalActive =
      modalState === ModalState.LOADING || modalState === ModalState.LOADED;

    const dataContentState = hasImage ? "found" : "not-found";

    const dataOverlayState =
      modalState === ModalState.UNLOADED || modalState === ModalState.UNLOADING
        ? "hidden"
        : "visible";

    // =========================================================================

    const styleContent: CSSProperties = {
      visibility: modalState === ModalState.UNLOADED ? "visible" : "hidden",
    };

    // Share this with UNSAFE_handleSvg
    this.styleModalImg = hasImage
      ? getStyleModalImg({
          hasZoomImg,
          imgSrc,
          isSvg,
          isZoomed: isZoomed && isModalActive,
          loadedImgEl,
          offset: zoomMargin,
          shouldRefresh,
          targetEl: imgEl,
        })
      : {};

    // =========================================================================

    let modalContent = null;

    if (hasImage) {
      const modalImg =
        isImg || isDiv ? (
          <img
            alt={imgAlt}
            sizes={imgSizes}
            src={imgSrc}
            srcSet={imgSrcSet}
            {...(isZoomImgLoaded && modalState === ModalState.LOADED
              ? zoomImg
              : {})}
            data-rmiz-modal-img=""
            height={this.styleModalImg.height || undefined}
            id={idModalImg}
            ref={refModalImg}
            style={this.styleModalImg}
            width={this.styleModalImg.width || undefined}
          />
        ) : isSvg ? (
          <div
            data-rmiz-modal-img
            ref={refModalImg}
            style={this.styleModalImg}
          />
        ) : null;

      modalContent = ZoomContent ? (
        <ZoomContent
          modalState={modalState}
          img={modalImg}
          onUnzoom={handleUnzoom}
        />
      ) : (
        modalImg
      );
    }

    // =========================================================================

    return (
      <WrapElement aria-owns={idModal} data-rmiz="" ref={refWrap}>
        <WrapElement
          data-rmiz-content={dataContentState}
          ref={refContent}
          style={styleContent}
        >
          {children}
        </WrapElement>
        {hasImage &&
          elDialogContainer !== null &&
          createPortal(
            <dialog
              aria-labelledby={idModalImg}
              aria-modal="true"
              className={classDialog}
              data-rmiz-modal=""
              id={idModal}
              onClick={handleDialogClick}
              // @ts-expect-error does not exist
              onClose={handleUnzoom}
              onCancel={handleDialogCancel}
              onKeyDown={handleDialogKeyDown}
              ref={refDialog}
              role="dialog"
            >
              <div data-rmiz-modal-overlay={dataOverlayState} />
              <div data-rmiz-modal-content="" ref={refModalContent}>
                {modalContent}
                <VisuallyHidden>
                  <button onClick={handleUnzoom}>Close</button>
                </VisuallyHidden>
              </div>
            </dialog>,
            elDialogContainer
          )}
      </WrapElement>
    );
  }

  componentDidMount() {
    this.setId();
    this.setAndTrackImg();
    this.handleImgLoad();
    this.UNSAFE_handleSvg();
  }

  componentWillUnmount() {
    this.changeObserver?.disconnect?.();
    this.imgElObserver?.disconnect?.();
    this.imgEl?.removeEventListener?.("load", this.handleImgLoad);
    this.imgEl?.removeEventListener?.("click", this.handleZoom);
    this.refModalImg.current?.removeEventListener?.(
      "transitionend",
      this.handleZoomEnd
    );
    this.refModalImg.current?.removeEventListener?.(
      "transitionend",
      this.handleUnzoomEnd
    );
    window.removeEventListener("wheel", this.handleWheel);
    window.removeEventListener("touchstart", this.handleTouchStart);
    window.removeEventListener("touchend", this.handleTouchMove);
    window.removeEventListener("touchcancel", this.handleTouchCancel);
    window.removeEventListener("resize", this.handleResize);
  }

  componentDidUpdate(prevProps: ControlledPropsWithDefaults) {
    this.UNSAFE_handleSvg();
    this.handleIfZoomChanged(prevProps.isZoomed);
  }

  // Because of SSR, set a unique ID after render

  setId = () => {
    const gen4 = () => Math.random().toString(16).slice(-4);
    this.setState({ id: gen4() + gen4() + gen4() });
  };

  // Find and set the image we're working with

  setAndTrackImg = () => {
    const contentEl = this.refContent.current;

    if (!contentEl) {
      return;
    }

    this.imgEl = contentEl.querySelector(
      'img:not([aria-hidden="true"])'
    ) as SupportedImage | null;

    if (this.imgEl) {
      this.changeObserver?.disconnect?.();
      this.imgEl?.addEventListener?.("load", this.handleImgLoad);
      this.imgEl?.addEventListener?.("click", this.handleZoom);

      if (!this.state.loadedImgEl) {
        this.handleImgLoad();
      }

      this.imgElObserver = new ResizeObserver((entries) => {
        const entry = entries[0];

        if (entry?.target) {
          this.imgEl = entry.target as SupportedImage;
          this.setState({}); // Force a re-render
        }
      });

      this.imgElObserver.observe(this.imgEl);
    } else if (!this.changeObserver) {
      this.changeObserver = new MutationObserver(this.setAndTrackImg);
      this.changeObserver.observe(contentEl, {
        childList: true,
        subtree: true,
      });
    }
  };

  // Show modal when zoomed; hide modal when unzoomed

  handleIfZoomChanged = (prevIsZoomed: boolean) => {
    const { isZoomed } = this.props;

    if (!prevIsZoomed && isZoomed) {
      this.zoom();
    } else if (prevIsZoomed && !isZoomed) {
      this.unzoom();
    }
  };

  // Ensure we always have the latest img src value loaded

  handleImgLoad = () => {
    const { imgEl } = this;

    const imgSrc = getImgSrc(imgEl);

    if (!imgSrc) {
      return;
    }

    const img = new Image();

    if (testImg(imgEl)) {
      img.sizes = imgEl.sizes;
      img.srcset = imgEl.srcset;
    }

    // img.src must be set after sizes and srcset
    // because of Firefox flickering on zoom
    img.src = imgSrc;

    const setLoaded = () => {
      this.setState({ loadedImgEl: img });
    };

    img
      .decode()
      .then(setLoaded)
      .catch(() => {
        img.onload = setLoaded;
      });
  };

  // Report zoom state changes

  handleZoom = () => {
    this.props.onZoomChange?.(true);
  };

  handleUnzoom = () => {
    this.props.onZoomChange?.(false);
  };

  // Prevent the browser from removing the dialog on Escape

  handleDialogCancel = (e: SyntheticEvent) => {
    e.preventDefault();
  };

  // Have dialog.click() only close in certain situations

  handleDialogClick = (e: MouseEvent<HTMLDialogElement>) => {
    if (
      e.target === this.refModalContent.current ||
      e.target === this.refModalImg.current
    ) {
      this.handleUnzoom();
    }
  };

  // Intercept default dialog.close() and use ours so we can animate

  handleDialogKeyDown = (e: KeyboardEvent<HTMLDialogElement>) => {
    if (e.key === "Escape" || e.keyCode === 27) {
      e.preventDefault();
      e.stopPropagation();
      this.handleUnzoom();
    }
  };

  // Handle wheel and swipe events

  handleWheel = (e: WheelEvent) => {
    e.stopPropagation();
    queueMicrotask(() => {
      this.handleUnzoom();
    });
  };

  handleTouchStart = (e: TouchEvent) => {
    if (e.changedTouches.length === 1 && e.changedTouches[0]) {
      this.touchYStart = e.changedTouches[0].screenY;
    }
  };

  handleTouchMove = (e: TouchEvent) => {
    if (this.touchYStart !== null && e.changedTouches[0]) {
      this.touchYEnd = e.changedTouches[0].screenY;

      const max = Math.max(this.touchYStart || 0, this.touchYEnd);
      const min = Math.min(this.touchYStart || 0, this.touchYEnd);
      const delta = Math.abs(max - min);
      const threshold = 10;

      if (delta > threshold) {
        this.touchYStart = undefined;
        this.touchYEnd = undefined;
        this.handleUnzoom();
      }
    }
  };

  handleTouchCancel = () => {
    this.touchYStart = undefined;
    this.touchYEnd = undefined;
  };

  // Force re-render on resize

  handleResize = () => {
    this.setState({ shouldRefresh: true });
  };

  // Perform zoom actions

  zoom = () => {
    this.refDialog.current?.showModal?.();
    this.setState({ modalState: ModalState.LOADING });
    this.loadZoomImg();

    window.addEventListener("wheel", this.handleWheel, { passive: true });
    window.addEventListener("touchstart", this.handleTouchStart, {
      passive: true,
    });
    window.addEventListener("touchend", this.handleTouchMove, {
      passive: true,
    });
    window.addEventListener("touchcancel", this.handleTouchCancel, {
      passive: true,
    });

    this.refModalImg.current?.addEventListener?.(
      "transitionend",
      this.handleZoomEnd,
      { once: true }
    );
  };

  handleZoomEnd = () => {
    setTimeout(() => {
      this.setState({ modalState: ModalState.LOADED });
      window.addEventListener("resize", this.handleResize, { passive: true });
    }, 0);
  };

  // Perform unzoom actions

  unzoom = () => {
    this.setState({ modalState: ModalState.UNLOADING });

    window.removeEventListener("wheel", this.handleWheel);
    window.removeEventListener("touchstart", this.handleTouchStart);
    window.removeEventListener("touchend", this.handleTouchMove);
    window.removeEventListener("touchcancel", this.handleTouchCancel);

    this.refModalImg.current?.addEventListener?.(
      "transitionend",
      this.handleUnzoomEnd,
      { once: true }
    );
  };

  handleUnzoomEnd = () => {
    setTimeout(() => {
      window.removeEventListener("resize", this.handleResize);

      this.setState({
        shouldRefresh: false,
        modalState: ModalState.UNLOADED,
      });

      this.refDialog.current?.close?.();
    }, 0);
  };

  // Load the zoomImg manually

  loadZoomImg = () => {
    const {
      props: { zoomImg },
    } = this;
    const zoomImgSrc = zoomImg?.src;

    if (zoomImgSrc) {
      const img = new Image();
      img.sizes = zoomImg?.sizes ?? "";
      img.srcset = zoomImg?.srcSet ?? "";
      img.src = zoomImgSrc;

      const setLoaded = () => {
        this.setState({ isZoomImgLoaded: true });
      };

      img
        .decode()
        .then(setLoaded)
        .catch(() => {
          img.onload = setLoaded;
        });
    }
  };

  // Hackily deal with SVGs because of all of their unknowns.

  UNSAFE_handleSvg = () => {
    const { imgEl, refModalImg, styleModalImg } = this;

    if (testSvg(imgEl)) {
      const tmp = document.createElement("div");
      tmp.innerHTML = imgEl.outerHTML;

      const svg = tmp.firstChild as SVGSVGElement;
      svg.style.width = `${styleModalImg.width || 0}px`;
      svg.style.height = `${styleModalImg.height || 0}px`;
      svg.addEventListener("click", this.handleUnzoom);

      refModalImg.current?.firstChild?.remove?.();
      refModalImg.current?.appendChild?.(svg);
    }
  };
}
