import { Context } from "./context";
import { QWebExprVar } from "./expression_parser";
import { QWeb } from "./qweb";

/**
 * Owl QWeb Directives
 *
 * This file contains the implementation of most standard QWeb directives:
 * - t-esc
 * - t-raw
 * - t-set/t-value
 * - t-if/t-elif/t-else
 * - t-call
 * - t-foreach/t-as
 * - t-debug
 * - t-log
 */

//------------------------------------------------------------------------------
// t-esc and t-raw
//------------------------------------------------------------------------------
QWeb.utils.getFragment = function(str: string): DocumentFragment {
  const temp = document.createElement("template");
  temp.innerHTML = str;
  return temp.content;
};

function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
  if (value === "0" && ctx.caller) {
    qweb._compileNode(ctx.caller, ctx);
    return;
  }

  if (value.xml instanceof NodeList) {
    for (let node of Array.from(value.xml)) {
      qweb._compileNode(<ChildNode>node, ctx);
    }
    return;
  }
  let exprID: string;
  if (typeof value === "string") {
    exprID = `_${ctx.generateID()}`;
    ctx.addLine(`var ${exprID} = ${ctx.formatExpression(value)};`);
  } else {
    exprID = value.id;
  }
  ctx.addIf(`${exprID} || ${exprID} === 0`);
  if (ctx.escaping) {
    if (ctx.parentTextNode) {
      ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
    } else if (ctx.parentNode) {
      ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
    } else {
      let nodeID = ctx.generateID();
      ctx.rootContext.rootNode = nodeID;
      ctx.rootContext.parentTextNode = nodeID;
      ctx.addLine(`var vn${nodeID} = {text: ${exprID}};`);
    }
  } else {
    let fragID = ctx.generateID();
    ctx.rootContext.shouldDefineUtils = true;
    ctx.addLine(`var frag${fragID} = utils.getFragment(${exprID})`);
    let tempNodeID = ctx.generateID();
    ctx.addLine(`var p${tempNodeID} = {hook: {`);
    ctx.addLine(`  insert: n => n.elm.parentNode.replaceChild(frag${fragID}, n.elm),`);
    ctx.addLine(`}};`);
    ctx.addLine(`var vn${tempNodeID} = h('div', p${tempNodeID})`);
    ctx.addLine(`c${ctx.parentNode}.push(vn${tempNodeID});`);
  }
  if (node.childNodes.length) {
    ctx.addElse();
    qweb._compileChildren(node, ctx);
  }

  ctx.closeIf();
}

QWeb.addDirective({
  name: "esc",
  priority: 70,
  atNodeEncounter({ node, qweb, ctx }): boolean {
    if (node.nodeName !== "t") {
      let nodeID = qweb._compileGenericNode(node, ctx);
      ctx = ctx.withParent(nodeID);
    }
    let value = ctx.getValue(node.getAttribute("t-esc")!);
    compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
    return true;
  }
});

QWeb.addDirective({
  name: "raw",
  priority: 80,
  atNodeEncounter({ node, qweb, ctx }): boolean {
    if (node.nodeName !== "t") {
      let nodeID = qweb._compileGenericNode(node, ctx);
      ctx = ctx.withParent(nodeID);
    }
    let value = ctx.getValue(node.getAttribute("t-raw")!);
    compileValueNode(value, node, qweb, ctx);
    return true;
  }
});

//------------------------------------------------------------------------------
// t-set
//------------------------------------------------------------------------------
QWeb.addDirective({
  name: "set",
  extraNames: ["value"],
  priority: 60,
  atNodeEncounter({ node, ctx }): boolean {
    const variable = node.getAttribute("t-set")!;
    let value = node.getAttribute("t-value")!;
    if (value) {
      const formattedValue = ctx.formatExpression(value);
      if (ctx.variables.hasOwnProperty(variable)) {
        ctx.addLine(`${(<QWebExprVar>ctx.variables[variable]).id} = ${formattedValue}`);
      } else {
        const varName = `_${ctx.generateID()}`;
        ctx.addLine(`var ${varName} = ${formattedValue};`);
        ctx.variables[variable] = {
          id: varName,
          expr: formattedValue
        };
      }
    } else {
      ctx.variables[variable] = {
        xml: node.childNodes
      };
    }
    return true;
  }
});

//------------------------------------------------------------------------------
// t-if, t-elif, t-else
//------------------------------------------------------------------------------
QWeb.addDirective({
  name: "if",
  priority: 20,
  atNodeEncounter({ node, ctx }): boolean {
    let cond = ctx.getValue(node.getAttribute("t-if")!);
    ctx.addIf(`${ctx.formatExpression(cond)}`);
    return false;
  },
  finalize({ ctx }) {
    ctx.closeIf();
  }
});

QWeb.addDirective({
  name: "elif",
  priority: 30,
  atNodeEncounter({ node, ctx }): boolean {
    let cond = ctx.getValue(node.getAttribute("t-elif")!);
    ctx.addLine(`else if (${ctx.formatExpression(cond)}) {`);
    ctx.indent();
    return false;
  },
  finalize({ ctx }) {
    ctx.closeIf();
  }
});

