/*
This file is part of the Notesnook project (https://notesnook.com/)

Copyright (C) 2023 Streetwriters (Private) Limited

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

export type TOCItem = {
  level: number;
  title: string;
  id: string;
  top: number;
};

const levelsMap: Record<string, number> = {
  H1: 1,
  H2: 2,
  H3: 3,
  H4: 4,
  H5: 5,
  H6: 6
};

function getOffsetTopRelativeTo(
  element: HTMLElement,
  ancestor: HTMLElement
): number {
  let top = 0;
  let current: HTMLElement | null = element;
  // Walk up the offsetParent chain until we reach ancestor (the editor root).
  // This correctly handles elements nested inside positioned containers such as
  // callout blocks, where `offsetTop` alone would only be relative to the
  // nearest positioned parent instead of the editor root.
  while (current && current !== ancestor) {
    top += current.offsetTop;
    current = current.offsetParent as HTMLElement | null;
  }
  return top;
}

export function getTableOfContents(content: HTMLElement) {
  const tableOfContents: TOCItem[] = [];
  let level = -1;
  let prevHeading = 0;
  for (const heading of content.querySelectorAll<HTMLHeadingElement>(
    "h1, h2, h3, h4, h5, h6"
  )) {
    const title = heading.textContent;
    const id = heading.dataset.blockId;
    if (!id || !title) continue;
    const nodeName = heading.nodeName;
    const currentHeading = levelsMap[nodeName];

    const isInsideCallout = !!closestWithin(heading, ".callout", content);
    if (isInsideCallout) continue;

    level =
      prevHeading < currentHeading
        ? level + 1
        : prevHeading > currentHeading
        ? level - (prevHeading - currentHeading)
        : level;
    level = Math.max(0, level);
    prevHeading = currentHeading;

    tableOfContents.push({
      level,
      title,
      id,
      top: getOffsetTopRelativeTo(heading, content)
    });
  }
  return tableOfContents;
}

export function scrollIntoViewById(blockId: string, optionalStyles = "") {
  const element = document.querySelector<HTMLElement>(
    `.active [data-block-id=${JSON.stringify(blockId)}]`
  );

  if (element) {
    const css = `.active [data-block-id=${JSON.stringify(blockId)}] {
    background-color: var(--shade) !important;
    ${optionalStyles}
}`;
    const stylesheet = document.createElement("style");
    stylesheet.innerText = css;
    document.head.appendChild(stylesheet);
    setTimeout(() => {
      stylesheet.remove();
    }, 5000);

    setTimeout(
      () =>
        element.scrollIntoView({
          behavior: "smooth",
          block: "start",
          inline: "start"
        }),
      100
    );
  }
}

function closestWithin(
  element: Element,
  selector: string,
  boundary: Element
): Element | null {
  let current: Element | null = element;
  while (current && current !== boundary) {
    if (current.matches(selector)) return current;
    current = current.parentElement;
  }
  return null;
}
