import { CSSProperties } from "react";
import type { SupportedImage } from "./types";

interface TestElType {
  (type: string, el: unknown): boolean;
}

const testElType: TestElType = (type, el) =>
  type === (el as Element)?.tagName?.toUpperCase?.();

export const testDiv = (el: unknown): el is HTMLDivElement | HTMLSpanElement =>
  testElType("DIV", el) || testElType("SPAN", el);

export const testImg = (el: unknown): el is HTMLImageElement =>
  testElType("IMG", el);

export const testSvg = (el: unknown): el is SVGElement => testElType("SVG", el);

export interface GetScaleToWindow {
  (data: { width: number; height: number; offset: number }): number;
}

export const getScaleToWindow: GetScaleToWindow = ({
  height,
  offset,
  width,
}) => {
  return Math.min(
    (window.innerWidth - offset * 2) / width, // scale X-axis
    (window.innerHeight - offset * 2) / height // scale Y-axis
  );
};

export interface GetScaleToWindowMax {
  (data: {
    containerHeight: number;
    containerWidth: number;
    offset: number;
    targetHeight: number;
    targetWidth: number;
  }): number;
}

export const getScaleToWindowMax: GetScaleToWindowMax = ({
  containerHeight,
  containerWidth,
  offset,
  targetHeight,
  targetWidth,
}) => {
  const scale = getScaleToWindow({
    height: targetHeight,
    offset,
    width: targetWidth,
  });

  const ratio =
    targetWidth > targetHeight
      ? targetWidth / containerWidth
      : targetHeight / containerHeight;

  return scale > 1 ? ratio : scale * ratio;
};

export interface GetScale {
  (data: {
    containerHeight: number;
    containerWidth: number;
    hasScalableSrc: boolean;
    offset: number;
    targetHeight: number;
    targetWidth: number;
  }): number;
}

export const getScale: GetScale = ({
  containerHeight,
  containerWidth,
  hasScalableSrc,
  offset,
  targetHeight,
  targetWidth,
}) => {
  return !hasScalableSrc && targetHeight && targetWidth
    ? getScaleToWindowMax({
        containerHeight,
        containerWidth,
        offset,
        targetHeight,
        targetWidth,
      })
    : getScaleToWindow({
        height: containerHeight,
        offset,
        width: containerWidth,
      });
};

