const fs = require('fs').promises;
const marked = require('marked');
const { mkdirp } = require('mkdirp');
const path = require('path');
const Prism = require('prismjs');
const loadLanguages = require('prismjs/components/');

marked.use(
  // Turn off deprecated options. See https://github.com/markedjs/marked/issues/2793.
  // For header IDs, these are implemented in our custom renderer below.
  // For email mangling, we don't need that option.
  { mangle: false, headerIds: false }
);

const encoding = 'utf-8';

loadLanguages(['bash', 'typescript']);

// From marked/helpers.js
const escapeReplacements = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&#39;'
};
function escape(html) {
  if (/[&<>"']/.test(html)) {
    return html.replace(/[&<>"']/g, (ch) => escapeReplacements[ch]);
  }
  return html;
}

class CustomRenderer extends marked.Renderer {
  constructor(options) {
    super(options);
    this.toc = [];
  }

  // Override default renderer to add language-* class to <pre> as well as
  // <code> (which is what Prism would do at runtime)
  code(code, infoString, escaped) {
    const lang = (infoString || '').match(/\S*/)[0];
    if (!lang) return `<pre><code>${escaped ? code : escape(code)}</code></pre>`;

    code = Prism.highlight(code, Prism.languages[lang], lang);
    const langId = this.options.langPrefix + escape(lang);
    return `<pre class="${langId}"><code class="${langId}">${code}</code></pre>`;
  }

  // Store headings in a table of contents to be rendered afterwards, and add
  // permalinks to all <h3> elements (function documentation headings)
  heading(text, level, raw, slugger) {
    let id = this.options.headerPrefix + slugger.slug(raw); // marked.js default

    // slugger increments it's internal 'seen' accumulator every time we call slug()
    // we don't want that, we just want the slug output, this reverses the change
    if (slugger.seen[id] === 0) delete slugger.seen[id];

    // Use the **function name** as the link ID, otherwise fall back to the ID
    // generated by marked.js
    const match = text.match(/<strong>(.*)<\/strong>/);
    if (match) id = match[1].replace(/[^a-zA-Z]/g, '-');

    this.toc.push({ id, level, raw: match ? match[1] : raw });
    return `<h${level} id="${id}"><a class="heading-link" href="#${id}"></a>${text}</h3>`;
  }

  // Rewrite .md to .html in links
  link(href, title, text) {
    href = href.replace(/\.md/, '.html');
    return super.link(href, title, text);
  }

  // Adapted from https://github.com/markedjs/marked/issues/545#issuecomment-495093214
  renderTOC() {
    function build(coll, ix, currentLevel, lines) {
      if (ix >= coll.length || coll[ix].level <= currentLevel) return ix;
      const { id, level, raw } = coll[ix];
      lines.push(`<li><a href="#${id}">${raw}</a>`);
      ix++;
      const innerLines = [];
      ix = build(coll, ix, level, innerLines);
      if (innerLines.length > 0) lines.push('<ul>', ...innerLines, '</ul>');
      lines.push('</li>');
      ix = build(coll, ix, currentLevel, lines);
      return ix;
    }
    const lines = [];
    lines.push('<ul>');
    build(this.toc, 1, 1, lines);
    lines.push('</ul>');
    return lines.join('\n');
  }
}

async function render(markdownFile, head, tail, basePath) {
  await mkdirp('../out/docs/assets');
  await mkdirp(path.join('../out/docs', path.dirname(markdownFile)));
  let markdownText = await fs.readFile(markdownFile, { encoding });

  // replace relative path in header/footer for sub dir pages. this expects '.' is doc root
  const replacePath = (html) => {
    const replacingPathPart = [/(href=)"\./g, /(src=)"\./g];
    let relativePath = path.relative(basePath, '.');
    relativePath = relativePath === '' ? '.' : relativePath;
    return replacingPathPart.reduce((pre, cur) => pre.replace(cur, `$1"${relativePath}`), html);
  };
  const _head = replacePath(head);
  const _tail = replacePath(tail);

  // Resolve transcludes
  const lines = await Promise.all(
    markdownText.split('\n').map(async (line) => {
      const match = line.match(/^{{(.*)}}$/);
      if (match) return await fs.readFile(match[1], { encoding });
      return line;
    })
  );
  markdownText = lines.join('\n');

  const renderer = new CustomRenderer();
  let htmlText = _head + marked.parse(markdownText, { renderer }) + _tail;
  htmlText = htmlText.replace(/^<!-- toc -->$/m, () => renderer.renderTOC());

  const htmlFile = path.resolve('../out/docs', markdownFile.replace(/\.md$/, '') + '.html');
  console.log(`${markdownFile} => ${htmlFile}`);
  await fs.writeFile(htmlFile, htmlText, { encoding });
}

async function go() {
  try {
    const head = await fs.readFile('head.html.part', { encoding });
    const tail = await fs.readFile('tail.html.part', { encoding });

    // read files and return content with sub dir structure
    const basePaths = ['.', './ja', './zh_CN'];
    const files = await Promise.all(
      basePaths.map(async (base) => {
        return (await fs.readdir(base)).map((file) => ({
          base,
          file: `${base}/${file}`
        }));
      })
    ).then((blocks) => blocks.flat());

    // copy or render /docs/* to /out/docs/
    await Promise.all(
      files.map(({ base, file }) => {
        switch (path.extname(file)) {
          // copy files *.css, *.html, *.svg to /out/docs
          case '.css':
          case '.html':
          case '.svg':
            return fs.copyFile(file, path.resolve('../out/docs/' + file));
          // convert files *.md to /out/docs/*.html
          case '.md':
            return render(file, head, tail, base);
          // skip remaining files
          default:
            return new Promise((resolve) => resolve());
        }
      })
    );
    // copy /docs/assets/* to /out/docs/assets/
    await Promise.all(
      (await fs.readdir('assets')).map((file) => {
        return fs.copyFile(path.resolve('assets', file), path.resolve('../out/docs/assets', file));
      })
    );
    // copy misc files to /out/docs/
    await Promise.all(
      [
        ['../out/docs/README.html', '../out/docs/index.html'],
        ['../out/docs/ja/README.html', '../out/docs/ja/index.html'],
        ['../out/docs/zh_CN/README.html', '../out/docs/zh_CN/index.html'],
        ['node_modules/prismjs/themes/prism.css', '../out/docs/prism.css']
      ].map(([file1, file2]) => {
        return fs.copyFile(path.resolve(file1), path.resolve(file2));
      })
    );
  } catch (e) {
    console.error(e);
    process.exit(1);
  }
  process.exit();
}

go();
