import './range.scss';

import type { ComponentProps, KeyboardEvent, ReactNode } from 'react';
import { composeListeners, OrProduceWith } from '@assets/js-shop/helpers';
import { translate as _ } from '@assets/js-shop/ShopUtils';

import * as React from 'react';
import * as Slider from '@radix-ui/react-slider';

export type Range = {
   from: number,
   to: number,
};

export type OptRange = Range | undefined;

type SliderProps = Omit<
   ComponentProps<typeof Slider.Root>,
   'className' | 'min' | 'max' | 'value' | 'onValueChange' | 'step'
>;

const STICKY_THRESHOLD_PERCENTAGE = 0.01;

export const RANGE_DEFAULT_MIN = 0;
export const RANGE_DEFAULT_MAX = 0;

export type RangeFilterProps = {
   config: Range,
   label?: OrProduceWith<ReactNode, Range>,

   range: OptRange,
   setRange: (range: OrProduceWith<OptRange>) => void,

   stickyThreshold?: number,
   sliderProps?: SliderProps,

   renderInputs?: false;
   renderInputValue?: (value: number | '') => string;
};

type UseRangeParams = Pick<
   RangeFilterProps,
   'range' | 'setRange' | 'config' | 'stickyThreshold'
>

function useRange(params: UseRangeParams) {
   const {
      range: rangeRaw,
      setRange: setRangeRaw,
      config,
      stickyThreshold = STICKY_THRESHOLD_PERCENTAGE,
   } = params;

   // handle price range min, max updates (on config change)
   // Je to potřeba?
   // React.useEffect(() => {
   //    if (!rangeRaw) {
   //       return;
   //    }
   //
   //    let { from, to } = rangeRaw;
   //
   //    if (from <= config.from) {
   //       from = config.from;
   //    }
   //
   //    if (to >= config.to) {
   //       to = config.to;
   //    }
   //
   //    if (from !== rangeRaw.from || to !== rangeRaw.to) {
   //       setRangeRaw({ from, to });
   //    }
   // }, [config]);

   const setRange = React.useCallback((newRange: Range) => {
      let { from: newMin, to: newMax } = newRange;

      const thresholdValue = (config.to - config.from) * stickyThreshold;

      if (newMin <= (config.from + thresholdValue)) {
         newMin = config.from;
      }

      if (newMax >= (config.to - thresholdValue)) {
         newMax = config.to;
      }

      const newPriceRange = {
         from: newMin,
         to: newMax,
      };

      if (newPriceRange.from === config.from && newPriceRange.to === config.to) {
         setRangeRaw(undefined);

         return;
      }

      setRangeRaw(newPriceRange);
   }, [config, setRangeRaw, stickyThreshold]);

   return React.useMemo(() => {
      const rangeWidth = config.to - config.from;
      let step: number;

      if (rangeWidth < 100) {
         step = 1;
      } else if (rangeWidth < 1000) {
         step = 5;
      } else if (rangeWidth < 3500) {
         step = 10;
      } else {
         step = 100;
      }

      const range = { ...config, ...(rangeRaw ?? {}) };
      const resultConfig = { ...config };

      range.from = Math.floor((range.from ?? RANGE_DEFAULT_MIN) / step) * step;
      resultConfig.from = Math.floor((config.from ?? RANGE_DEFAULT_MIN) / step) * step;

      range.to ??= RANGE_DEFAULT_MAX;
      resultConfig.to ??= RANGE_DEFAULT_MAX;

      return {
         config: resultConfig,
         range,
         setRange,
         rangeRaw,
         setRangeRaw,
         step,
      };
   }, [config, rangeRaw, setRange, setRangeRaw]);
}

export type CommitEvent = React.FocusEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>;

type NumberInputProps = {
   range: Range,
   setRange: (range: OrProduceWith<OptRange>, ev: CommitEvent) => void;
   config: Range,
   renderValue?: RangeFilterProps['renderInputValue'],
   disabled: boolean,
};