const URL_REGEX = /url(?:\(['"]?)(.*?)(?:['"]?\))/;

export interface GetImgSrc {
  (imgEl: SupportedImage | null): string | undefined;
}

export const getImgSrc: GetImgSrc = (imgEl) => {
  if (imgEl) {
    if (testImg(imgEl)) {
      return imgEl.currentSrc;
    } else if (testDiv(imgEl)) {
      const bgImg = window.getComputedStyle(imgEl).backgroundImage;

      if (bgImg) {
        return URL_REGEX.exec(bgImg)?.[1];
      }
    }
  }
  return;
};

export interface GetImgAlt {
  (imgEl: SupportedImage | null): string | undefined;
}

export const getImgAlt: GetImgAlt = (imgEl) => {
  if (imgEl) {
    if (testImg(imgEl)) {
      return imgEl.alt ?? undefined;
    } else {
      return imgEl.getAttribute("aria-label") ?? undefined;
    }
  }
  return;
};

export interface GetImgRegularStyle {
  (data: {
    containerHeight: number;
    containerLeft: number;
    containerTop: number;
    containerWidth: number;
    hasScalableSrc: boolean;
    offset: number;
    targetHeight: number;
    targetWidth: number;
  }): CSSProperties;
}

export const getImgRegularStyle: GetImgRegularStyle = ({
  containerHeight,
  containerLeft,
  containerTop,
  containerWidth,
  hasScalableSrc,
  offset,
  targetHeight,
  targetWidth,
}) => {
  const scale = getScale({
    containerHeight,
    containerWidth,
    hasScalableSrc,
    offset,
    targetHeight,
    targetWidth,
  });

  return {
    top: containerTop,
    left: containerLeft,
    width: containerWidth * scale,
    height: containerHeight * scale,
    transform: `translate(0,0) scale(${1 / scale})`,
  };
};

export interface ParsePosition {
  (data: { position: string; relativeNum: number }): number;
}

export const parsePosition: ParsePosition = ({ position, relativeNum }) => {
  const positionNum = parseFloat(position);

  return position.endsWith("%")
    ? (relativeNum * positionNum) / 100
    : positionNum;
};

export interface GetImgObjectFitStyle {
  (data: {
    containerHeight: number;
    containerLeft: number;
    containerTop: number;
    containerWidth: number;
    hasScalableSrc: boolean;
    objectFit: string;
    objectPosition: string;
    offset: number;
    targetHeight: number;
    targetWidth: number;
  }): CSSProperties;
}

export const getImgObjectFitStyle: GetImgObjectFitStyle = ({
  containerHeight,
  containerLeft,
  containerTop,
  containerWidth,
  hasScalableSrc,
  objectFit,
  objectPosition,
  offset,
  targetHeight,
  targetWidth,
}) => {
  if (objectFit === "scale-down") {
    if (targetWidth <= containerWidth && targetHeight <= containerHeight) {
      objectFit = "none";
    } else {
      objectFit = "contain";
    }
  }

  if (objectFit === "cover" || objectFit === "contain") {
    const widthRatio = containerWidth / targetWidth;
    const heightRatio = containerHeight / targetHeight;

    const ratio =
      objectFit === "cover"
        ? Math.max(widthRatio, heightRatio)
        : Math.min(widthRatio, heightRatio);

    const [posLeft = "50%", posTop = "50%"] = objectPosition.split(" ");
    const posX = parsePosition({
      position: posLeft,
      relativeNum: containerWidth - targetWidth * ratio,
    });
    const posY = parsePosition({
      position: posTop,
      relativeNum: containerHeight - targetHeight * ratio,
    });

    const scale = getScale({
      containerHeight: targetHeight * ratio,
      containerWidth: targetWidth * ratio,
      hasScalableSrc,
      offset,
      targetHeight,
      targetWidth,
    });

    return {
      top: containerTop + posY,
      left: containerLeft + posX,
      width: targetWidth * ratio * scale,
      height: targetHeight * ratio * scale,
      transform: `translate(0,0) scale(${1 / scale})`,
    };
  } else if (objectFit === "none") {
    const [posLeft = "50%", posTop = "50%"] = objectPosition.split(" ");
    const posX = parsePosition({
      position: posLeft,
      relativeNum: containerWidth - targetWidth,
    });
    const posY = parsePosition({
      position: posTop,
      relativeNum: containerHeight - targetHeight,
    });

    const scale = getScale({
      containerHeight: targetHeight,
      containerWidth: targetWidth,
      hasScalableSrc,
      offset,
      targetHeight,
      targetWidth,
    });

    return {
      top: containerTop + posY,
      left: containerLeft + posX,
      width: targetWidth * scale,
      height: targetHeight * scale,
      transform: `translate(0,0) scale(${1 / scale})`,
    };
  } else if (objectFit === "fill") {
    const widthRatio = containerWidth / targetWidth;
    const heightRatio = containerHeight / targetHeight;
    const ratio = Math.max(widthRatio, heightRatio);

    const scale = getScale({
      containerHeight: targetHeight * ratio,
      containerWidth: targetWidth * ratio,
      hasScalableSrc,
      offset,
      targetHeight,
      targetWidth,
    });

    return {
      width: containerWidth * scale,
      height: containerHeight * scale,
      transform: `translate(0,0) scale(${1 / scale})`,
    };
  } else {
    return {};
  }
};

export interface GetDivImgStyle {
  (data: {
    backgroundPosition: string;
    backgroundSize: string;
    containerHeight: number;
    containerLeft: number;
    containerTop: number;
    containerWidth: number;
    hasScalableSrc: boolean;
    offset: number;
    targetHeight: number;
    targetWidth: number;
  }): CSSProperties;
}

export const getDivImgStyle: GetDivImgStyle = ({
  backgroundPosition,
  backgroundSize,
  containerHeight,
  containerLeft,
  containerTop,
  containerWidth,
  hasScalableSrc,
  offset,
  targetHeight,
  targetWidth,
}) => {
  if (backgroundSize === "cover" || backgroundSize === "contain") {
    const widthRatio = containerWidth / targetWidth;
    const heightRatio = containerHeight / targetHeight;

    const ratio =
      backgroundSize === "cover"
        ? Math.max(widthRatio, heightRatio)
        : Math.min(widthRatio, heightRatio);

    const [posLeft = "50%", posTop = "50%"] = backgroundPosition.split(" ");
    const posX = parsePosition({
      position: posLeft,
      relativeNum: containerWidth - targetWidth * ratio,
    });
    const posY = parsePosition({
      position: posTop,
      relativeNum: containerHeight - targetHeight * ratio,
    });

    const scale = getScale({
      containerHeight: targetHeight * ratio,
      containerWidth: targetWidth * ratio,
      hasScalableSrc,
      offset,
      targetHeight,
      targetWidth,
    });

    return {
      top: containerTop + posY,
      left: containerLeft + posX,
      width: targetWidth * ratio * scale,
      height: targetHeight * ratio * scale,
      transform: `translate(0,0) scale(${1 / scale})`,
    };
  } else if (backgroundSize === "auto") {
    const [posLeft = "50%", posTop = "50%"] = backgroundPosition.split(" ");
    const posX = parsePosition({
      position: posLeft,
      relativeNum: containerWidth - targetWidth,
    });
    const posY = parsePosition({
      position: posTop,
      relativeNum: containerHeight - targetHeight,
    });

    const scale = getScale({
      containerHeight: targetHeight,
      containerWidth: targetWidth,
      hasScalableSrc,
      offset,
      targetHeight,
      targetWidth,
    });

    return {
      top: containerTop + posY,
      left: containerLeft + posX,
      width: targetWidth * scale,
      height: targetHeight * scale,
      transform: `translate(0,0) scale(${1 / scale})`,
    };
  } else {
    const [sizeW = "50%", sizeH = "50%"] = backgroundSize.split(" ");
    const sizeWidth = parsePosition({
      position: sizeW,
      relativeNum: containerWidth,
    });
    const sizeHeight = parsePosition({
      position: sizeH,
      relativeNum: containerHeight,
    });

    const widthRatio = sizeWidth / targetWidth;
    const heightRatio = sizeHeight / targetHeight;

    // @TODO: something funny is happening with this ratio
    const ratio = Math.min(widthRatio, heightRatio);

    const [posLeft = "50%", posTop = "50%"] = backgroundPosition.split(" ");
    const posX = parsePosition({
      position: posLeft,
      relativeNum: containerWidth - targetWidth * ratio,
    });
    const posY = parsePosition({
      position: posTop,
      relativeNum: containerHeight - targetHeight * ratio,
    });

    const scale = getScale({
      containerHeight: targetHeight * ratio,
      containerWidth: targetWidth * ratio,
      hasScalableSrc,
      offset,
      targetHeight,
      targetWidth,
    });

    return {
      top: containerTop + posY,
      left: containerLeft + posX,
      width: targetWidth * ratio * scale,
      height: targetHeight * ratio * scale,
      transform: `translate(0,0) scale(${1 / scale})`,
    };
  }
};

const SRC_SVG_REGEX = /\.svg$/i;

export interface GetStyleModalImg {
  (data: {
    hasZoomImg: boolean;
    imgSrc: string | undefined;
    isSvg: boolean;
    isZoomed: boolean;
    loadedImgEl: HTMLImageElement | undefined;
    offset: number;
    shouldRefresh: boolean;
    targetEl: SupportedImage;
  }): CSSProperties;
}

export const getStyleModalImg: GetStyleModalImg = ({
  hasZoomImg,
  imgSrc,
  isSvg,
  isZoomed,
  loadedImgEl,
  offset,
  shouldRefresh,
  targetEl,
}) => {
  const hasScalableSrc =
    isSvg ||
    imgSrc?.slice?.(0, 18) === "data:image/svg+xml" ||
    hasZoomImg ||
    !!(imgSrc && SRC_SVG_REGEX.test(imgSrc));

  const imgRect = targetEl.getBoundingClientRect();
  const targetElComputedStyle = window.getComputedStyle(targetEl);

  const styleImgRegular = getImgRegularStyle({
    containerHeight: imgRect.height,
    containerLeft: imgRect.left,
    containerTop: imgRect.top,
    containerWidth: imgRect.width,
    hasScalableSrc,
    offset,
    targetHeight: loadedImgEl?.naturalHeight ?? imgRect.height,
    targetWidth: loadedImgEl?.naturalWidth ?? imgRect.width,
  });

  const styleImgObjectFit =
    loadedImgEl && targetElComputedStyle.objectFit
      ? getImgObjectFitStyle({
          containerHeight: imgRect.height,
          containerLeft: imgRect.left,
          containerTop: imgRect.top,
          containerWidth: imgRect.width,
          hasScalableSrc,
          objectFit: targetElComputedStyle.objectFit,
          objectPosition: targetElComputedStyle.objectPosition,
          offset,
          targetHeight: loadedImgEl.naturalHeight,
          targetWidth: loadedImgEl.naturalWidth,
        })
      : undefined;

  const styleDivImg =
    loadedImgEl && testDiv(targetEl)
      ? getDivImgStyle({
          backgroundPosition: targetElComputedStyle.backgroundPosition,
          backgroundSize: targetElComputedStyle.backgroundSize,
          containerHeight: imgRect.height,
          containerLeft: imgRect.left,
          containerTop: imgRect.top,
          containerWidth: imgRect.width,
          hasScalableSrc,
          offset,
          targetHeight: loadedImgEl.naturalHeight,
          targetWidth: loadedImgEl.naturalWidth,
        })
      : undefined;

  const style = Object.assign(
    {},
    styleImgRegular,
    styleImgObjectFit,
    styleDivImg
  );

  if (isZoomed) {
    const viewportX = window.innerWidth / 2;
    const viewportY = window.innerHeight / 2;

    const childCenterX =
      parseFloat(String(style.left || 0)) +
      parseFloat(String(style.width || 0)) / 2;
    const childCenterY =
      parseFloat(String(style.top || 0)) +
      parseFloat(String(style.height || 0)) / 2;

    const translateX = viewportX - childCenterX;
    const translateY = viewportY - childCenterY;

    // For scenarios like resizing the browser window
    if (shouldRefresh) {
      style.transitionDuration = "0.01ms";
    }

    style.transform = `translate(${translateX}px,${translateY}px) scale(1)`;
  }

  return style;
};

export interface GetStyleGhost {
  (imgEl: SupportedImage | null): CSSProperties;
}
