import { Component, ComponentChild, RenderableProps, h } from 'preact';
import deepEqual from 'deep-equal';
import scrollIntoView from 'scroll-into-view-if-needed';
import classes from './MarginalAnnotations.module.scss';
import cls from 'classnames';
import { asArray, targetsUrl } from 'web-annotation-utils';
import { highlightText } from '@apache-annotator/dom';
import {
  MarginalAnnotationCard,
  MarginalAnnotationCardProps,
} from './MarginalAnnotationCard';
import { findTargetsInDocument } from '../util/dom-selectors'
import type { IAnnotationWithSource } from '../storage/Annotation';

interface AnnotationTargetHighlightProps extends MarginalAnnotationCardProps {
  appContainer: Node;
  annotation: IAnnotationWithSource;
  inFocus?: boolean;
}

interface AnnotationTargetHighlightState {
  highlightHovered: boolean;
  highlightClicked: boolean;
  anchorPosition?: number;
}

// This component renders the annotation card, but as a side
// effect highlights the annotation’s target in the page. It adds classes and
// event listeners to the wrapped annotation, to light up matching bodies and
// targets on clicking&hovering.
export class AnnotationTargetHighlight extends Component<
  AnnotationTargetHighlightProps,
  AnnotationTargetHighlightState
> {
  state: AnnotationTargetHighlightState = {
    highlightHovered: false,
    highlightClicked: false,
  };

  highlightCleanupFunctions: (() => void)[] = [];
  highlightMarkElements: HTMLElement[] = [];

  constructor() {
    super();
    this.handleMouseClickOnCard = this.handleMouseClickOnCard.bind(this);
    this.handleHighlightMouseEnter = this.handleHighlightMouseEnter.bind(this);
    this.handleHighlightMouseLeave = this.handleHighlightMouseLeave.bind(this);
    this.handleKeyUp = this.handleKeyUp.bind(this);
  }

  componentDidMount() {
    this.highlightTargets();
    // if (this.props.inFocus) {
    //   this.setState({ highlightClicked: true });
    // }
  }

  componentDidUpdate(
    previousProps: Readonly<AnnotationTargetHighlightProps>,
    previousState: Readonly<AnnotationTargetHighlightState>,
  ) {
    if (this.state.highlightClicked !== previousState.highlightClicked) {
      this.highlightMarkElements.forEach((markElement) =>
        this.state.highlightClicked
          ? markElement.classList.add(classes.clicked)
          : markElement.classList.remove(classes.clicked),
      );
    }

    if (this.state.highlightHovered !== previousState.highlightHovered) {
      this.highlightMarkElements.forEach((markElement) =>
        this.state.highlightHovered
          ? markElement.classList.add(classes.hovered)
          : markElement.classList.remove(classes.hovered),
      );
    }

    if (
      !deepEqual(
        this.props.annotation.annotation.target,
        previousProps.annotation.annotation.target,
      )
    ) {
      this.removeHighlights();
      this.highlightTargets();
    }

    if (this.props.inFocus && !previousProps.inFocus) {
      this.setState({ highlightClicked: true });
    }
  }

  componentWillUnmount() {
    this.removeHighlights();
  }

  async highlightTargets() {
    const targets = asArray(this.props.annotation.annotation.target);

    // Annotations may have multiple targets; ignore those not on this page.
    const targetsForThisPage = targets.filter((target) =>
      targetsUrl(target, document.URL),
    );

    // Try find the annotation targets within the document (if the target has a Selector).
    const domTargets = await findTargetsInDocument(targetsForThisPage);

    // Highlight the found targets.
    this.highlightCleanupFunctions = domTargets.flatMap((domTarget) => {
      // Ignore any matches inside our own annotations etc.
      if (
        (domTarget instanceof Range &&
          domTarget.intersectsNode(this.props.appContainer)) ||
        (domTarget instanceof Node &&
          domTarget.compareDocumentPosition(this.props.appContainer) &
            Node.DOCUMENT_POSITION_CONTAINS)
      ) {
        return [];
      }

      // Highlight the text contained in the Range using <mark> elements.
      const highlightCleanupFunction = highlightText(domTarget, 'mark', {
        class: classes.highlight,
        'data-annotation-highlight-temp': 'true',
      });

      // TODO tweak highlightText upstream to not need this silly workaround.
      const markElements = [
        ...document.querySelectorAll('[data-annotation-highlight-temp]'),
      ] as HTMLElement[];

      // React to mouse interactions with the highlight.
      markElements.forEach((markElement) => {
        markElement.removeAttribute('data-annotation-highlight-temp');

        markElement.addEventListener('click', () => this.handleMouseClick());
        markElement.addEventListener('mouseenter', () =>
          this.handleHighlightMouseEnter(),
        );
        markElement.addEventListener('mouseleave', () =>
          this.handleHighlightMouseLeave(),
        );
      });

      this.highlightMarkElements.push(...markElements);

      return [highlightCleanupFunction];
    });
    if (this.highlightMarkElements.length > 0) {
      const top = this.highlightMarkElements[0].getBoundingClientRect().top + window.scrollY;
      this.setState({
        anchorPosition: top,
      });
    }
  }

  removeHighlights() {
    this.highlightCleanupFunctions.forEach((removeHighlight) =>
      removeHighlight(),
    );
    this.highlightCleanupFunctions = [];
    this.highlightMarkElements = [];
    this.setState({
      highlightClicked: false,
      highlightHovered: false,
    });
  }

  handleMouseClick() {
    this.setState((currentState) => ({
      highlightClicked: !currentState.highlightClicked,
    }));
  }

  handleMouseClickOnCard() {
    if (!this.state.highlightClicked && this.highlightMarkElements.length > 0) {
      const firstMarkElement = this.highlightMarkElements[0];
      if (firstMarkElement.getBoundingClientRect().top)
      scrollIntoView(firstMarkElement, { behavior: 'smooth', block: 'center', scrollMode: 'if-needed' });
    }
    this.setState({
      highlightClicked: false,
    });
  }

  handleHighlightMouseEnter() {
    this.setState({ highlightHovered: true });
  }

  handleHighlightMouseLeave() {
    this.setState({ highlightHovered: false });
  }

  handleKeyUp(event: KeyboardEvent) {
    if (event.key === 'Escape') {
      this.setState({ highlightClicked: false });
      // TODO move focus out of annotation to hide it.
    }
  }

  render(
    {
      annotation,
      inFocus,
      ...otherProps
    }: RenderableProps<AnnotationTargetHighlightProps>,
    {
      highlightHovered,
      highlightClicked,
      anchorPosition,
    }: Readonly<AnnotationTargetHighlightState>,
  ): ComponentChild {
    const extraClasses = cls({
      [classes.highlightClicked]: highlightClicked,
      [classes.highlightHovered]: highlightHovered,
    });
    // Pass the extra class to the wrapped annotation.
    return (
      <MarginalAnnotationCard
        {...otherProps}
        annotation={annotation}
        onclick={this.handleMouseClickOnCard}
        onmouseenter={this.handleHighlightMouseEnter}
        onmouseleave={this.handleHighlightMouseLeave}
        onkeyup={this.handleKeyUp}
        anchorPosition={anchorPosition}
        extraClasses={extraClasses}
        inFocus={inFocus}
      />
    );
  }
}
