import { DirectiveBinding } from 'vue';
import * as rangy from 'rangy';
import 'rangy/lib/rangy-textrange';
import log from 'loglevel';
import * as Mark from 'mark.js';
import { ContentBlockNode } from '@/__generated__/graphql';
import ContentBlock from '@/components/ContentBlock.vue';
import popover from '@/helpers/popover';

// todo: we need to get the following information for a highlight:
// Which ContentBlock? => contentBlock.id
// Which ContentComponent inside the ContentBlock? => Index of contentBlock.contents
// Which paragraph inside the Element
// Which start and endpoint of the selection? Or rather start and length?
// Also, we probably want to store the text of the selection at least, and maybe even some more for context?

interface Highlight {
  contentBlock: string; // the id
  contentIndex: number; // which content of the .contents array do I come from?
  contentUUID: string; // the wagtail UUID of the content element
  paragraphIndex: number; // which paragraph inside the textBlock do I come from?
  startPosition: number; // start of the selection
  selectionLength: number; // length of the selection
  text: string; // text that was actually selected
}

const TOP_TRESHOLD = 0;
const BOTTOM_TRESHOLD = window.innerHeight || document.documentElement.clientHeight;

const isInsideViewport = (position: number) => {
  return position >= TOP_TRESHOLD && position <= BOTTOM_TRESHOLD;
};

const findClosestAncestorWithTag = (node: Node, tag: string): HTMLElement | null => {
  let currentNode: Node | null = node;
  while (currentNode && currentNode.nodeName.toLowerCase() !== tag && currentNode.nodeName.toLowerCase() !== 'body') {
    currentNode = currentNode.parentNode;
  }
  if (currentNode) {
    return currentNode as HTMLElement;
  }
  return null;
};

const findClosestAncestorWithClass = (node: Node, className: string): HTMLElement | null => {
  let currentNode: Node | null = node;
  while (
    currentNode && // current node exists
    // current node is not an HTMLElement, so probably a TextNode
    // or, current node does not contain the class
    (!(currentNode instanceof HTMLElement) || !currentNode.classList.contains(className)) &&
    // we have not arrived at the top yet
    currentNode.nodeName.toLowerCase() !== 'body'
  ) {
    // so, we continue looking
    currentNode = currentNode.parentNode;
  }
  if (currentNode instanceof HTMLElement && currentNode.classList.contains(className)) {
    // make sure we're not up at the body element
    return currentNode as HTMLElement;
  }
  return null;
};

const findPositionInParent = (element: HTMLElement, className: string = 'content-component'): number => {
  /*
   * currently, a content block has the structure
   * div.content-block__container
   *  div.content-block
   *    div.content-block__visibility
   *    div.content-block__title
   *    div.content-component
   *    div.content-component
   *    [...]
   *    div.content-component
   *
   * We only care about the .content-component divs, not the others
   *
   */

  if (!element.parentElement) {
    log.error('element has no parent', element);
    return -1; // element has no parent, probably somehow ended up at the top. But this should probably never happen in our application
  }
  const children = Array.from(element.parentElement.children).filter((child) => child.classList.contains(className));
  return children.indexOf(element);
};

export const getSelectionHandler = (el: HTMLElement, contentBlock: ContentBlockNode) => (_e: Event) => {
  const rect = el.getBoundingClientRect();
  if (isInsideViewport(rect.top) || isInsideViewport(rect.bottom)) {
    // the listener only does something if the `el` is visible in the viewport, to save resources

    // now we check if the selection is inside our container

    const selection = rangy.getSelection();
    if (selection.rangeCount && !selection.isCollapsed) {
      const range = selection.getRangeAt(0);
      if (range.intersectsNode(el)) {
        // the selection is inside our container, so we can do something with it
        const { startContainer, endContainer } = range;
        const paragraph = findClosestAncestorWithTag(startContainer, 'p');
        const endAncestor = findClosestAncestorWithTag(endContainer, 'p');

        if (paragraph === endAncestor) {
          // todo: rename this variable, `parent` is not correct anymore
          const contentComponent = findClosestAncestorWithClass(startContainer, 'content-component');
          if (contentComponent) {
            // our selection is wholly inside the container node, we continue with it
            const position = findPositionInParent(contentComponent);
            const siblings = Array.from(paragraph.parentElement.children);
            const positionInTextBlock = siblings.indexOf(paragraph);

            const numOfParagraphs = Array.from(contentComponent.children[1].children).filter(
              (child) => child.nodeName.toLowerCase() === 'p'
            ).length;

            const { start, end } = range.toCharacterRange(paragraph);
            const uuid = contentComponent.dataset.uuid || '';
            const highlightedText: Highlight = {
              contentBlock: contentBlock.id,
              contentIndex: position,
              contentUUID: uuid,
              startPosition: start,
              paragraphIndex: positionInTextBlock,
              selectionLength: end - start,
              text: range.toString(),
            };
            console.log(highlightedText);

            const positionOfContentBlock = el.getBoundingClientRect();
            const positionOfSelection = paragraph?.getBoundingClientRect();
            const offsetTop = positionOfSelection.top - positionOfContentBlock.top;
            popover
              .show({
                wrapper: el,
                offsetTop,
                onChooseColor: (color: string) => {
                  console.log('chosenColor', color);
                  // const newHighlight: Highlight = {
                  //   contentBlock: contentBlock.id,
                  //   contentIndex:
                  //
                  // };
                },
              })
              .then(() => {
                console.log('confirmed');
              })
              .catch(() => {
                console.log('canceled');
              });
            // todo: how do we save this? maybe we need to move away from the directive and do this inside the mounted
            // and unmounted hooks of the ContentBlock
          }
        }
      }
    }
  }
};

const highlight = (element: HTMLElement, ranges: Mark.Range[]) => {
  // todo: make the ContentComponent / TextBlock component handle the highlighting
  const instance = new Mark(element);
  instance.markRanges(ranges);
};

// const mounted = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, prevVnode: VNode) => {
const mounted = (el: HTMLElement, binding: DirectiveBinding) => {
  const contentBlock = (binding.instance as InstanceType<typeof ContentBlock>).contentBlock;
  document.addEventListener('selectionchange', getSelectionHandler(el, contentBlock));
  const demoRanges: Mark.Range[] = [
    { start: 3, length: 50 },
    { start: 182, length: 10 },
  ];
  // todo: find a way for the ContentBlock component to tell us when it's 'ready' and everything loaded. Otherwise, the
  // code inside this setTimeout would run too early and yield no elements
  setTimeout(() => {
    const elements = el.querySelectorAll<HTMLElement>('.content-component');
    // console.log(el);
    // console.log(elements);
    for (const element of elements) {
      // console.log(element);
      // console.log(typeof element);
      highlight(element, demoRanges);
    }
    // highlight(el, demoRanges);
  }, 2000);
};

// const removeListener = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, prevVnode: VNode) => {
const removeListener = (el: HTMLElement, binding: DirectiveBinding) => {
  const contentBlock = (binding.instance as InstanceType<typeof ContentBlock>).contentBlock;
  document.removeEventListener('selectionchange', getSelectionHandler(el, contentBlock));
};

// todo: move this to the contentBlock
export default {
  mounted,
  unmounted: removeListener,
};
