import React, {
  CSSProperties,
  forwardRef,
  KeyboardEvent,
  useEffect,
  useMemo,
  useRef,
  useState,
  useTransition,
} from 'react';
import classNames from 'clsx';
import Prism from 'prismjs';
import { CodeRounded } from '@mui/icons-material';

import './helpers/prismJson';
import { useDebouncedCallback } from './helpers';
import { useCombinedRefs } from '@/mui/helpers';

import { Button, FormHelperText, Tooltip } from '@/mui/components';

import styles from './JSONEditor.module.scss';

type UndoState = {
  value: string;
  selectionStart: number;
  selectionEnd: number;
};

type Props = {
  value?: string;
  disabled?: boolean;
  error?: boolean;
  minRows?: number;
  maxRows?: number;
  fieldName?: string;
  helperText?: string | React.ReactNode;
  onChange: (value: string) => void;
  onFocus?: (event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement, Element>) => void;
};

const MAX_UNDO = 40;
const TAB_SIZE = 2;
const fontSize = 14;
const lineHeight = 1.5;

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

const JSONEditor = forwardRef(
  (
    {
      value = '',
      disabled = false,
      fieldName = '',
      helperText = '',
      error,
      minRows = 5,
      maxRows,
      onChange,
      onFocus,
    }: Props,
    ref,
  ) => {
    const wrapperRef = useRef<HTMLDivElement | null>(null);
    const wrapperScroll = useRef<number>(0);
    const textareaRef = useRef<HTMLTextAreaElement | null>(null);
    const undoStack = useRef<UndoState[]>([]);

    const [innerValue, setInnerValue] = useState<string>(value || '');
    const rows = useMemo(() => innerValue.split('\n').map((_, i) => i + 1), [innerValue]);
    const [highlightedCode, setHighlightedCode] = useState(() =>
      Prism.highlight(innerValue, Prism.languages.json, 'json'),
    );
    const [, startTransition] = useTransition();

    const setUndoStack = ({ value: value$, selectionStart, selectionEnd }: UndoState) => {
      if (undoStack.current.length >= MAX_UNDO) {
        undoStack.current.shift();
      }
      undoStack.current.push({ value: value$, selectionStart, selectionEnd });
    };

    const setCaretPosition = (start: number, end: number) => {
      const textarea$ = textareaRef.current;

      if (!textarea$) {
        return;
      }
      textarea$.selectionStart = start;
      textarea$.selectionEnd = end;
    };

    const handleScrollToCaret = () => {
      const textarea$ = textareaRef.current;
      const wrapper$ = wrapperRef.current;

      if (!textarea$ || !wrapper$) return;

      const { selectionStart } = textarea$;
      const lines = innerValue.slice(0, selectionStart).split('\n');
      const lineIndex = lines.length - 1;

      const lineHeightPx = fontSize * lineHeight;
      const caretY = lineIndex * lineHeightPx;

      if (caretY < wrapper$.scrollTop) {
        wrapper$.scrollTop = caretY;
      } else if (caretY > wrapper$.scrollTop + wrapper$.clientHeight - lineHeightPx) {
        wrapper$.scrollTop = caretY - wrapper$.clientHeight + lineHeightPx;
      }
    };

    useEffect(() => setInnerValue(value || ''), [value]);

    const debouncedOnChange = useDebouncedCallback((val: string) => {
      if (onChange) {
        onChange(val);
      }
    }, 300);

    useEffect(() => {
      startTransition(() => setHighlightedCode(Prism.highlight(innerValue, Prism.languages.json, 'json')));

      debouncedOnChange(innerValue);
    }, [innerValue]);

    useEffect(() => {
      window.requestAnimationFrame(handleScrollToCaret);
    }, [innerValue]);

    const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
      const textarea$ = e.currentTarget;
      const { selectionStart, selectionEnd, value: value$ } = textarea$;

      const isCtrl = isMac ? e.metaKey : e.ctrlKey;

      if (isCtrl && e.key === 'c') {
        return;
      }

      if (isCtrl && e.key === 'z') {
        e.preventDefault();
        if (undoStack.current.length > 0) {
          const previousState = { ...undoStack.current.pop()! };

          setInnerValue(previousState.value);

          window.requestAnimationFrame(() => {
            setCaretPosition(previousState.selectionStart, previousState.selectionEnd);
          });
        }
        return;
      }

      if (
        !e.ctrlKey &&
        !e.metaKey &&
        !(e.key === 'Backspace' && selectionStart <= 0 && selectionStart === selectionEnd)
      ) {
        setUndoStack({ value: innerValue, selectionStart, selectionEnd });
      }

      if (isCtrl && e.key === 'ArrowLeft') {
        const lineStart = value$.lastIndexOf('\n', selectionStart - 1) + 1;
        const symbolPos = value$.slice(lineStart, selectionStart).match(/^ +/)?.[0]?.length || 0;

        const absoluteCursorPos = lineStart + symbolPos;

        window.requestAnimationFrame(() => {
          setCaretPosition(absoluteCursorPos, e.shiftKey ? selectionEnd : absoluteCursorPos);
        });
      }

      if (e.key === 'Tab') {
        e.preventDefault();

        const before = value$.slice(0, selectionStart);
        const after = value$.slice(selectionEnd);
        const tabInsert = ' '.repeat(TAB_SIZE);

        setInnerValue(before + tabInsert + after);

        window.requestAnimationFrame(() => {
          setCaretPosition(selectionStart + tabInsert.length, selectionStart + tabInsert.length);
        });
      }

      if (e.key === 'Backspace') {
        e.preventDefault();

        const before = value$.slice(0, selectionStart);
        const after = value$.slice(selectionEnd);
        let newValue;
        let newSelectionStart = selectionStart;

        if (selectionStart !== selectionEnd) {
          newValue = before + after;
        } else if (selectionStart > 0) {
          const lastNewline = before.lastIndexOf('\n');
          const beforeLine = before.slice(lastNewline + 1);

          if (/^\s*$/.test(beforeLine)) {
            newValue = before.slice(0, lastNewline) + after;
            newSelectionStart = lastNewline > 0 ? lastNewline : 0;
          } else if (before.endsWith('  ')) {
            newValue = before.slice(0, -2) + after;
            newSelectionStart -= 2;
          } else {
            const prevChar = before.slice(-1);
            const nextChar = after.slice(0, 1);

            const pairMap: Record<string, string> = {
              '{': '}',
              '[': ']',
            };

            if (pairMap[prevChar] === nextChar) {
              newValue = before.slice(0, -1) + after.slice(1);
              newSelectionStart -= 1;
            } else {
              newValue = before.slice(0, -1) + after;
              newSelectionStart -= 1;
            }
          }
        } else {
          return;
        }

        if (/^\s*$/.test(newValue)) {
          newValue = '';
          newSelectionStart = 0;
        }

        setInnerValue(newValue);

        window.requestAnimationFrame(() => {
          setCaretPosition(newSelectionStart, newSelectionStart);
        });
      }

      if (e.key === 'Enter') {
        e.preventDefault();

        const before = value$.slice(0, selectionStart);
        const after = value$.slice(selectionEnd);
        const lineStart = before.lastIndexOf('\n') + 1;
        const leadingSpaces = before.slice(lineStart).match(/^\s*/)?.[0] || '';
        const prevChar = before.replace(/ +$/, '');
        const nextChar = after.replace(/^ +/, '');

        let newLine = `\n${leadingSpaces}`;
        let caretPos = 0;

        if (prevChar.endsWith('{') || prevChar.endsWith('[')) {
          if (before.split('\n').pop()?.trim()) {
            newLine += ' '.repeat(TAB_SIZE);
          }

          if (nextChar.startsWith('}') || nextChar.startsWith(']')) {
            caretPos = newLine.length;
            newLine += `\n${leadingSpaces}`;
          } else {
            caretPos = newLine.length;
          }
        } else {
          caretPos = newLine.length;
        }

        setInnerValue(before + newLine + after);

        window.requestAnimationFrame(() => {
          setCaretPosition(selectionStart + caretPos, selectionStart + caretPos);
        });
      }

      if (['{', '['].includes(e.key)) {
        e.preventDefault();

        const closingChar = e.key === '{' ? '}' : ']';

        if (selectionStart !== selectionEnd) {
          const selectedText = value$.slice(selectionStart, selectionEnd);

          setInnerValue(
            value$.slice(0, selectionStart) + e.key + selectedText + closingChar + value$.slice(selectionEnd),
          );

          window.requestAnimationFrame(() => {
            setCaretPosition(selectionStart + 1, selectionEnd + 1 + selectedText.length);
          });
        } else {
          setInnerValue(value$.slice(0, selectionStart) + e.key + closingChar + value$.slice(selectionEnd));

          window.requestAnimationFrame(() => {
            setCaretPosition(selectionStart + 1, selectionEnd + 1);
          });
        }
      }

      if (e.key === '"') {
        e.preventDefault();

        const before = value$.slice(0, selectionStart);
        const after = value$.slice(selectionEnd);
        const selectedText = value$.slice(selectionStart, selectionEnd);
        const prevChar = before.slice(-1);
        const nextChar = after.slice(0, 1);

        let newValue;
        if (selectionStart !== selectionEnd) {
          newValue = `${before}"${selectedText}"${after}`;
        } else if (prevChar !== '"' && nextChar !== '"') {
          newValue = `${before}""${after}`;
        } else {
          newValue = `${before}"${after}`;
        }

        setInnerValue(newValue);

        window.requestAnimationFrame(() => {
          setCaretPosition(selectionStart + 1, selectionStart + 1);
        });
      }
    };

    let isPasteEvent = false;

    const combinedRefs = useCombinedRefs(ref, textareaRef);

    return (
      <div className={styles.Wrapper}>
        <div
          className={classNames(styles.InnerWrapper, {
            [styles.InnerWrapper___error]: error,
          })}
          style={
            {
              '--internal-line-count': Math.max(minRows ?? 0, rows.length),
              '--internal-min-line-count': minRows,
              '--internal-max-line-count': maxRows,
              '--internal-font-size': `${fontSize}px`,
              '--internal-line-height': lineHeight,
            } as CSSProperties
          }
        >
          <div ref={wrapperRef} className={styles.ContentWrapper}>
            <div className={styles.Content}>
              <div className={styles.Lines}>{rows.join('\n')}</div>
              <div className={styles.CodeEditor}>
                <textarea
                  ref={combinedRefs}
                  name={fieldName}
                  spellCheck="false"
                  value={innerValue}
                  disabled={disabled}
                  rows={Math.max(minRows ?? 0, rows.length)}
                  className={styles.Code}
                  onFocus={onFocus}
                  onKeyDown={(event) => {
                    if (wrapperRef.current) {
                      wrapperScroll.current = wrapperRef.current.scrollTop;
                    }

                    handleKeyDown(event);
                  }}
                  onPaste={() => {
                    isPasteEvent = true;
                    const textarea$ = textareaRef.current;
                    const wrapper$ = wrapperRef.current;
                    if (!textarea$ || !wrapper$) return;

                    const { selectionStart, selectionEnd } = textarea$;

                    wrapperScroll.current = wrapper$.scrollTop;

                    setUndoStack({ value: innerValue, selectionStart, selectionEnd });
                  }}
                  onChange={(e) => {
                    let result = e.target.value;

                    const lastAction = undoStack.current[undoStack.current.length - 1];

                    if (isPasteEvent) {
                      try {
                        result = JSON.stringify(JSON.parse(result), null, TAB_SIZE);

                        window.requestAnimationFrame(() => {
                          setCaretPosition(lastAction.selectionStart, lastAction.selectionStart);
                        });
                      } catch {
                        /* empty */
                      }
                    }

                    setInnerValue(result);

                    window.requestAnimationFrame(() => {
                      if (wrapperRef.current) {
                        wrapperRef.current.scrollTop = wrapperScroll.current;
                      }
                    });
                  }}
                  onBlur={(e) => {
                    let result = e.target.value;

                    try {
                      result = JSON.stringify(JSON.parse(result), null, TAB_SIZE);
                    } catch {
                      /* empty */
                    }

                    setInnerValue(result);
                  }}
                />
                <div
                  className={classNames(styles.Code, styles.Code___syntax)}
                  dangerouslySetInnerHTML={{
                    __html: highlightedCode,
                  }}
                />
              </div>
              <div className={styles.FormattingWrapper}>
                <Tooltip title="Prettify JSON">
                  <Button
                    startIcon={<CodeRounded />}
                    square
                    className={styles.Formatting}
                    size="xSmall"
                    disabled={disabled}
                    onClick={() => {
                      try {
                        const formatted = JSON.stringify(JSON.parse(innerValue), null, TAB_SIZE);

                        setInnerValue(formatted);
                      } catch {
                        /* empty */
                      }
                    }}
                  />
                </Tooltip>
              </div>
            </div>
          </div>
        </div>
        {helperText && <FormHelperText error={error}>{helperText}</FormHelperText>}
      </div>
    );
  },
);

export default JSONEditor;
