import { Remarkable } from "remarkable";
import assert from "assert";
// Node is absolutely stupid. The remarkable project has an esm setting which
// specifies the dist/esm directory, but it completely ignores it and complains.
import { linkify } from "remarkable/dist/cjs/linkify.js";
import Prism from "../static/js/prism.cjs";
import _ from "lodash";
import slugify from "slugify";
import fs from "fs";
import PATH from "path";
import child_process from "child_process";

_.templateSettings.interpolate = /{{([\s\S]+?)}}/g;

class Ork {

  constructor(base, metadata) {
    // base needs to go away, but for now it's where the root of the db is
    if(metadata.code) {
      this.code_path = `${base}${metadata.code}`;
    } else {
      this.code_path = false;
    }
  }

  js(file='code.js') {
    assert(this.code_path, "You are using ork but no code path set in metadata.");
    return `\`\`\`javascript\n${fs.readFileSync(`${this.code_path}/${file}`).toString().trim()}\n\`\`\``
  }

  node(file='code.js', args='') {
    assert(this.code_path, "You are using ork but no code path set.");
    let cmd = `node "${file}" ${args}`;
    let result = child_process.execSync(cmd, {cwd: this.code_path});
    return `\`\`\`shell-session\n$ ${cmd}\n${result.toString().trim()}\n\`\`\``
  }
}

const count_lines = (str) => {
  let match = str.match(/\n(?!$)/g);
  return match ? match.length + 1 : 1;
}

const line_number_spans = (str) => {
  let count = count_lines(str);
  let spans = new Array(count + 1).join('<span></span>');
  return `<span aria-hidden="true" class="line-numbers-rows">${spans}</span>`;
}

/**
 * Runs Prism on the string, defaulting to the "markup"
 * language which will encode most code correctly even
 * if you don't specify a language after the ```
 **/
const run_prism = (str, lang="markup") => {
  assert(lang !== undefined, "Language can't be undefined.");

  // do the check again since the language might not exist
  if(Prism.languages[lang]) {
    return Prism.highlight(str, Prism.languages[lang], lang);
  } else {
    console.error("Unknown language", lang);
    return Prism.highlight(str, Prism.languages["markup"], "markup");
  }
}

export const highlight = (str, lang) => {
  return run_prism(str, lang) + line_number_spans(str);
}

/* A separate renderer just for the titles that doesn't need anything else. */
export const title_render = new Remarkable('full').use(rem => {
  let pass = (tokens, idx) => '';
  rem.renderer.rules.paragraph_open = pass;
  rem.renderer.rules.paragraph_close = pass;
});

export const split = (raw_md) => {
  let [metadata, ...body] = raw_md.split('------');
  metadata = JSON.parse(metadata);
  body = body.join('------');
  return [metadata, body];
}

const null_cb = (metadata, body) => body;

export const create_renderer = (toc) => {
  const renderer = new Remarkable('full', {
    html: true,
    highlight
  });

  renderer.use(linkify);

  renderer.use(rem => {
    rem.renderer.rules.heading_open = (tokens, idx) => {
      let level = tokens[idx].hLevel;
      let content = tokens[idx + 1].content;
      let slug = slugify(content, {lower: true, strict: true});
      toc.push({level, content, slug});
      return `<h${level} id="${slug}">`;
    }

    rem.renderer.rules.heading_close = (tokens, idx) => {
      return `</h${tokens[idx].hLevel}>\n`;
    }
  });

  renderer.use(rem => {
    const orig_open = rem.renderer.rules.link_open;
    const orig_close = rem.renderer.rules.link_close;

    rem.renderer.rules.link_open = (tokens, idx, options) => {
      return orig_open(tokens, idx, options);
    }

    rem.renderer.rules.link_close = (tokens, idx, options) => {
      return orig_close(tokens, idx, options);
    }
  });

  return renderer;
}

export const render = (raw_md, cb=null_cb) => {
  let toc = [];
  let [metadata, body] = split(raw_md);

  const renderer = create_renderer(toc);
  let content =  renderer.render(cb(metadata, body));

  // now we can use the TOC to figure out a title
  metadata.title = toc[0].content;

  try {
    // finally, run the renderer on all of the TOC
    toc.forEach(t => t.html = title_render.render(t.content));
  } catch(error) {
    console.error(error);
  }

  metadata.original_md = body;

  return {toc, content, metadata};
}

export const render_with_code = (source_dir, md_file) => {
  // get the file without path or extension
  const tail_no_ext = PATH.basename(md_file, PATH.extname(md_file));

  // this is a closure that is passed to render to run Ork for the code wrapping
  const load_code = (metadata, body) => {
    return _.template(body)({ fs, ork: new Ork(source_dir, metadata)});
  }

  // load the .md file contents
  let raw_md = fs.readFileSync(md_file);

  let { toc, content, metadata } = render(raw_md.toString(), load_code);

  metadata.slug = slugify(tail_no_ext, {lower: true, strict: true});

  return {content, toc, metadata};
}

export default {
  render,
  split,
  render_with_code,
  create_renderer
}
