import { clamp, Mouse } from '@vev/utils';
import React, { useEffect, useMemo, useRef } from 'react';
import { useFormField } from '..';
import { FormFieldProps } from '../silke-form';
import { SilkeSliderPoint } from './silke-slider-point';
import { findClosestIndex, getNewValue } from './silke-slider-utils';
import styles from './silke-slider.module.scss';
import { SliderTrack } from './slider-track';
import { debounce } from 'lodash';

export type SilkeSliderProps<T extends number | number[]> = FormFieldProps<T> & {
  className?: string;
  disabled?: boolean;

  trackClassName?: string;
  handleClassName?: string;

  style?: React.CSSProperties;
  trackStyle?: React.CSSProperties;
  handleStyle?: React.CSSProperties;

  /** disabling order will allow points to be dragged anywhere on the slide, no matter index in the value array */
  disableOrder?: boolean;
  /** To highlight one of the dots as selected */
  selected?: number;
  /** If onAdd will allow points to be added by clicking on the slider */
  onTrackClick?: (e: React.MouseEvent, value: number) => void;
  onHandleClick?: (e: React.MouseEvent, index: number) => void;
  onRequestRemove?: (index: number) => boolean;
  debounceMs?: number;
};

export function SilkeSlider<T extends number | number[]>(props: SilkeSliderProps<T>) {
  const {
    disabled,
    disableOrder,
    trackClassName,
    trackStyle,
    handleClassName,
    handleStyle,
    value,
    className,
    style,
    selected,
    onChange,
    onHandleClick,
    onTrackClick,
    onRequestRemove,
    debounceMs = 0,
  } = useFormField(props);

  const ref = useRef<HTMLDivElement>(null);
  const pointRef = useRef<number[]>([]);
  const [tmpValue, setTmpValue] = React.useState<number[] | undefined>(undefined);
  const [dragging, setDragging] = React.useState<number | undefined>(undefined);
  const lastDrag = useRef<number>(0);
  if (dragging !== undefined) lastDrag.current = dragging;

  const valueList =
    typeof value === 'number' ? [value] : !value?.length ? [1] : (value as number[]);

  // Add memoized debounced onChange
  const debouncedOnChange = useMemo(
    () => debounce((newValue: number | number[]) => onChange?.(newValue), debounceMs),
    [onChange],
  );

  // Cleanup debounce on unmount
  useEffect(() => debouncedOnChange.flush, [debouncedOnChange]);

  useEffect(() => {
    const el = ref.current;
    if (dragging === undefined || !el) return;
    const handleMouseMove = (e: MouseEvent) => {
      const { left, width, top } = el.getBoundingClientRect();
      const dy = Math.abs(e.pageY - top);
      if (dy > 100 && onRequestRemove?.(dragging)) {
        return handleMouseUp();
      }
      const pointValue = clamp((e.pageX - left) / width, 0, 1);
      const newValues = getNewValue(
        [...(tmpValue || valueList || [1])],
        dragging,
        pointValue,
        disableOrder,
      );

      setTmpValue(newValues);
      debouncedOnChange(typeof value === 'number' ? newValues[0] : newValues);
    };

    const handleMouseUp = () => {
      setDragging(undefined);
      setTmpValue(undefined);
    };

    Mouse.on('mousemove', handleMouseMove);
    Mouse.on('mouseup', handleMouseUp);

    return () => {
      Mouse.off('mousemove', handleMouseMove);
      Mouse.off('mouseup', handleMouseUp);
    };
  }, [dragging, disableOrder]);

  const handlePointDown = (e: React.MouseEvent, index: number) => {
    e.stopPropagation();
    onHandleClick?.(e, index);
    setTmpValue(points);
    setDragging(index);
  };

  const handleTrackDown = (e: React.MouseEvent) => {
    const { left, width } = e.currentTarget.getBoundingClientRect();
    const pointValue = clamp((e.pageX - left) / width, 0, 1);
    if (onTrackClick) onTrackClick(e, pointValue);
    else {
      const closesIndex = findClosestIndex(points, pointValue);
      lastDrag.current = closesIndex;
      const newValues = getNewValue(
        [...(tmpValue || valueList || [1])],
        closesIndex,
        pointValue,
        disableOrder,
      );
      setTmpValue(newValues);
      setDragging(closesIndex);
      debouncedOnChange?.(typeof value === 'number' ? newValues[0] : newValues);
    }

    // Focus the closest handle after a short delay in case a new point was added
    setTimeout(() => {
      const closesIndex = findClosestIndex(pointRef.current, pointValue);

      const handleEl = ref.current?.querySelector(
        `.${styles.handle}:nth-child(${closesIndex + 1})`,
      ) as HTMLElement;
      if (handleEl) handleEl.focus();
    }, 300);
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    const index = lastDrag.current;
    if (e.key === 'Backspace' || e.key === 'Delete') {
      if (onRequestRemove?.(lastDrag.current)) {
        e.preventDefault();
        e.stopPropagation();
      }
    }

    if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
      e.preventDefault();
      e.stopPropagation();

      let step = e.key === 'ArrowRight' ? -0.01 : 0.01;
      if (e.shiftKey) step *= 10;
      const newValues = getNewValue(
        [...(tmpValue || valueList || [1])],
        index,
        points[index] - step,
        disableOrder,
      );
      debouncedOnChange?.(typeof value === 'number' ? newValues[0] : newValues);
    }
  };

  const points = (tmpValue || valueList).map((v) => clamp(v, 0, 1));
  pointRef.current = points;
  const range = [points[0] || 0, points[points.length - 1] || 1] as [number, number];

  let cl = styles.root;
  if (dragging !== undefined) cl += ` ${styles.dragging}`;
  if (className) cl += ` ${className}`;
  if (disabled) cl += ` ${styles.disabled}`;

  return (
    <div ref={ref} className={cl} style={style} onMouseDown={handleTrackDown}>
      <SliderTrack
        className={trackClassName}
        style={trackStyle}
        range={points.length === 2 && !disableOrder ? range : undefined}
      >
        {points.map((point, i) => (
          <SilkeSliderPoint
            key={i}
            className={handleClassName}
            selected={selected === i}
            dragging={dragging === i}
            value={point}
            style={handleStyle}
            onMouseDown={(e) => handlePointDown(e, i)}
            onKeyDown={handleKeyDown}
            onFocus={() => (lastDrag.current = i)}
          />
        ))}
      </SliderTrack>
    </div>
  );
}
