function resolve(path) {
  if (path.indexOf("settings") === 0 || path.indexOf("transformed") === 0) {
    return `this.${path}`;
  }
  return path;
}

function sexp(value) {
  if (value.path.original === "hash") {
    let result = [];

    value.hash.pairs.forEach(p => {
      let pValue = p.value.original;
      if (p.value.type === "StringLiteral") {
        pValue = JSON.stringify(pValue);
      }

      result.push(`"${p.key}": ${pValue}`);
    });

    return `{ ${result.join(", ")} }`;
  }
}

function argValue(arg) {
  let value = arg.value;
  if (value.type === "SubExpression") {
    return sexp(arg.value);
  } else if (value.type === "PathExpression") {
    return value.original;
  } else if (value.type === "StringLiteral") {
    return JSON.stringify(value.value);
  }
}

function mustacheValue(node, state) {
  let path = node.path.original;

  switch (path) {
    case "attach":
      let widgetName = argValue(node.hash.pairs.find(p => p.key === "widget"));

      let attrs = node.hash.pairs.find(p => p.key === "attrs");
      if (attrs) {
        return `this.attach(${widgetName}, ${argValue(attrs)})`;
      }
      return `this.attach(${widgetName}, attrs)`;

      break;
    case "yield":
      return `this.attrs.contents()`;
      break;
    case "i18n":
      let value;
      if (node.params[0].type === "StringLiteral") {
        value = `"${node.params[0].value}"`;
      } else if (node.params[0].type === "PathExpression") {
        value = resolve(node.params[0].original);
      }

      if (value) {
        return `I18n.t(${value})`;
      }

      break;
    case "fa-icon":
    case "d-icon":
      state.helpersUsed.iconNode = true;
      let icon = node.params[0].value;
      return `__iN("${icon}")`;
      break;
    default:
      if (node.escaped) {
        return `${resolve(path)}`;
      } else {
        state.helpersUsed.rawHtml = true;
        return `new __rH({ html: '<span>' + ${resolve(path)} + '</span>'})`;
      }
      break;
  }
}

class Compiler {
  constructor(ast) {
    this.idx = 0;
    this.ast = ast;

    this.state = {
      helpersUsed: {}
    };
  }

  newAcc() {
    return `_a${this.idx++}`;
  }

  processNode(parentAcc, node) {
    let instructions = [];
    let innerAcc;

    switch (node.type) {
      case "Program":
        node.body.forEach(bodyNode => {
          instructions = instructions.concat(
            this.processNode(parentAcc, bodyNode)
          );
        });
        break;
      case "ElementNode":
        innerAcc = this.newAcc();
        instructions.push(`var ${innerAcc} = [];`);
        node.children.forEach(child => {
          instructions = instructions.concat(this.processNode(innerAcc, child));
        });

        if (node.attributes.length) {
          let attributes = [];
          node.attributes.forEach(a => {
            const name = a.name === "class" ? "className" : a.name;
            if (a.value.type === "MustacheStatement") {
              attributes.push(
                `"${name}":${mustacheValue(a.value, this.state)}`
              );
            } else {
              attributes.push(`"${name}":"${a.value.chars}"`);
            }
          });

          const attrString = `{${attributes.join(", ")}}`;
          instructions.push(
            `${parentAcc}.push(virtualDom.h('${
              node.tag
            }', ${attrString}, ${innerAcc}));`
          );
        } else {
          instructions.push(
            `${parentAcc}.push(virtualDom.h('${node.tag}', ${innerAcc}));`
          );
        }

        break;

      case "TextNode":
        return `${parentAcc}.push(${JSON.stringify(node.chars)});`;

      case "MustacheStatement":
        const value = mustacheValue(node, this.state);
        if (value) {
          instructions.push(`${parentAcc}.push(${value})`);
        }
        break;
      case "BlockStatement":
        let negate = "";

        switch (node.path.original) {
          case "unless":
            negate = "!";
          case "if":
            instructions.push(
              `if (${negate}${resolve(node.params[0].original)}) {`
            );
            node.program.body.forEach(child => {
              instructions = instructions.concat(
                this.processNode(parentAcc, child)
              );
            });

            if (node.inverse) {
              instructions.push(`} else {`);
              node.inverse.body.forEach(child => {
                instructions = instructions.concat(
                  this.processNode(parentAcc, child)
                );
              });
            }
            instructions.push(`}`);
            break;
          case "each":
            const collection = resolve(node.params[0].original);
            instructions.push(`if (${collection} && ${collection}.length) {`);
            instructions.push(
              `  ${collection}.forEach(${node.program.blockParams[0]} => {`
            );
            node.program.body.forEach(child => {
              instructions = instructions.concat(
                this.processNode(parentAcc, child)
              );
            });
            instructions.push(`  });`);
            instructions.push("}");

            break;
        }
        break;
      default:
        break;
    }

    return instructions.join("\n");
  }

  compile() {
    return this.processNode("_r", this.ast);
  }
}

function compile(template) {
  const preprocessor = Ember.__loader.require("@glimmer/syntax");
  const compiled = preprocessor.preprocess(template);
  const compiler = new Compiler(compiled);

  let code = compiler.compile();

  let imports = "";
  if (compiler.state.helpersUsed.iconNode) {
    imports += "var __iN = Discourse.__widget_helpers.iconNode; ";
  }
  if (compiler.state.helpersUsed.rawHtml) {
    imports += "var __rH = Discourse.__widget_helpers.rawHtml; ";
  }

  return `function(attrs, state) { ${imports}var _r = [];\n${code}\nreturn _r; }`;
}

exports.compile = compile;

function error(path, state, msg) {
  const filename = state.file.opts.filename;
  return path.replaceWithSourceString(
    `function() { console.error("${filename}: ${msg}"); }`
  );
}

exports.WidgetHbsCompiler = function(babel) {
  let t = babel.types;
  return {
    visitor: {
      ImportDeclaration(path, state) {
        let node = path.node;
        if (
          t.isLiteral(node.source, { value: "discourse/widgets/hbs-compiler" })
        ) {
          let first = node.specifiers && node.specifiers[0];
          if (!t.isImportDefaultSpecifier(first)) {
            let input = state.file.code;
            let usedImportStatement = input.slice(node.start, node.end);
            let msg = `Only \`import hbs from 'discourse/widgets/hbs-compiler'\` is supported. You used: \`${usedImportStatement}\``;
            throw path.buildCodeFrameError(msg);
          }

          state.importId =
            state.importId ||
            path.scope.generateUidIdentifierBasedOnNode(path.node.id);
          path.scope.rename(first.local.name, state.importId.name);
          path.remove();
        }
      },

      TaggedTemplateExpression(path, state) {
        if (!state.importId) {
          return;
        }

        let tagPath = path.get("tag");
        if (tagPath.node.name !== state.importId.name) {
          return;
        }

        if (path.node.quasi.expressions.length) {
          return error(
            path,
            state,
            "placeholders inside a tagged template string are not supported"
          );
        }

        let template = path.node.quasi.quasis
          .map(quasi => quasi.value.cooked)
          .join("");

        try {
          path.replaceWithSourceString(compile(template));
        } catch (e) {
          return error(path, state, e.toString());
        }
      }
    }
  };
};