import React, {
  ChangeEvent,
  forwardRef,
  HTMLAttributes,
  ReactNode,
  useEffect,
  useRef,
  useState,
} from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'

import { useForkedRef } from '../../hooks'
import { isRTL } from '../../utils'

import { ThumbSize, type Label } from './types'
import {
  calculateClickValue,
  calculateLabelPosition,
  calculateMoveValue,
  calculateTooltipPosition,
  getLabelValue,
  getNearestValueIndex,
  getThumbSize,
  updateGradient,
  updateValue,
  validateValue,
} from './utils'

export interface CRangeSliderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
  /**
   * Customize the styling of your React Range Slider by adding a custom `className`.
   * This allows you to apply additional CSS classes for enhanced design flexibility and integration with your existing stylesheets.
   */
  className?: string
  /**
   * Enable or disable clickable labels in the React Range Slider.
   * When set to `true`, users can click on labels to adjust the slider's value directly, enhancing interactivity and user experience.
   */
  clickableLabels?: boolean
  /**
   * Control the interactive state of the React Range Slider with the `disabled` prop.
   * Setting it to `true` will disable all slider functionalities, preventing user interaction and visually indicating a non-interactive state.
   */
  disabled?: boolean
  /**
   * Define the minimum distance between slider handles using the `distance` prop in the React Range Slider.
   * This ensures that the handles maintain a specified separation, preventing overlap and maintaining clear value distinctions.
   */
  distance?: number
  /**
   * Add descriptive labels to your React Range Slider by providing an array of `labels`.
   * These labels enhance the slider's usability by clearly indicating key values and providing contextual information to users.
   */
  labels?: Label[]
  /**
   * Specify the maximum value for the React Range Slider with the `max` prop.
   * This determines the upper limit of the slider's range, enabling precise control over the highest selectable value.
   */
  max?: number
  /**
   * Set the minimum value for the React Range Slider using the `min` prop.
   * This defines the lower bound of the slider's range, allowing you to control the starting point of user selection.
   */
  min?: number
  /**
   * Assign a `name` to the React Range Slider for form integration.
   * Whether using a single string or an array of strings, this prop ensures that the slider's values are correctly identified when submitting forms.
   */
  name?: string | string[]
  /**
   * Control the granularity of the React Range Slider by setting the `step` prop.
   * This defines the increment intervals between selectable values, allowing for precise adjustments based on your application's requirements.
   */
  step?: number
  /**
   * Handle value changes in the React Range Slider by utilizing the `onChange` callback.
   * This function is triggered whenever the slider's value updates, enabling you to manage state and respond to user interactions effectively.
   */
  onChange?: (value: number[]) => void
  /**
   * Toggle the visibility of tooltips in the React Range Slider with the `tooltips` prop.
   * When enabled, tooltips display the current value of the slider handles, providing real-time feedback to users.
   */
  tooltips?: boolean
  /**
   * Customize the display format of tooltips in the React Range Slider using the `tooltipsFormat` function.
   * This allows you to format the tooltip values according to your specific requirements, enhancing the clarity and presentation of information.
   */
  tooltipsFormat?: (value: number) => ReactNode
  /**
   * Controls the visual representation of the slider's track. When set to `'fill'`, the track is dynamically filled based on the slider's value(s). Setting it to `false` disables the filled track.
   */
  track?: 'fill' | boolean
  /**
   * Set the current value(s) of the React Range Slider using the `value` prop.
   * Whether you're using a single value or an array for multi-handle sliders, this prop controls the slider's position and ensures it reflects the desired state.
   */
  value?: number | number[]
  /**
   * Orient the React Range Slider vertically by setting the `vertical` prop to `true`.
   * This changes the slider's layout from horizontal to vertical, providing a different aesthetic and fitting various UI designs.
   */
  vertical?: boolean
}