QWeb.addDirective({
  name: "else",
  priority: 40,
  atNodeEncounter({ ctx }): boolean {
    ctx.addLine(`else {`);
    ctx.indent();
    return false;
  },
  finalize({ ctx }) {
    ctx.closeIf();
  }
});

//------------------------------------------------------------------------------
// t-call
//------------------------------------------------------------------------------
QWeb.addDirective({
  name: "call",
  priority: 50,
  atNodeEncounter({ node, qweb, ctx }): boolean {
    if (node.nodeName !== "t") {
      throw new Error("Invalid tag for t-call directive (should be 't')");
    }
    const subTemplate = node.getAttribute("t-call")!;
    const nodeTemplate = qweb.templates[subTemplate];
    if (!nodeTemplate) {
      throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
    }
    const nodeCopy = node.cloneNode(true) as Element;
    nodeCopy.removeAttribute("t-call");

    // extract variables from nodecopy
    const tempCtx = new Context();
    tempCtx.nextID = ctx.rootContext.nextID;
    qweb._compileNode(nodeCopy, tempCtx);
    const vars = Object.assign({}, ctx.variables, tempCtx.variables);
    ctx.rootContext.nextID = tempCtx.nextID;

    // open new scope, if necessary
    const hasNewVariables = Object.keys(tempCtx.variables).length > 0;
    if (hasNewVariables) {
      ctx.addLine("{");
      ctx.indent();
      // add new variables, if any
      for (let key in tempCtx.variables) {
        const v = tempCtx.variables[key];
        if ((<QWebExprVar>v).expr) {
          ctx.addLine(`let ${(<QWebExprVar>v).id} = ${(<QWebExprVar>v).expr};`);
        }
        // todo: handle XML variables...
      }
    }

    // compile sub template
    const subCtx = ctx.subContext("caller", nodeCopy).subContext("variables", Object.create(vars));

    qweb._compileNode(nodeTemplate.elem, subCtx);

    // close new scope
    if (hasNewVariables) {
      ctx.dedent();
      ctx.addLine("}");
    }

    if (node.hasAttribute("t-if") || node.hasAttribute("t-else") || node.hasAttribute("t-elif")) {
      ctx.closeIf();
    }

    return true;
  }
});

//------------------------------------------------------------------------------
// t-foreach
//------------------------------------------------------------------------------
QWeb.addDirective({
  name: "foreach",
  extraNames: ["as"],
  priority: 10,
  atNodeEncounter({ node, qweb, ctx }): boolean {
    ctx.rootContext.shouldProtectContext = true;
    ctx = ctx.subContext("inLoop", true);
    const elems = node.getAttribute("t-foreach")!;
    const name = node.getAttribute("t-as")!;
    let arrayID = ctx.generateID();
    ctx.addLine(`var _${arrayID} = ${ctx.formatExpression(elems)};`);
    ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
    let keysID = ctx.generateID();
    let valuesID = ctx.generateID();
    ctx.addLine(`var _${keysID} = _${valuesID} = _${arrayID};`);
    ctx.addIf(`!(_${arrayID} instanceof Array)`);
    ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
    ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
    ctx.closeIf();
    ctx.addLine(`var _length${keysID} = _${keysID}.length;`);
    ctx.addLine(`for (let i = 0; i < _length${keysID}; i++) {`);
    ctx.indent();
    ctx.addToScope(name + "_first", "i === 0");
    ctx.addToScope(name + "_last", `i === _length${keysID} - 1`);
    ctx.addToScope(name + "_index", "i");
    ctx.addToScope(name, `_${keysID}[i]`);
    ctx.addToScope(name + "_value", `_${valuesID}[i]`);
    const nodeCopy = <Element>node.cloneNode(true);
    let shouldWarn = nodeCopy.tagName !== "t" && !nodeCopy.hasAttribute("t-key");
    if (!shouldWarn && node.tagName === "t") {
      if (node.hasAttribute("t-component") && !node.hasAttribute("t-key")) {
        shouldWarn = true;
      }
      if (
        !shouldWarn &&
        node.children.length === 1 &&
        node.children[0].tagName !== "t" &&
        !node.children[0].hasAttribute("t-key")
      ) {
        shouldWarn = true;
      }
    }
    if (shouldWarn) {
      console.warn(
        `Directive t-foreach should always be used with a t-key! (in template: '${
          ctx.templateName
        }')`
      );
    }
    nodeCopy.removeAttribute("t-foreach");
    qweb._compileNode(nodeCopy, ctx);
    ctx.dedent();
    ctx.addLine("}");
    return true;
  }
});

//------------------------------------------------------------------------------
// t-debug
//------------------------------------------------------------------------------
QWeb.addDirective({
  name: "debug",
  priority: 1,
  atNodeEncounter({ ctx }) {
    ctx.addLine("debugger;");
  }
});

//------------------------------------------------------------------------------
// t-log
//------------------------------------------------------------------------------
QWeb.addDirective({
  name: "log",
  priority: 1,
  atNodeEncounter({ ctx, value }) {
    const expr = ctx.formatExpression(value);
    ctx.addLine(`console.log(${expr})`);
  }
});
