import React, { useCallback, useEffect, useRef, useState } from "react";
import {
  findNodeHandle,
  ImageBackground,
  LayoutChangeEvent,
} from "react-native";

import { Button, View } from "mazlo-ui";
import checkerboard from "mazlo-ui/images/checkerboard.svg";

import { getScaleRange, getDefaultCrop } from "../../utils/image";
import styles from "./styles";

export type Crop = {
  x: number;
  y: number;
  scale: number;
};

type Image = {
  height: number;
  width: number;
  url: string;
};

type Props = {
  crop?: Crop;
  image: Image;
  height: number;
  width: number;
  frameHeight?: number;
  onClear?: () => void;
  onCrop: (crop: Crop) => void;
  onStartAdjust?: () => void;
};

function useWheel(view, handler, params) {
  const callback = useCallback(handler, params);
  const ref = useRef(callback);
  useEffect(() => {
    ref.current = callback;
  }, [callback]);

  useEffect(() => {
    const elem = findNodeHandle(view) as any;
    if (!elem) return;
    const listener = (event) => ref.current(event);
    elem.addEventListener("wheel", listener, { passive: false });
    return () => {
      elem.removeEventListener("wheel", listener, { passive: false });
    };
  }, [view]);
}

const noOffset = { scale: 0, x: 0, y: 0 };

const ImageCrop = ({
  crop,
  image,
  height,
  width,
  onClear,
  onCrop,
  onStartAdjust,
}: Props) => {
  const { x, y, scale } = crop || getDefaultCrop(image, height, width);
  const { min: minScale, max: maxScale } = getScaleRange(image, height, width);

  const [frameWidth, setFrameWidth] = useState(500);
  const [adjusting, setAdjusting] = useState(false);
  const [dragging, setDragging] = useState(false);
  const [{ scale: dScale, x: dx, y: dy }, setOffset] = useState(noOffset);

  const frameScale = Math.min(1, frameWidth / width, 400 / height);
  const offsetScale = scale + dScale;
  const imageScale = offsetScale * frameScale;
  const translateX = (dx - x) / offsetScale;
  const translateY = (dy - y) / offsetScale;

  const clampOffset = useCallback(
    (fn) => {
      setOffset((current) => {
        const offset = { ...current, ...fn(current) };
        const { min, max } = getScaleRange(image, height, width);
        const clampedScale = Math.max(Math.min(scale + offset.scale, max), min);
        const minOffsetX = x - image.width * clampedScale + width;
        const minOffsetY = y - image.height * clampedScale + height;

        return {
          scale: clampedScale - scale,
          x: Math.max(Math.min(offset.x, x), minOffsetX),
          y: Math.max(Math.min(offset.y, y), minOffsetY),
        };
      });
    },
    [image, scale, x, y, width, height]
  );

  const container = useRef();
  useWheel(
    container.current,
    (e: MouseWheelEvent) => {
      if (!adjusting) return;
      e.preventDefault();
      clampOffset((offset) => ({ scale: offset.scale + e.deltaY / 100 }));
    },
    [adjusting, clampOffset]
  );

  const onMouseDown = useCallback(() => {
    if (!adjusting) return;

    setDragging(true);

    const onMouseMove = (e: MouseEvent) => {
      e.preventDefault();
      clampOffset((offset) => ({
        x: offset.x + e.movementX / frameScale,
        y: offset.y + e.movementY / frameScale,
      }));
    };
    const onMouseUp = () => {
      setDragging(false);
      window.removeEventListener("mousemove", onMouseMove);
      window.removeEventListener("mouseup", onMouseUp);
    };
    window.addEventListener("mousemove", onMouseMove);
    window.addEventListener("mouseup", onMouseUp);
  }, [adjusting, clampOffset]);

  const onZoom = useCallback(
    (e) => {
      const { value } = e.target;
      clampOffset(() => ({
        scale: +value - scale,
      }));
    },
    [clampOffset, scale]
  );

  const toggleAdjusting = useCallback(() => {
    setAdjusting((active) => {
      if (active) {
        setOffset((offset) => {
          onCrop({
            scale: offsetScale,
            x: x - offset.x,
            y: y - offset.y,
          });
          return noOffset;
        });
      } else if (onStartAdjust) {
        onStartAdjust();
      }
      return !active;
    });
  }, [x, y, offsetScale, onStartAdjust]);

  const onLayout = useCallback((e: LayoutChangeEvent) => {
    setFrameWidth(e.nativeEvent.layout.width);
  }, []);

  return (
    <ImageBackground
      ref={container}
      resizeMode="repeat"
      source={{ uri: checkerboard }}
      style={styles.container}
    >
      <View style={styles.frame} onLayout={onLayout}>
        <View
          style={[
            styles.image,
            {
              height: `${height * frameScale}px`,
              width: `${width * frameScale}px`,
            },
          ]}
        >
          <img
            src={image.url}
            style={{
              cursor: adjusting ? (dragging ? "grabbing" : "grab") : "auto",
              transform: `scale(${imageScale}) translate(${translateX}px, ${translateY}px)`,
              transformOrigin: "top left",
            }}
            onMouseDown={onMouseDown}
          />
          <View
            pointerEvents="none"
            style={[styles.mask, adjusting && styles.maskAdjusting]}
          />
        </View>
      </View>
      <View style={styles.controls}>
        <View style={styles.zoom}>
          {adjusting && maxScale > minScale && (
            <input
              type="range"
              value={offsetScale}
              min={minScale}
              max={maxScale}
              step={(maxScale - minScale) / 100}
              style={{ width: "100%", maxWidth: 200 }}
              onChange={onZoom}
            />
          )}
        </View>
        {!adjusting && !!onClear && (
          <Button onPress={onClear} title="Clear Image" variant="tertiary" />
        )}
        <Button
          onPress={toggleAdjusting}
          style={styles.button}
          title={adjusting ? "Accept Crop" : "Adjust Crop"}
          variant="primary"
        />
      </View>
    </ImageBackground>
  );
};

export default ImageCrop;
