/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { debounce, uniqueId } from 'lodash';
import { color, fontWeight } from 'styled-system';
import {
  collectSegmentsFromNewLine,
  collectSegmentStyles,
  removeNoBreakingSpace,
  removeNoBreakingSpaceFromArray,
  localStyle,
  applyTopActionStyles,
  emptyLine,
} from 'utils/richText';
import { useTheme } from 'hooks';
import { COLOR_DARK, COLOR_LIGHT } from 'theme';
import {
  Maybe,
  RichTextContentSegment,
  RichTextContentSegmentString,
  RichTextContentSegmentTypeEnum,
  RichTextContentStringColorEnum,
  RichTextContentStringStyle,
} from 'expo-schema';
import { metrics } from 'config';
import { Visible } from 'components/visible';

const { INPUT_DEBOUNCE } = metrics.RICH_TEXT_EDITOR;

const RichText = styled('span')(color, fontWeight, {
  fontFamily: 'CircularStd-Book',
});
const ContentEditableDiv = styled.div`
  &:empty:before {
    content: attr(placeholder);
    pointer-events: none;
    display: block; /* For Firefox */
  }
`;

export enum TagNameEnum {
  div = 'div',
  span = 'span',
  br = 'br',
}
const enum LineBreak {
  br = '<br>',
  space = ' ',
}