export const CRangeSlider = forwardRef<HTMLDivElement, CRangeSliderProps>(
  (
    {
      className,
      clickableLabels = true,
      disabled = false,
      distance = 0,
      labels,
      min = 0,
      max = 100,
      name,
      step = 1,
      value = [],
      onChange,
      tooltips = true,
      tooltipsFormat,
      track = 'fill',
      vertical = false,
      ...rest
    },
    ref,
  ) => {
    const rangeSliderRef = useRef<HTMLDivElement>(null)
    const forkedRef = useForkedRef(ref, rangeSliderRef)
    const inputsRef = useRef<HTMLInputElement[]>([])
    const labelsContainerRef = useRef<HTMLDivElement>(null)
    const labelsRef = useRef<HTMLDivElement[]>([])
    const trackRef = useRef<HTMLDivElement>(null)

    const [currentValue, setCurrentValue] = useState<number[]>(
      Array.isArray(value) ? value : [value],
    )
    const [isDragging, setIsDragging] = useState(false)
    const [_isRTL, setIsRTL] = useState(false)
    const [dragIndex, setDragIndex] = useState(0)
    const [thumbSize, setThumbSize] = useState<ThumbSize | null>()

    useEffect(() => {
      setCurrentValue(Array.isArray(value) ? value : [value])
    }, [value])

    useEffect(() => {
      if (rangeSliderRef.current) {
        setIsRTL(isRTL(rangeSliderRef.current))
        setThumbSize(getThumbSize(rangeSliderRef.current, vertical))
      }
    }, [rangeSliderRef])

    useEffect(() => {
      const maxSize = Math.max(
        ...labelsRef.current.map((label) => (vertical ? label.offsetWidth : label.offsetHeight)),
      )

      if (labelsContainerRef.current) {
        labelsContainerRef.current.style[vertical ? 'width' : 'height'] = `${maxSize}px`
      }
    }, [labelsRef])

    useEffect(() => {
      if (isDragging) {
        document.addEventListener('mousemove', handleMouseMove)
        document.addEventListener('mouseup', handleMouseUp)
      }

      return () => {
        document.removeEventListener('mousemove', handleMouseMove)
        document.removeEventListener('mouseup', handleMouseUp)
      }
    }, [currentValue])

    const updateNearestValue = (value: number) => {
      const nearestIndex = getNearestValueIndex(value, currentValue)
      const newCurrentValue = [...currentValue]
      newCurrentValue[nearestIndex] = validateValue(value, currentValue, distance, nearestIndex)

      setTimeout(() => {
        inputsRef.current[nearestIndex].focus()
      })

      setCurrentValue(newCurrentValue)

      if (onChange) {
        onChange(newCurrentValue)
      }
    }

    const handleInputChange = (event: ChangeEvent<HTMLInputElement>, index: number) => {
      setIsDragging(false)

      const target = event.target
      const value = Number(target.value)

      const newCurrentValue = updateValue(value, currentValue, distance, index)

      setCurrentValue(newCurrentValue)

      // Trigger change event if needed
      if (onChange) {
        onChange(newCurrentValue)
      }
    }

    const handleInputsContainerMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
      if (trackRef.current === null || event.button !== 0 || disabled) {
        return
      }

      const target = event.target as HTMLDivElement | HTMLInputElement
      if (!(target instanceof HTMLInputElement) && target !== trackRef.current) {
        return
      }

      const clickValue = calculateClickValue(
        event,
        trackRef.current,
        min,
        max,
        step,
        vertical,
        _isRTL,
      )

      const index = getNearestValueIndex(clickValue, currentValue)

      setIsDragging(true)
      setDragIndex(index)
      updateNearestValue(clickValue)
    }

    const handleLabelClick = (event: React.MouseEvent<HTMLDivElement>, value: number) => {
      if (!clickableLabels || disabled || event.button !== 0) {
        return
      }

      updateNearestValue(value)
    }

    const handleMouseMove = (event: MouseEvent) => {
      if (!isDragging || trackRef.current === null || disabled) {
        return
      }

      const moveValue = calculateMoveValue(
        event,
        trackRef.current,
        min,
        max,
        step,
        vertical,
        _isRTL,
      )

      const newCurrentValue = updateValue(moveValue, currentValue, distance, dragIndex)

      setCurrentValue(newCurrentValue)

      if (onChange) {
        onChange(newCurrentValue)
      }
    }

    const handleMouseUp = () => {
      setIsDragging(false)
    }

    return (
      <div
        className={classNames('range-slider', className, {
          'range-slider-vertical': vertical,
          disabled,
        })}
        {...rest}
        ref={forkedRef}
      >
        <div className="range-slider-inputs-container" onMouseDown={handleInputsContainerMouseDown}>
          {currentValue.map((value, index) => (
            <React.Fragment key={index}>
              <input
                className="range-slider-input"
                type="range"
                min={min}
                max={max}
                step={step}
                value={value}
                name={Array.isArray(name) ? name[index] : `${name || ''}-${index}}`}
                role="slider"
                aria-valuemin={min}
                aria-valuemax={max}
                aria-valuenow={value}
                aria-orientation={vertical ? 'vertical' : 'horizontal'}
                disabled={disabled}
                onChange={(event) => handleInputChange(event, index)}
                ref={(el: HTMLInputElement) => {
                  inputsRef.current[index] = el
                }}
              />
              {tooltips && (
                <div
                  className="range-slider-tooltip"
                  {...(thumbSize && {
                    style: calculateTooltipPosition(min, max, value, thumbSize, vertical, _isRTL),
                  })}
                >
                  <div className="range-slider-tooltip-inner">
                    {tooltipsFormat ? tooltipsFormat(value) : value}
                  </div>
                  <div className="range-slider-tooltip-arrow"></div>
                </div>
              )}
            </React.Fragment>
          ))}
          <div
            className="range-slider-track"
            {...(track && {
              style: updateGradient(min, max, currentValue, vertical, _isRTL),
            })}
            ref={trackRef}
          ></div>
        </div>
        {labels && (
          <div className="range-slider-labels-container" ref={labelsContainerRef}>
            {Array.isArray(labels) &&
              labels.map((label: Label, index) => {
                const labelPosition = calculateLabelPosition(min, max, labels, label, index)
                const labelValue = getLabelValue(min, max, labels, label, index)
                const labelStyle = Object.assign(
                  vertical
                    ? { bottom: labelPosition }
                    : _isRTL
                      ? { right: labelPosition }
                      : { left: labelPosition },
                  typeof label === 'object' && 'style' in label && label.style,
                )
                return (
                  <div
                    className={classNames(
                      'range-slider-label',
                      {
                        clickable: clickableLabels,
                      },
                      typeof label === 'object' && 'className' in label && label.className,
                    )}
                    style={labelStyle}
                    onMouseDown={(event) => handleLabelClick(event, labelValue)}
                    key={index}
                    ref={(el: HTMLDivElement) => {
                      labelsRef.current[index] = el
                    }}
                  >
                    {typeof label === 'object' && 'label' in label ? label.label : label}
                  </div>
                )
              })}
          </div>
        )}
      </div>
    )
  },
)

CRangeSlider.propTypes = {
  clickableLabels: PropTypes.bool,
  disabled: PropTypes.bool,
  distance: PropTypes.number,
  labels: PropTypes.any,
  max: PropTypes.number,
  min: PropTypes.number,
  name: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
  step: PropTypes.number,
  tooltips: PropTypes.bool,
  tooltipsFormat: PropTypes.func,
  track: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf<'fill'>(['fill'])]),
  value: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
  vertical: PropTypes.bool,
}

CRangeSlider.displayName = 'CRangeSlider'
