import fs from "fs";
import path from "path";

export default class {
  #views;
  #globals;
  #cache;
  #templates;
  #debug;

  constructor() {
    this.#views = "./views";
    this.#globals = {};
    this.#debug = false;
    this.#cache = true;
    this.#templates = new Map();
  };

  set debug(d) {
    this.#debug = !!d;
  };
  set views(v) {
    this.#views = v;
    this.readdir(v);
  };
  set globals(g) {
    this.#globals = g;
  };
  set cache(c) {
    this.#cache = !!c;
  };

  readdir(dir, root = dir) {
    for(const d of fs.readdirSync(`${path.resolve()}/${dir}`)) {
      if(d.endsWith(".html")) { // template
        const file = path.parse(`${dir.replace(this.#views, '')}/${d}`);
        const t_dir = file.dir.substring(1);
        const t_file = file.name;
        this.getTemplate(!t_dir ? t_file : `${t_dir}/${t_file}`);
      }
      else { // directory
        this.readdir(`${dir}/${d}`, root);
      }
    }
  }

  getTemplate(tpl) {
    let template = {};
    let cache = false;
    if(this.#cache && this.#templates.has(tpl)) {
      template = this.#templates.get(tpl);
      cache = template.cached;
    }
    else {
      template = {
        code: fs.readFileSync(`${this.#views}/${tpl}.html`, "utf-8"),
        cached: new Date()
      };
      this.#templates.set(tpl, template);
    }
    return [
      template.code,
      (this.#debug ? `<!-- ${tpl}.html ` + (cache ? `cached ${template.cached}` : `not cached`) + " -->" : "")
    ].join("");
  };

  render(file, data = {}, locals) {
    data = { ...data, ...this.#globals, ...locals };
    try {
      const code = 'with(_data){const __html = [];' +
        '__html.push(\`' +
        this.getTemplate(file)
          .replace(/[\t]/g, ' ')
          .split('\`').join('\\\`')

          .replace(/{{--(.*?)--}}/g, "") // comments
          .replace(/{{(.+?)}}/g, '\`,$1,\`') // yield variable
          .replace(/{!!(.+?)!!}/g, '\`,this.escape($1),\`') // yield escaped variable

          .replace(/@js/g, "`);") // inject bare javascript
          .replace(/@endjs/g, ";__html.push(`")

          .replace(/@include\((.*?)\)/g, (_, inc) => this.render(inc, data)) // include template

          .replace(/@for\((.*?)\)$/gm, `\`);for($1){__html.push(\``)
          .replace(/@endfor/g, `\`);}__html.push(\``)

          .replace(/@each\((.*?) as (.*?)\)/g, `\`);this.forEach($1,($2,key)=>{__html.push(\``) // foreach for the poor
          .replace(/@endeach/g, "\`);});__html.push(`")

          .replace(/@elseif\((.*?)\)(\)?)/g, `\`);}else if($1$2){__html.push(\``) // if lol
          .replace(/@if\((.*?)\)(\)?)/g, `\`);if($1$2){__html.push(\``)
          .replace(/@else/g, "`);}else{__html.push(`")
          .replace(/@endif/g, "`);}__html.push(`") +
        '\`);return __html.join(\'\').replace(/\\n\\s*\\n/g, "\\n");}';

      return (new Function("_data", code)).bind({
        escape: this.escape,
        forEach: this.forEach
      })(data);
    } catch(err) {
      console.log(file, err.message);
      return (this.#debug ? `${err.message} in ${file}` : '');
    }
  };

  escape(str) {
    return (str + '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;')
      .replace(/{/g, '&#123;')
      .replace(/}/g, '&#125;')
    ;
  };

  forEach(o, f) {
    if(Array.isArray(o))
      o.forEach(f);
    else if(typeof o === "object")
      Object.keys(o).forEach(k => f.call(null, o[k], k));
    else
      throw new Error(`${o} is not a iterable object`);
  };
};