function NumberInputs(props: NumberInputProps) {
   const { config, setRange, range, renderValue, disabled } = props;
   const [focused, setFocused] = React.useState<'from' | 'to' | undefined>();
   const [value, setValue] = React.useState<{ from: number | '', to: number | '' }>({ ...config });

   // Handle change from above (from slider, config change, ...)
   React.useEffect(() => setValue({ ...value, ...(range ?? {}) }), [range]);

   const handleChange = React.useCallback((ev: React.ChangeEvent<HTMLInputElement>) => {
      const inputValue = ev.target.value.trim();
      // Only valid numbers or an empty string (with the + op. coalesces to 0)
      if (Number.isNaN(+inputValue)) {
         return;
      }

      const key = ev.target.dataset.key as keyof typeof value;

      setValue(prev => ({ ...prev, [key]: inputValue === '' ? '' : +inputValue }));
   }, [setValue]);

   const commitValue = React.useCallback((ev: CommitEvent) => {
      if (ev.type === 'keydown' && (ev as KeyboardEvent).key !== 'Enter') {
         return;
      }

      const maybeValue = ev.currentTarget.value.trim();
      if (maybeValue === '' || Number.isNaN(+maybeValue)) {
         return;
      }

      const value = +maybeValue;
      const key = ev.currentTarget.dataset.key as 'from' | 'to';

      const newRange = { ...range, [key]: value };
      if (newRange.from === config.from && newRange.to === config.to) {
         setRange(undefined, ev);

         return;
      }

      setRange(newRange, ev);
   }, [range, setRange, config]);

   function handleChangeFocus(ev: React.FocusEvent<HTMLInputElement>) {
      if (ev.type === 'blur') {
         setFocused(undefined);

         return;
      }

      setFocused(ev.currentTarget.dataset.key as 'from' | 'to');
   }

   return (
      <div className="slider-range-inputs">
         <input
            className="form-control"
            type={focused === 'from' ? 'number' : 'text'}
            min={config.from}
            max={config.to}
            value={(!renderValue || focused === 'from')
               ? value.from
               : renderValue(value.from)
            }
            onChange={handleChange}
            onBlur={composeListeners(commitValue, handleChangeFocus)}
            onFocus={handleChangeFocus}
            onKeyDown={commitValue}
            data-key="from"
            placeholder={_('hodnotaOd')}
            disabled={disabled}
         />
         <span>-</span>
         <input
            className="form-control"
            type={focused === 'to' ? 'number' : 'text'}
            min={config.from}
            max={config.to}
            value={(!renderValue || focused === 'to')
               ? value.to
               : renderValue(value.to)
            }
            onChange={handleChange}
            onBlur={composeListeners(commitValue, handleChangeFocus)}
            onFocus={handleChangeFocus}
            onKeyDown={commitValue}
            data-key="to"
            placeholder={_('hodnotaDo')}
            disabled={disabled}
         />
      </div>
   );
}

function numberSort(a: number, b: number) {
   return a - b;
}

export function RangeFilter(props: RangeFilterProps) {
   const {
      label,
      renderInputs = true,
      sliderProps = {},
      renderInputValue,
   } = props;

   const { range, rangeRaw, setRange, setRangeRaw, step, config } = useRange(props);
   const disabled = config.from == config.to;
   const effectiveTo = disabled ? config.from + 0.1 : config.to;
   const [immediateValue, setImmediateValue] = React.useState({...(range ?? config)});

   const commitRange = React.useCallback((newValue: number[]) => {
      const value = newValue.slice().sort(numberSort);

      setRange({ from: value[0], to: value[1] });
   }, [setRange]);

   // Sync immediate value if range is updated from somewhere else than the slider
   React.useEffect(() => setImmediateValue({...config, ...(rangeRaw ?? {})}), [rangeRaw, config]);

   return (
      <div className="filter-item-values">
         <label className="slider-range-label">
            {typeof label === 'function' ? label(immediateValue) : (label ?? null)}
            <Slider.Root
               className="slider-root"
               minStepsBetweenThumbs={0.1}
               min={config.from}
               max={effectiveTo}
               value={[immediateValue.from, disabled ? effectiveTo : immediateValue.to]}
               onValueChange={value => {
                  const sorted = value.slice().sort(numberSort);

                  setImmediateValue({
                     from: sorted[0],
                     to: sorted[1],
                  });
               }}
               onValueCommit={commitRange}
               step={step}
               disabled={disabled}
               {...sliderProps}
            >
               <Slider.Track className="slider-track">
                  <Slider.Range className="slider-range"/>
               </Slider.Track>
               <Slider.Thumb key="frst" className="slider-thumb"/>
               <Slider.Thumb key="scnd" className="slider-thumb"/>
            </Slider.Root>
         </label>

         {renderInputs && (
            <NumberInputs
               setRange={setRangeRaw}
               config={config}
               range={immediateValue}
               renderValue={renderInputValue}
               disabled={disabled}
            />
         )}
      </div>
   );
}
