import React, { ComponentProps, forwardRef, useEffect, useMemo, useRef, useState } from 'react';

import {
  buildBlocks,
  formatFromMs,
  getBlockIndexFromPos,
  joinMs,
  PAD,
  parseBlock,
  parseTemplateOrMs,
  putBlock,
  safetyParseValue,
} from './helpers';
import { getNumberInRange } from '@/modules/@common/helpers/number';

import { TextField } from '@/mui/components';

type Props = ComponentProps<typeof TextField> & {
  value: number | string;
  wrap?: boolean;
  hh?: boolean;
  ms?: boolean;
  onChange: (ms: number) => void;
};

const isMac = navigator.platform.toUpperCase().includes('MAC');

const DurationField = forwardRef<HTMLInputElement, Props>(
  ({ value, onChange, wrap = false, hh = true, ms = true, disabled, ...props }, ref) => {
    const [BLOCKS_CUR, SEP_POS, TEMPLATE, durationTemplate] = useMemo(() => {
      const blocks = buildBlocks({ hh, ms });

      return [
        blocks,
        blocks.map(({ end }) => end),
        blocks.map(({ len }) => '0'.repeat(len)).join(':'),
        blocks
          .map((b) => {
            if (b.key === 'H') {
              return 'HH';
            }

            if (b.key === 'm') {
              return 'mm';
            }

            if (b.key === 's') {
              return 'ss';
            }

            return 'MSS';
          })
          .join(':'),
      ];
    }, [hh, ms]);

    const innerRef = useRef<HTMLInputElement>(null);
    const selectingRef = useRef(false);
    const typedCountRef = useRef(0);
    const lastBlockRef = useRef(0);
    const reselectId = useRef<number | null>(null);

    const [text, setText] = useState<string>(() => formatFromMs(safetyParseValue(value), BLOCKS_CUR));
    const [activeBlock, setActiveBlock] = useState<number>(0);
    const [isComposing, setIsComposing] = useState(false);

    const selectBlock = (idx: number) => {
      const input = innerRef.current;
      const block = BLOCKS_CUR[idx];

      if (input && block) {
        if (reselectId.current != null) {
          window.cancelAnimationFrame(reselectId.current);
          reselectId.current = null;
        }

        selectingRef.current = true;
        window.queueMicrotask(() => {
          window.requestAnimationFrame(() => {
            reselectId.current = window.requestAnimationFrame(() => {
              input.setSelectionRange(block.start, block.end);
              setTimeout(() => {
                selectingRef.current = false;
              }, 0);
            });
          });
        });
      }
    };

    const resetTypedCount = (idx = activeBlock) => {
      typedCountRef.current = 0;
      lastBlockRef.current = idx;
    };

    const commitFromText = (t: string) => {
      let h = 0;
      let mm = 0;
      let ss = 0;
      let mss = 0;

      // eslint-disable-next-line no-restricted-syntax
      for (const b of BLOCKS_CUR) {
        const val = parseBlock(t, b.start, b.end);

        if (b.key === 'H') {
          h = val;
        } else if (b.key === 'm') {
          mm = val;
        } else if (b.key === 's') {
          ss = val;
        } else if (b.key === 'M') {
          mss = val;
        }
      }
      onChange(joinMs(h, mm, ss, mss));
    };

    useEffect(() => {
      setText(formatFromMs(safetyParseValue(value), BLOCKS_CUR));
    }, [value, hh, ms]);

    useEffect(() => {
      if (props.autoFocus) {
        innerRef.current?.focus();
        selectBlock(activeBlock);
      }
    }, [props.autoFocus]);

    const setBlockNumLocal = (idx: number, num: number) => {
      const block = BLOCKS_CUR[idx];
      const next = getNumberInRange(num, block.min, block.max);
      const digits = PAD(next, block.len);
      const nextText = putBlock(text, block.start, block.end, digits);

      setText(nextText);

      return nextText;
    };

    const increment = (idx: number, delta: number) => {
      const block = BLOCKS_CUR[idx];
      const curr = parseBlock(text, block.start, block.end);
      let next = curr + delta;

      if (wrap) {
        const range = block.max - block.min + 1;
        next = ((((next - block.min) % range) + range) % range) + block.min;
      } else {
        next = getNumberInRange(next, block.min, block.max);
      }

      const nt = setBlockNumLocal(idx, next);

      commitFromText(nt);
    };

    const handleKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
      if (disabled || isComposing) return;
      const idx = activeBlock;
      const isCtrl = isMac ? e.metaKey : e.ctrlKey;

      if (e.key === 'Enter' || isCtrl) {
        return;
      }

      if (e.key === 'Tab') {
        const atFirst = idx === 0;
        const atLast = idx === BLOCKS_CUR.length - 1;
        if (e.shiftKey) {
          if (!atFirst) {
            e.preventDefault();
            const n = idx - 1;
            setActiveBlock(n);
            resetTypedCount(n);
            selectBlock(n);
          }
        } else if (!atLast) {
          e.preventDefault();
          const n = idx + 1;
          setActiveBlock(n);
          resetTypedCount(n);
          selectBlock(n);
        }
        return;
      }

      if (e.key === 'ArrowLeft') {
        e.preventDefault();
        const n = Math.max(0, idx - 1);
        setActiveBlock(n);
        resetTypedCount(n);
        selectBlock(n);
        return;
      }
      if (e.key === 'ArrowRight') {
        e.preventDefault();
        const n = Math.min(BLOCKS_CUR.length - 1, idx + 1);
        setActiveBlock(n);
        resetTypedCount(n);
        selectBlock(n);
        return;
      }
      if (e.key === 'Home') {
        e.preventDefault();
        setActiveBlock(0);
        resetTypedCount(0);
        selectBlock(0);
        return;
      }
      if (e.key === 'End') {
        e.preventDefault();
        const last = BLOCKS_CUR.length - 1;
        setActiveBlock(last);
        resetTypedCount(last);
        selectBlock(last);
        return;
      }
      if (e.key === 'ArrowUp') {
        e.preventDefault();
        resetTypedCount(idx);
        increment(idx, +1);
        selectBlock(idx);
        return;
      }
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        resetTypedCount(idx);
        increment(idx, -1);
        selectBlock(idx);
        return;
      }

      if (/^[0-9]$/.test(e.key)) {
        e.preventDefault();

        if (idx !== lastBlockRef.current) {
          resetTypedCount(idx);
        }

        const block = BLOCKS_CUR[idx];
        const baseStr = typedCountRef.current === 0 ? '0'.repeat(block.len) : text.slice(block.start, block.end);
        const shifted = (baseStr + e.key).slice(-block.len);
        const num = parseInt(shifted, 10) || 0;

        const nt = setBlockNumLocal(idx, num);

        typedCountRef.current += 1;
        const filled = typedCountRef.current >= block.len;
        const atLast = idx === BLOCKS_CUR.length - 1;

        if (filled) {
          commitFromText(nt);

          if (!atLast) {
            const nextIdx = idx + 1;
            setActiveBlock(nextIdx);
            resetTypedCount(nextIdx);
            selectBlock(nextIdx);
          } else {
            selectBlock(idx);
            resetTypedCount(idx);
          }
        } else {
          selectBlock(idx);
        }

        return;
      }

      if (e.key === 'Backspace' || e.key === 'Delete') {
        e.preventDefault();
        resetTypedCount(idx);
        const nt = setBlockNumLocal(idx, 0);
        commitFromText(nt);
        selectBlock(idx);
        return;
      }

      if (e.key.length === 1) {
        e.preventDefault();
      }
    };

    const handleSelect: React.ReactEventHandler<HTMLInputElement> = () => {
      const input = innerRef.current;
      if (!input || selectingRef.current) return;

      const pos = input.selectionStart ?? 0;
      const idx = getBlockIndexFromPos(pos, SEP_POS);

      if (idx !== activeBlock) {
        setActiveBlock(idx);
        resetTypedCount(idx);
        selectBlock(idx);
      }
    };

    const handlePaste: React.ClipboardEventHandler<HTMLInputElement> = (e) => {
      e.preventDefault();
      const data = e.clipboardData.getData('text').trim();
      const parsed = parseTemplateOrMs(data, BLOCKS_CUR);

      if (parsed !== null) {
        const next = formatFromMs(parsed, BLOCKS_CUR);

        setText(next);
        commitFromText(next);
        selectBlock(activeBlock);
      }
    };

    return (
      <TextField
        {...props}
        dir="ltr"
        value={text}
        aria-label={`Duration (${durationTemplate})`}
        disabled={disabled}
        onChange={() => {}}
        onKeyDown={handleKeyDown}
        onKeyUp={(e) => {
          if (disabled || isComposing) return;
          if (
            /^[0-9]$/.test(e.key) ||
            e.key.startsWith('Arrow') ||
            e.key === 'Home' ||
            e.key === 'End' ||
            e.key === 'Tab'
          ) {
            selectBlock(activeBlock);
          }
        }}
        onMouseDown={() => {
          window.requestAnimationFrame(() => {
            if (innerRef.current) {
              const pos = innerRef.current.selectionStart ?? 0;
              const idx = getBlockIndexFromPos(pos, SEP_POS);

              setActiveBlock(idx);
              resetTypedCount(idx);
              selectBlock(idx);
            }
          });
        }}
        onFocus={(event) => {
          selectBlock(activeBlock);
          props.onFocus?.(event);
        }}
        onSelect={handleSelect}
        onPaste={handlePaste}
        onBlur={(event) => {
          commitFromText(text);
          props.onBlur?.(event);
        }}
        onCompositionStart={() => setIsComposing(true)}
        onCompositionEnd={() => setIsComposing(false)}
        inputRef={(node) => {
          if (typeof ref === 'function') {
            ref(node);
          } else if (ref && 'current' in ref) {
            ref.current = node;
          }

          innerRef.current = node;
        }}
        inputProps={{
          ...(props.inputProps || {}),
          readOnly: true,
          inputMode: 'numeric',
          maxLength: TEMPLATE.length,
        }}
        placeholder={TEMPLATE}
        autoComplete="off"
        spellCheck={false}
      />
    );
  },
);

export default DurationField;
