import React, { useState, useEffect } from 'react';

import useActions from '../actions';
import useStore from '../store';
import SearchComponent from '../components/Search';
import {
  normalize,
  calculatePhraseMatch,
  convertMatches,
} from '../helpers/pdf';
import { scrollIntoView } from '../helpers/utility';

type MatchType = {
  page: number;
  index: number;
};

const Search: React.FC = () => {
  const [queryString, setQueryString] = useState('');
  const [matchesMap, setMatchesMap] = useState<MatchType[]>([]);
  const [matchTotal, setMatchTotal] = useState(0);
  const [prevIndex, setPrevIndex] = useState(-1);
  const [currentIndex, setCurrentIndex] = useState(-1);
  const [{ navbarState, pdf, totalPage, textDivs }, dispatch] = useStore();
  const { setNavbar } = useActions(dispatch);

  const getTextContent = async (pageNum: number): Promise<any[]> => {
    const page = await pdf.getPage(pageNum);
    const textContent = await page.getTextContent({
      normalizeWhitespace: true,
    });

    return textContent.items;
  };

  const extractTextItems = async (pageNum: number): Promise<string[]> => {
    const textContent = await getTextContent(pageNum);
    const strBuf = [];

    for (let j = 0, len = textContent.length; j < len; j += 1) {
      if (textContent[j].str) {
        strBuf.push(textContent[j].str);
      }
    }

    return strBuf;
  };

  const getMatchTextIndex = async (
    pageNum: number,
    queryStr: string
  ): Promise<void> => {
    const contentItems = await extractTextItems(pageNum);
    const content = normalize(contentItems.join('').toLowerCase());
    const matches = calculatePhraseMatch(content, queryStr);

    if (matches.length) {
      matches.forEach(ele => {
        matchesMap.push({
          page: pageNum,
          index: ele,
        });
      });

      setMatchesMap(matchesMap);
      setMatchTotal(cur => cur + matches.length);
    }

    if (pageNum < totalPage) {
      await getMatchTextIndex(pageNum + 1, queryStr);
    }
  };

  const startSearchPdf = async (queryStr: string): Promise<void> => {
    getMatchTextIndex(1, queryStr);
  };

  const appendTextToDiv = async (
    pageNum: number,
    divIdx: number,
    fromOffset: number,
    toOffset: number | undefined,
    highlight: boolean
  ): Promise<any> => {
    const textContentItem = await extractTextItems(pageNum);

    if (textDivs[pageNum]) {
      const domElements = textDivs[pageNum][divIdx];

      const content = textContentItem[divIdx].substring(fromOffset, toOffset);
      const node = document.createTextNode(content);
      const span = document.createElement('span');
      if (highlight) {
        span.style.backgroundColor = 'rgba(255, 211, 0, 0.7)';
        span.appendChild(node);
        domElements.appendChild(span);
        scrollIntoView(domElements, { top: -120 });
      } else {
        domElements.textContent = '';
        domElements.appendChild(node);
      }
    }
  };

  const beginText = async (
    pageNum: number,
    begin: Record<string, any>
  ): Promise<any> => {
    const { divIdx } = begin;
    const domElements = textDivs[pageNum][divIdx];
    if (domElements) {
      domElements.textContent = '';
      appendTextToDiv(pageNum, divIdx, 0, begin.offset, false);
    }
  };

  const cleanMatches = async (
    pageNum: number,
    matchIndex: number,
    queryStr: string
  ): Promise<any> => {
    const textContentItem = await extractTextItems(pageNum);
    const { begin, end } = convertMatches(
      queryStr,
      matchIndex,
      textContentItem
    );

    for (let i = begin.divIdx; i <= end.divIdx; i += 1) {
      appendTextToDiv(pageNum, i, 0, undefined, false);
    }
  };

  const reset = (): void => {
    setMatchTotal(0);
    setCurrentIndex(-1);
    setPrevIndex(-1);
    setMatchesMap([]);
  };

  const renderMatches = async (
    pageNum: number,
    matchIndex: number,
    queryStr: string
  ): Promise<any> => {
    const textContentItem = await extractTextItems(pageNum);
    const { begin, end } = convertMatches(
      queryStr,
      matchIndex,
      textContentItem
    );

    beginText(pageNum, begin);

    if (begin.divIdx === end.divIdx) {
      appendTextToDiv(pageNum, begin.divIdx, begin.offset, end.offset, true);
    } else {
      for (let i = begin.divIdx; i <= end.divIdx; i += 1) {
        switch (i) {
          case begin.divIdx:
            appendTextToDiv(
              pageNum,
              begin.divIdx,
              begin.offset,
              undefined,
              true
            );
            break;
          case end.divIdx:
            beginText(pageNum, { divIdx: end.divIdx, offset: 0 });
            appendTextToDiv(pageNum, end.divIdx, 0, end.offset, true);
            break;
          default: {
            beginText(pageNum, { divIdx: i, offset: 0 });
            appendTextToDiv(pageNum, i, 0, undefined, true);
            break;
          }
        }
      }
    }
  };

  const handleSearch = (val: string): void => {
    if (!val) return;

    const queryStr = normalize(val.toLowerCase());
    if (queryStr !== queryString) {
      reset();
      setQueryString(queryStr);
      startSearchPdf(queryStr);
    }
  };

  const clickPrev = (): void => {
    setCurrentIndex(cur => {
      setPrevIndex(cur);
      if (cur > 0) {
        return cur - 1;
      }
      setPrevIndex(cur - 1);
      return cur;
    });
  };

  const clickNext = (): void => {
    setCurrentIndex(cur => {
      setPrevIndex(cur);
      if (cur + 1 < matchTotal) {
        return cur + 1;
      }
      return cur;
    });
  };

  const handleClose = (): void => {
    setNavbar('');
    reset();

    const currentMatches = matchesMap[currentIndex];
    if (currentMatches) {
      cleanMatches(currentMatches.page, currentMatches.index, queryString);
    }
  };

  useEffect(() => {
    if (matchTotal >= 1 && currentIndex === -1) {
      clickNext();
    }
  }, [matchTotal, currentIndex]);

  useEffect(() => {
    if (currentIndex >= 0) {
      const indexObj = matchesMap[currentIndex];
      const pageDiv: HTMLDivElement = document.getElementById(
        `page_${indexObj.page}`
      ) as HTMLDivElement;

      scrollIntoView(pageDiv);
    }
  }, [currentIndex, matchesMap]);

  useEffect(() => {
    if (currentIndex >= 0) {
      const currentMatches = matchesMap[currentIndex];

      if (textDivs[currentMatches.page]) {
        renderMatches(currentMatches.page, currentMatches.index, queryString);
      }

      if (currentIndex !== prevIndex && prevIndex >= 0) {
        const prevMatches = matchesMap[prevIndex];
        if (prevMatches) {
          cleanMatches(prevMatches.page, prevMatches.index, queryString);
        }
      }
    }
  }, [currentIndex, prevIndex, matchesMap, queryString, textDivs]);

  return (
    <SearchComponent
      matchesTotal={matchTotal}
      current={currentIndex + 1}
      onPrev={clickPrev}
      onNext={clickNext}
      onEnter={handleSearch}
      isActive={navbarState === 'search'}
      close={handleClose}
    />
  );
};

export default Search;