export const Content: React.FC<RichTextContentProps> = ({
  richTextContent,
  placeholder,
  handleSubmit,
  setRichTextContent,
  segmentStyle,
}) => {
  const [cursorPosition, setCursorPosition] = useState(0);
  const [localStrings, setLocalStrings] = useState<RichTextContentSegmentString[]>([]);
  const richRef = useRef() as MutableRefObject<HTMLDivElement>;

  const { activeThemeMode } = useTheme();

  const textColor = activeThemeMode === 'dark' ? COLOR_DARK.title : COLOR_LIGHT.title;
  const { strings, id: segmentId } = richTextContent;
  const { isBold, isCode, isItalic, isStrikeThrough, isUnderLine } = segmentStyle || {
    isBold: false,
    isCode: false,
    isItalic: false,
    isStrikeThrough: false,
    isUnderLine: false,
  };
  const isAnyStyleActive = isBold || isItalic || isStrikeThrough || isUnderLine;
  const handleSelectedText = useCallback(
    ({ start, end }: SelectedTextProps) => {
      const startId = start.getAttribute('id');
      const endId = end.getAttribute('id');
      const isInputContent = startId && endId; // when draft is loaded into input but user have not edited message
      const isJustDraft = isInputContent && localStrings.length === 0;
      const localStringsTemp = isJustDraft ? [...strings] : [...localStrings];

      const startIndex = localStringsTemp.findIndex((string) => string.id === startId);
      const endIndex = localStringsTemp.findIndex((string) => string.id === endId);
      const slicedSegments = localStringsTemp.slice(startIndex, endIndex + 1);

      const splicedIds = slicedSegments.map((segment) => segment.id);

      const styledLocalStringsTemp = localStringsTemp.map((localString) => {
        if (splicedIds.includes(localString.id)) {
          const reformedStringStyle = applyTopActionStyles({
            string: localString,
            isBold,
            isCode,
            isItalic,
            isStrikeThrough,
            isUnderLine,
          });
          return { ...localString, style: reformedStringStyle };
        } else {
          return localString;
        }
      });
      const existingContent = isJustDraft
        ? JSON.stringify([...strings])
        : JSON.stringify([...localStrings]);
      if (existingContent !== JSON.stringify(styledLocalStringsTemp)) {
        setLocalStrings(styledLocalStringsTemp);
      }
    },
    [isBold, isCode, isItalic, isStrikeThrough, isUnderLine, localStrings, strings]
  );
  const handleSelection = useCallback(() => {
    const response = getSelectedElements();
    const isSelectionValid =
      response?.startElement && response?.endingElement && response.selection;
    if (isSelectionValid) {
      handleSelectedText({
        start: response?.startElement as HTMLElement,
        end: response?.endingElement as HTMLElement,
      });
    }
  }, [handleSelectedText]);

  useEffect(() => {
    handleSelection();
  }, [isBold, isItalic, isUnderLine, isStrikeThrough, handleSelection]);

  useEffect(() => {
    if (setRichTextContent) {
      setRichTextContent({
        order: 1,
        id: uniqueId(),
        type: RichTextContentSegmentTypeEnum.Paragraph,
        strings: localStrings,
        variant: 'input',
      });
    }
  }, [localStrings]);

  const getCaretAndLine = useCallback(() => {
    const editable = document.getElementById('contenteditable');
    if (editable) {
      // collapse selection to end
      window.getSelection()?.collapseToEnd();

      const selection = window.getSelection();
      const range = selection?.getRangeAt(0);

      // get anchor node if startContainer parent is editable
      const selectedNode =
        editable === range?.startContainer.parentNode
          ? selection?.anchorNode
          : range?.startContainer.parentNode;

      if (!selectedNode) {
        return {
          caret: 0,
          line: 0,
        };
      }

      // select to top of editable
      if (editable.firstChild) {
        range?.setStart(editable?.firstChild as Node, 0);
      }

      // do not use 'this' sel anymore since the selection has changed
      const content = window.getSelection()?.toString();
      const text = JSON.stringify(content);
      const lines = (text.match(/\\n/g) || []).length + 1;

      // clear selection
      window.getSelection()?.collapseToEnd();

      // minus 2 because of strange text formatting
      return {
        caret: text.length - 2,
        line: lines,
      };
    }
    return {
      caret: 0,
      line: 0,
    };
  }, []);

  const updateCursorPosition = useCallback(() => {
    const { caret } = getCaretAndLine();
    setCursorPosition(caret);
  }, [setCursorPosition]);

  const setCaret = useCallback(() => {
    const range = document.createRange();
    const selection = window.getSelection();
    let words: string[] = [];
    const richChildLength = richRef.current.childNodes.length;
    [...richRef.current.childNodes].every((child, index) => {
      const currentWords = child.textContent?.split('') as string[];
      const currentWordsCount = currentWords.length;
      words = [...words, ...currentWords];
      if (index === 0) {
        words.shift();
      }
      const isAtTheEnd = words.length < cursorPosition && richChildLength - 1 === index;
      if (cursorPosition <= words.length) {
        if (localStrings.length > 0) {
          const removePositions = words.length - cursorPosition;
          const childPosition = currentWordsCount - removePositions - 1; // 1 is here due to difference between index and length e.g index:0 === length:1
          if (richRef.current.childNodes[index].childNodes[0]) {
            range.setStart(richRef.current.childNodes[index].childNodes[0], childPosition);
          }
          return false;
        }
      } else if (isAtTheEnd) {
        if (richRef.current.childNodes[index].childNodes[0]) {
          range.setStart(richRef.current.childNodes[index].childNodes[0], currentWordsCount);
        }
      }
      return true;
    });
    range.collapse(true);
    selection?.removeAllRanges();
    selection?.addRange(range);
  }, [richRef, cursorPosition, localStrings]);

  useEffect(() => {
    if (!isAnyStyleActive) {
      setCaret();
    }
  }, [strings, richRef, isAnyStyleActive]);

  const emptyRichContentContainer = useCallback(() => {
    richRef.current.innerHTML = '';
  }, []);

  const addEmptyLine = useCallback(() => {
    const exampleSegment = localStrings[localStrings.length - 1];
    exampleSegment.value = '';
    exampleSegment.onNewLine = true;
    exampleSegment.style = localStyle;
    setLocalStrings([...localStrings, exampleSegment]);
  }, [localStrings]);

  const storeDivSegments = useCallback((segmentValues: string[]) => {
    const localStringsTemp: RichTextContentSegmentString[] = [];

    for (const segValue of segmentValues) {
      // Handling the line breaks within the span
      if (segValue.includes(LineBreak.br)) {
        const lineBreakSegments = segValue.split(LineBreak.br);
        const parsedLineBreakSegments: string[] = [];
        const lastIndex = lineBreakSegments.length - 1;
        lineBreakSegments.forEach((segment, index) => {
          if (segment) {
            parsedLineBreakSegments.push(segment);
            const isSplitDueToSpace = index !== lastIndex && lineBreakSegments[index + 1] === '';
            if (isSplitDueToSpace) {
              parsedLineBreakSegments.push('');
            }
          } else {
            parsedLineBreakSegments.push('');
          }
        });
        parsedLineBreakSegments.forEach((br) => {
          if (br) {
            const newItem: RichTextContentSegmentString = {
              value: ' ' + br.trim(),
              order: 1,
              id: uniqueId(),
              style: localStyle,
            };
            localStringsTemp.push(newItem);
          } else {
            localStringsTemp.push({ ...emptyLine, id: uniqueId() });
          }
        });
      } else {
        const newItem: RichTextContentSegmentString = {
          value: ' ' + segValue.trim(),
          id: uniqueId(),
          style: localStyle,
          order: 1,
        };
        localStringsTemp.push(newItem);
      }
    }

    // It Fix the duplication of segments by distinct on the base of id
    const key = 'id';
    const uniqueSegments = [...new Map(localStringsTemp.map((seg) => [seg[key], seg])).values()];
    return uniqueSegments;
  }, []);

  const addAndUpdateSegment = useCallback(
    (
      item: HTMLElement,
      segmentValues: string[],
      innerLocalStrings: RichTextContentSegmentString[]
    ) => {
      const itemId = item.getAttribute('id');
      const isStringCode = item.getAttribute('data-is-code') === 'true';
      let localStringsTemp: RichTextContentSegmentString[] = [];
      if (!isStringCode) {
        const segmentIndex = innerLocalStrings.findIndex((seg) => seg.id === itemId);
        const localStyleInner = collectSegmentStyles(item);

        if (segmentIndex < 0) {
          // When user will change value of style of an existing span
          localStringsTemp = innerLocalStrings?.map((segment) => {
            if (segment.id === itemId) {
              segment.value = ' ' + ` ${segmentValues[0]}`;
              segment.style = localStyleInner;
              return segment;
            } else {
              return segment;
            }
          });

          for (const segValue of segmentValues) {
            // Handling the line breaks within the span
            if (segValue.includes(LineBreak.br)) {
              const lineBreakSegments = segValue.split(LineBreak.br);

              for (const br of lineBreakSegments) {
                if (br) {
                  const newItem: RichTextContentSegmentString = {
                    ...innerLocalStrings[segmentIndex],
                    value: ' ' + br.trim(),
                    id: innerLocalStrings[segmentIndex]
                      ? innerLocalStrings[segmentIndex].id
                      : uniqueId(),
                    style: localStyle,
                  };
                  localStringsTemp.push(newItem);
                } else {
                  localStringsTemp.push({ ...emptyLine, id: uniqueId() });
                }
              }
            } else {
              const newItem: RichTextContentSegmentString = {
                ...innerLocalStrings[segmentIndex],
                value: ' ' + segValue.trim(),
                id: innerLocalStrings[segmentIndex]
                  ? innerLocalStrings[segmentIndex].id
                  : uniqueId(),
                style: localStyle,
              };
              localStringsTemp.push(newItem);
            }
          }
        }
      }
      // It Fix the duplication of segments by distinct on the base of id
      const key = 'id';
      const uniqueSegments = [...new Map(localStringsTemp.map((seg) => [seg[key], seg])).values()];
      return uniqueSegments;
    },
    []
  );

  const addPlainSegments = useCallback(
    (splitStrings: string[]) => {
      const localStringsTemp = [...localStrings];

      for (const stringValue of splitStrings) {
        // @ts-ignore
        const newItem: RichTextContentSegmentString = {
          id: uniqueId(),
          value: ` ${stringValue}`,
          style: localStyle,
        };
        localStringsTemp.push(newItem);
      }
      emptyRichContentContainer();
      setLocalStrings(localStringsTemp);
      return localStringsTemp;
    },
    [emptyRichContentContainer, localStrings]
  );

  const getSelectedElements = useCallback(() => {
    const currentSelection = window.getSelection();
    const selection = currentSelection?.toString();
    if (currentSelection && currentSelection.rangeCount > 0) {
      const startElement = currentSelection.getRangeAt(0).startContainer.parentNode;
      const endingElement = currentSelection.getRangeAt(0).endContainer.parentNode;
      return { selection, startElement, endingElement };
    } else {
      return null;
    }
  }, []);

  const parseRichTextInput = useCallback(
    ({
      children,
      newLines,
      oldSegmentIds,
    }: {
      children: Element[];
      newLines: Element[];
      oldSegmentIds: String[];
    }) => {
      let innerLocalStrings: RichTextContentSegmentString[] = localStrings;

      for (const item of children) {
        /**
         * This will process the old segments coming from server/local state
         */

        if (item.tagName.toLowerCase() === TagNameEnum.span) {
          /** we are checking either server/local state provider is same or user
           * has performed any of these actions
           * 1: update style
           * 2: edit/enter new text in the span
           * 3: remove text from span and we have &nbsp; left in the span
           */
          let spanSegments: string[] = item.innerHTML.trim().split(' ');
          /**Loop spanSegments and if found &nbsp as element then remove that element */
          // spanSegments = removeNoBreakingSpaceFromArray(spanSegments);
          /**Remove &nbsp before and after the element*/
          spanSegments = spanSegments.map((seg) => removeNoBreakingSpace(seg));
          if (spanSegments.length === 1) {
            /**
             * check if span contain 1 segment
             * span can contain(s) <br>, if so line break otherwise simply update text along with styles
             */
            const addedSegments = addAndUpdateSegment(
              item as HTMLElement,
              spanSegments,
              innerLocalStrings
            );
            if (addedSegments) {
              const key = 'id';
              innerLocalStrings = [...innerLocalStrings, ...addedSegments];
              innerLocalStrings = [
                ...new Map(innerLocalStrings.map((string) => [string[key], string])).values(),
              ] as unknown as RichTextContentSegmentString[];
            }
            // Write generic function to store segment to existing one / add new / remove existing
            /** user not enter 2nd work in the span now we can update that span value with style into local state segment */
          } else if (spanSegments.length > 1) {
            /**
             * check if span contain 2 || > 2 segments
             * simply update text along with styles
             */
            // converting to HtMLElement to support dataset property
            // const trimSpanSegment = spanSegments.slice(1, spanSegments.length);
            const splitAddedSegment = addAndUpdateSegment(
              item as HTMLElement,
              spanSegments,
              innerLocalStrings
            );

            if (splitAddedSegment) {
              const key = 'id';
              innerLocalStrings = [...innerLocalStrings, ...splitAddedSegment];
              innerLocalStrings = [
                ...new Map(innerLocalStrings.map((string) => [string[key], string])).values(),
              ] as unknown as RichTextContentSegmentString[];
            }

            /** user entered new words in the span now we need to add new segments with segment style into local state segment */
          }
          const newItemId = item.getAttribute('id');
          // Add segment ids to oldSegmentIds Array and compare with existing in local states to remove those who in local state but not here
          if (newItemId) {
            oldSegmentIds.push(newItemId);
          }
        } else if (item.tagName.toLowerCase() === TagNameEnum.div) {
          /**
           * Else block will process all new entered lines of text or empty spaces
           * each new line will be a div with span as child, span further will contain text or <br />(in case empty span of div)
           */
          const isChildren = item.children && item.children.length > 0;
          if (isChildren && item.children[0].tagName.toLowerCase() === TagNameEnum.span) {
            let spanSegments: string[] = item.children[0].innerHTML.trim().split(' ');
            /**Loop spanSegments and if found &nbsp as element then remove that element */
            spanSegments = removeNoBreakingSpaceFromArray(spanSegments);
            /**Remove &nbsp before and after the element*/
            spanSegments = spanSegments.map((seg) => removeNoBreakingSpace(seg));
            const newLineCollectedSegments = collectSegmentsFromNewLine(
              item.children[0] as HTMLElement,
              spanSegments,
              localStrings[localStrings.length - 1]
            );
            setLocalStrings([...localStrings, ...newLineCollectedSegments]);
          }
          newLines.push(item);
          // Iterate new lines to separate empty lines and text strings
          for (const newLine of newLines) {
            const newLineChild = newLine.children[0];
            const areNewLineChild = newLine.children.length > 0;
            // This will work when we have empty line
            if (areNewLineChild && newLine.children[0].innerHTML === '<br>') {
              // create new segment string with onNewLine property true
              addEmptyLine();
              // This will work when we have string in the line
            } else if (newLineChild.innerHTML.length > 0) {
              const newLineSpanSegments: string[] = newLineChild.innerHTML.trim().split(' ');
              const newLineAddedSegment = addAndUpdateSegment(
                item as HTMLElement,
                newLineSpanSegments,
                innerLocalStrings
              );

              if (newLineAddedSegment) {
                const key = 'id';
                innerLocalStrings = [...innerLocalStrings, ...newLineAddedSegment];
                innerLocalStrings = [
                  ...new Map(innerLocalStrings.map((string) => [string[key], string])).values(),
                ] as unknown as RichTextContentSegmentString[];
              }
            }
          }
        } else {
          if (item.tagName.toLowerCase() === TagNameEnum.br) {
            innerLocalStrings = [...innerLocalStrings, { ...emptyLine, id: uniqueId() }];
          } else {
            // TODO: Need to handle else, currently dot getting here!
          }
        }
      }
      setLocalStrings(innerLocalStrings);
      return innerLocalStrings;
    },
    [richRef, localStrings]
  );

  const handleInputChange = useCallback(() => {
    if (richRef?.current) {
      const oldSegmentIds: string[] = [];
      const newLines: Element[] = [];
      const richText = richRef.current;
      const isRichTextHasChildren = richText.children.length > 0;
      // const isRichTextHasOneChildren = richText.children.length === 1;

      if (isRichTextHasChildren) {
        const children = [...richText.children];

        const isEverythingInDev = children.every(
          (value) => value.tagName.toLowerCase() === TagNameEnum.br
        );
        if (isEverythingInDev) {
          // handel <br> combined with strings
          const cleanFromSpaces = removeNoBreakingSpace(richText.innerHTML);
          const splitInput = cleanFromSpaces.split(' ');
          const segments = storeDivSegments(splitInput);
          setLocalStrings(segments);
          return segments;
        } else {
          return parseRichTextInput({ children, newLines, oldSegmentIds });
        }
      } else {
        const innerText = richRef.current.innerText;
        if (innerText) {
          const innerTextSegments: string[] = innerText.split(' ');
          return addPlainSegments(innerTextSegments);
        } else {
          setLocalStrings([]);
          return [];
        }
      }
      // if (
      //   isRichTextHasOneChildren &&
      //   richRef.current.children[0].tagName.toLowerCase() === TagNameEnum.br
      // ) {
      //   setLocalStrings([]);
      // }
      // To Apply the styling
      // if (isRichTextHasChildren) {
      //   alert('handle selection');
      //   handleSelection();
      // }
    }
    return [];
  }, [
    localStrings,
    addAndUpdateSegment,
    addEmptyLine,
    addPlainSegments,
    getSelectedElements,
    handleSelectedText,
  ]);

  const debouncedChangeHandler = useCallback(debounce(handleInputChange, INPUT_DEBOUNCE), []);

  const getTextDecoration = useCallback((style: Maybe<RichTextContentStringStyle> | undefined) => {
    const underLine = style?.underline ? 'underline' : 'none';
    return style?.strikethrough ? 'line-through' : underLine;
  }, []);

  const getFontStyle = (style: RichTextContentStringStyle) =>
    style?.italics ? 'italic' : 'normal';
  const handleEnter = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === 'Enter') {
        e.preventDefault();
        const responseStrings = handleInputChange();
        if (handleSubmit) {
          handleSubmit({
            id: segmentId,
            order: 1,
            variant: 'input',
            type: RichTextContentSegmentTypeEnum.Paragraph,
            strings: responseStrings as RichTextContentSegmentString[],
          });
        }
      }
    },
    [localStrings]
  );

  return (
    <div
      style={{
        position: 'relative',
      }}
      key={segmentId}
    >
      <ContentEditableDiv
        contentEditable="true"
        placeholder={placeholder}
        suppressContentEditableWarning={true}
        ref={richRef}
        onKeyDown={handleEnter}
        onClick={updateCursorPosition}
        onKeyUp={updateCursorPosition}
        id="contenteditable"
        onInput={debouncedChangeHandler}
        style={{
          fontSize: 14,
          color: textColor,
          fontFamily: 'CircularStd-Book',
          flexWrap: 'wrap',
          overflow: 'scroll',
          outline: 'none',
        }}
      >
        {strings.map(({ id, style, value, onNewLine }) => {
          if (value === LineBreak.space) {
            return (
              <RichText key={id} id={id} data-is-code={style?.codeStyle}>
                &nbsp;
              </RichText>
            );
          } else {
            return (
              <>
                <RichText
                  key={id}
                  id={id}
                  data-is-code={style?.codeStyle}
                  style={{
                    fontStyle: getFontStyle(style as RichTextContentStringStyle),
                    fontWeight: style?.bold ? 'bold' : 'normal',
                    textDecorationLine: getTextDecoration(style),
                    color:
                      style?.textColor === RichTextContentStringColorEnum.Default.toLowerCase()
                        ? textColor
                        : style?.textColor,
                  }}
                  backgroundColor={style?.codeStyle ? 'primary' : 'transparent'} // TODO:sarmad need to change background color
                >
                  {value}
                </RichText>
                <Visible if={!!onNewLine}>
                  <br key={id} />
                </Visible>
              </>
            );
          }
        })}
      </ContentEditableDiv>
    </div>
  );
};

type RichTextContentSegmentProps = RichTextContentSegment & {
  variant: 'input' | 'blur';
};

export type RichTextContentProps = {
  /**
   * content contains the rich text content which is holding the segments array
   */
  richTextContent: RichTextContentSegment;
  setRichTextContent: ({ id, strings, type, order, variant }: RichTextContentSegmentProps) => void;
  placeholder: string;
  handleSubmit?: ({ id, strings, type, order, variant }: RichTextContentSegmentProps) => void;
  segmentStyle?: LocalSegmentStyleProps;
};
interface SelectedTextProps {
  start: HTMLElement;
  end: HTMLElement;
}
interface LocalSegmentStyleProps {
  isBold: boolean;
  isItalic: boolean;
  isCode: boolean;
  isUnderLine: boolean;
  isStrikeThrough: boolean;
}
