/**
 * Copyright (C) 2019-2025  The Software Heritage developers
 * See the AUTHORS file at the top-level directory of this distribution
 * License: GNU Affero General Public License version 3, or any later version
 * See top-level LICENSE file for more information
 */

import 'script-loader!notebookjs';
import {AnsiUp} from 'ansi_up';
import './notebook.css';

const ansiup = new AnsiUp();
ansiup.escape_for_html = false;

function escapeHTML(text) {
  text = text.replace(/</g, '&lt;');
  text = text.replace(/>/g, '&gt;');
  return text;
}

function unescapeHTML(text) {
  text = text.replace(/&lt;/g, '<');
  text = text.replace(/&gt;/g, '>');
  return text;
}

function escapeLaTeX(text) {

  const blockMath = /\$\$(.+?)\$\$|\\\\\[(.+?)\\\\\]/msg;
  const inlineMath = /\$(.+?)\$|\\\\\((.+?)\\\\\)/g;
  const latexEnvironment = /\\begin\{([a-z]*\*?)\}(.+?)\\end\{\1\}/msg;

  const mathTextFound = [];
  let bm;
  while ((bm = blockMath.exec(text)) !== null) {
    mathTextFound.push(bm[1]);
  }

  let im;
  while ((im = inlineMath.exec(text)) !== null) {
    mathTextFound.push(im[1]);
  }

  let le;
  while ((le = latexEnvironment.exec(text)) !== null) {
    mathTextFound.push(le[1]);
  }

  for (const mathText of mathTextFound) {
    // showdown will remove line breaks in LaTex array and
    // some escaping sequences when converting md to html.
    // So we use the following escaping hacks to keep them in the html
    // output and avoid MathJax typesetting errors.
    let escapedText = mathText.replace(/\\\\/g, '\\\\\\\\');
    for (const specialLaTexChar of ['{', '}', '#', '%', '&', '_']) {
      escapedText = escapedText.replace(new RegExp(`\\\\${specialLaTexChar}`, 'g'),
                                        `\\\\${specialLaTexChar}`);
    }

    // some html escaping is also needed
    escapedText = escapeHTML(escapedText);

    // hack to prevent showdown to replace _ or * characters
    // by html em tags as it will break some math typesetting
    // (setting the literalMidWordUnderscores option is not
    // enough as iy only works for _ characters contained in words)
    escapedText = escapedText.replace(/_/g, '{@}underscore{@}');
    escapedText = escapedText.replace(/\*/g, '{@}star{@}');

    if (mathText !== escapedText) {
      text = text.replace(mathText, escapedText);
    }
  }

  // fix math that should be rendered inline
  function replacer(match, p1, p2, offset, string) {
    const endIndex = offset + match.length;
    console.log(endIndex, text.length);
    const prevLineBreakPos = text.lastIndexOf('\n', offset);
    const nextLineBreakPos = text.indexOf('\n', endIndex);
    const textBeforeOnLine = text.slice(prevLineBreakPos, offset).trim();
    const textAfterOnLine = text.slice(endIndex, nextLineBreakPos).trim();
    if (!match.startsWith('\n') && // no line break after $$
        (textBeforeOnLine || // characters before $$
         textAfterOnLine || // characters after $$
         // $$ <math> $$ not on a single line
         ((offset > 0 && text[offset - 1] !== '\n') &&
          (endIndex < text.length - 1 && text[endIndex] !== '\n')))) {
      // not a block and should be inlined
      return match.slice(1, -1);
    }
    return match;
  }

  text = text.replace(blockMath, replacer);

  return text;
}

function renderMarkdownSync(showdown, text) {
  const converter = new showdown.Converter({
    tables: true,
    simplifiedAutoLink: true,
    rawHeaderId: true,
    literalMidWordUnderscores: true
  });

  // some LaTeX escaping is required to get correct math typesetting
  text = escapeLaTeX(text);

  // render markdown
  let rendered = converter.makeHtml(text);

  // restore underscores and stars in rendered HTML (see escapeLaTeX function)
  rendered = rendered.replace(/{@}underscore{@}/g, '_');
  rendered = rendered.replace(/{@}star{@}/g, '*');

  return rendered;
}

export async function renderMarkdownWithMath(text) {
  const showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown');
  return renderMarkdownSync(showdown, text);
}

export async function renderNotebook(nbJsonUrl, domElt) {

  const showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown');
  await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs');

  function renderMarkdown(text) {
    return renderMarkdownSync(showdown, text);
  }

  function highlightCode(text, preElt, codeElt, lang) {
    // no need to unescape text processed by ansiup
    if (text.indexOf('<span style="color:rgb(') === -1) {
      text = unescapeHTML(text);
    }
    if (lang && hljs.getLanguage(lang)) {
      return hljs.highlight(text, {language: lang}).value;
    } else {
      return text;
    }
  }

  function renderAnsi(text) {
    return ansiup.ansi_to_html(text);
  }

  nb.markdown = renderMarkdown;
  nb.highlighter = highlightCode;
  nb.ansi = renderAnsi;

  const response = await fetch(nbJsonUrl);
  const nbJson = await response.json();

  // parse the notebook
  const notebook = nb.parse(nbJson);
  // render it to HTML and apply XSS filtering
  const rendered = swh.webapp.filterXSS(notebook.render());
  // insert rendered notebook in the DOM
  $(domElt).append(rendered);
  // fix invalid double escaping done by notebookjs
  $(domElt).find('pre').each((i, pre) => {
    pre.innerHTML = pre.innerHTML.replace(/&amp;lt;/g, '&lt;');
    pre.innerHTML = pre.innerHTML.replace(/&amp;gt;/g, '&gt;');
  });
  // set light red background color for stderr output cells
  $('pre.nb-stderr').parent().css('background', '#fdd');
  // load MathJax library for math typesetting
  swh.mathjax.typesetMath();
}
