import {HtmlParser, HtmlParseTreeResult} from 'angular2/src/compiler/html_parser';
import {ParseSourceSpan, ParseError} from 'angular2/src/compiler/parse_util';
import {
  HtmlAst,
  HtmlAstVisitor,
  HtmlElementAst,
  HtmlAttrAst,
  HtmlTextAst,
  HtmlCommentAst,
  HtmlExpansionAst,
  HtmlExpansionCaseAst,
  htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {RegExpWrapper, NumberWrapper, isPresent} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Message, id} from './message';
import {expandNodes} from './expander';
import {
  messageFromAttribute,
  I18nError,
  I18N_ATTR_PREFIX,
  I18N_ATTR,
  partition,
  Part,
  stringifyNodes,
  meaning,
  getPhNameFromBinding,
  dedupePhName
} from './shared';

const _I18N_ATTR = "i18n";
const _PLACEHOLDER_ELEMENT = "ph";
const _NAME_ATTR = "name";
const _I18N_ATTR_PREFIX = "i18n-";
let _PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\w)+")\\>\\<\\/ph\\>`);

/**
 * Creates an i18n-ed version of the parsed template.
 *
 * Algorithm:
 *
 * To understand the algorithm, you need to know how partitioning works.
 * Partitioning is required as we can use two i18n comments to group node siblings together.
 * That is why we cannot just use nodes.
 *
 * Partitioning transforms an array of HtmlAst into an array of Part.
 * A part can optionally contain a root element or a root text node. And it can also contain
 * children.
 * A part can contain i18n property, in which case it needs to be transalted.
 *
 * Example:
 *
 * The following array of nodes will be split into four parts:
 *
 * ```
 * <a>A</a>
 * <b i18n>B</b>
 * <!-- i18n -->
 * <c>C</c>
 * D
 * <!-- /i18n -->
 * E
 * ```
 *
 * Part 1 containing the a tag. It should not be translated.
 * Part 2 containing the b tag. It should be translated.
 * Part 3 containing the c tag and the D text node. It should be translated.
 * Part 4 containing the E text node. It should not be translated.
 *
 *
 * It is also important to understand how we stringify nodes to create a message.
 *
 * We walk the tree and replace every element node with a placeholder. We also replace
 * all expressions in interpolation with placeholders. We also insert a placeholder element
 * to wrap a text node containing interpolation.
 *
 * Example:
 *
 * The following tree:
 *
 * ```
 * <a>A{{I}}</a><b>B</b>
 * ```
 *
 * will be stringified into:
 * ```
 * <ph name="e0"><ph name="t1">A<ph name="0"/></ph></ph><ph name="e2">B</ph>
 * ```
 *
 * This is what the algorithm does:
 *
 * 1. Use the provided html parser to get the html AST of the template.
 * 2. Partition the root nodes, and process each part separately.
 * 3. If a part does not have the i18n attribute, recurse to process children and attributes.
 * 4. If a part has the i18n attribute, merge the translated i18n part with the original tree.
 *
 * This is how the merging works:
 *
 * 1. Use the stringify function to get the message id. Look up the message in the map.
 * 2. Get the translated message. At this point we have two trees: the original tree
 * and the translated tree, where all the elements are replaced with placeholders.
 * 3. Use the original tree to create a mapping Index:number -> HtmlAst.
 * 4. Walk the translated tree.
 * 5. If we encounter a placeholder element, get is name property.
 * 6. Get the type and the index of the node using the name property.
 * 7. If the type is 'e', which means element, then:
 *     - translate the attributes of the original element
 *     - recurse to merge the children
 *     - create a new element using the original element name, original position,
 *     and translated children and attributes
 * 8. If the type if 't', which means text, then:
 *     - get the list of expressions from the original node.
 *     - get the string version of the interpolation subtree
 *     - find all the placeholders in the translated message, and replace them with the
 *     corresponding original expressions
 */
export class I18nHtmlParser implements HtmlParser {
  errors: ParseError[];

  constructor(private _htmlParser: HtmlParser, private _parser: Parser,
              private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]}) {}

  parse(sourceContent: string, sourceUrl: string,
        parseExpansionForms: boolean = false): HtmlParseTreeResult {
    this.errors = [];

    let res = this._htmlParser.parse(sourceContent, sourceUrl, true);
    if (res.errors.length > 0) {
      return res;
    } else {
      let nodes = this._recurse(expandNodes(res.rootNodes).nodes);
      return this.errors.length > 0 ? new HtmlParseTreeResult([], this.errors) :
                                      new HtmlParseTreeResult(nodes, []);
    }
  }

  private _processI18nPart(p: Part): HtmlAst[] {
    try {
      return p.hasI18n ? this._mergeI18Part(p) : this._recurseIntoI18nPart(p);
    } catch (e) {
      if (e instanceof I18nError) {
        this.errors.push(e);
        return [];
      } else {
        throw e;
      }
    }
  }

  private _mergeI18Part(p: Part): HtmlAst[] {
    let message = p.createMessage(this._parser);
    let messageId = id(message);
    if (!StringMapWrapper.contains(this._messages, messageId)) {
      throw new I18nError(
          p.sourceSpan, `Cannot find message for id '${messageId}', content '${message.content}'.`);
    }

    let parsedMessage = this._messages[messageId];
    return this._mergeTrees(p, parsedMessage, p.children);
  }

  private _recurseIntoI18nPart(p: Part): HtmlAst[] {
    // we found an element without an i18n attribute
    // we need to recurse in cause its children may have i18n set
    // we also need to translate its attributes
    if (isPresent(p.rootElement)) {
      let root = p.rootElement;
      let children = this._recurse(p.children);
      let attrs = this._i18nAttributes(root);
      return [
        new HtmlElementAst(root.name, attrs, children, root.sourceSpan, root.startSourceSpan,
                           root.endSourceSpan)
      ];

      // a text node without i18n or interpolation, nothing to do
    } else if (isPresent(p.rootTextNode)) {
      return [p.rootTextNode];

    } else {
      return this._recurse(p.children);
    }
  }

  private _recurse(nodes: HtmlAst[]): HtmlAst[] {
    let ps = partition(nodes, this.errors);
    return ListWrapper.flatten(ps.map(p => this._processI18nPart(p)));
  }

  private _mergeTrees(p: Part, translated: HtmlAst[], original: HtmlAst[]): HtmlAst[] {
    let l = new _CreateNodeMapping();
    htmlVisitAll(l, original);

    // merge the translated tree with the original tree.
    // we do it by preserving the source code position of the original tree
    let merged = this._mergeTreesHelper(translated, l.mapping);

    // if the root element is present, we need to create a new root element with its attributes
    // translated
    if (isPresent(p.rootElement)) {
      let root = p.rootElement;
      let attrs = this._i18nAttributes(root);
      return [
        new HtmlElementAst(root.name, attrs, merged, root.sourceSpan, root.startSourceSpan,
                           root.endSourceSpan)
      ];

      // this should never happen with a part. Parts that have root text node should not be merged.
    } else if (isPresent(p.rootTextNode)) {
      throw new BaseException("should not be reached");

    } else {
      return merged;
    }
  }

  private _mergeTreesHelper(translated: HtmlAst[], mapping: HtmlAst[]): HtmlAst[] {
    return translated.map(t => {
      if (t instanceof HtmlElementAst) {
        return this._mergeElementOrInterpolation(t, translated, mapping);

      } else if (t instanceof HtmlTextAst) {
        return t;

      } else {
        throw new BaseException("should not be reached");
      }
    });
  }

  private _mergeElementOrInterpolation(t: HtmlElementAst, translated: HtmlAst[],
                                       mapping: HtmlAst[]): HtmlAst {
    let name = this._getName(t);
    let type = name[0];
    let index = NumberWrapper.parseInt(name.substring(1), 10);
    let originalNode = mapping[index];

    if (type == "t") {
      return this._mergeTextInterpolation(t, <HtmlTextAst>originalNode);
    } else if (type == "e") {
      return this._mergeElement(t, <HtmlElementAst>originalNode, mapping);
    } else {
      throw new BaseException("should not be reached");
    }
  }

  private _getName(t: HtmlElementAst): string {
    if (t.name != _PLACEHOLDER_ELEMENT) {
      throw new I18nError(
          t.sourceSpan,
          `Unexpected tag "${t.name}". Only "${_PLACEHOLDER_ELEMENT}" tags are allowed.`);
    }
    let names = t.attrs.filter(a => a.name == _NAME_ATTR);
    if (names.length == 0) {
      throw new I18nError(t.sourceSpan, `Missing "${_NAME_ATTR}" attribute.`);
    }
    return names[0].value;
  }

  private _mergeTextInterpolation(t: HtmlElementAst, originalNode: HtmlTextAst): HtmlTextAst {
    let split =
        this._parser.splitInterpolation(originalNode.value, originalNode.sourceSpan.toString());
    let exps = isPresent(split) ? split.expressions : [];

    let messageSubstring =
        this._messagesContent.substring(t.startSourceSpan.end.offset, t.endSourceSpan.start.offset);
    let translated =
        this._replacePlaceholdersWithExpressions(messageSubstring, exps, originalNode.sourceSpan);

    return new HtmlTextAst(translated, originalNode.sourceSpan);
  }

  private _mergeElement(t: HtmlElementAst, originalNode: HtmlElementAst,
                        mapping: HtmlAst[]): HtmlElementAst {
    let children = this._mergeTreesHelper(t.children, mapping);
    return new HtmlElementAst(originalNode.name, this._i18nAttributes(originalNode), children,
                              originalNode.sourceSpan, originalNode.startSourceSpan,
                              originalNode.endSourceSpan);
  }

  private _i18nAttributes(el: HtmlElementAst): HtmlAttrAst[] {
    let res = [];
    el.attrs.forEach(attr => {
      if (attr.name.startsWith(I18N_ATTR_PREFIX) || attr.name == I18N_ATTR) return;

      let i18ns = el.attrs.filter(a => a.name == `i18n-${attr.name}`);
      if (i18ns.length == 0) {
        res.push(attr);
        return;
      }

      let i18n = i18ns[0];
      let message = messageFromAttribute(this._parser, el, i18n);
      let messageId = id(message);

      if (StringMapWrapper.contains(this._messages, messageId)) {
        let updatedMessage = this._replaceInterpolationInAttr(attr, this._messages[messageId]);
        res.push(new HtmlAttrAst(attr.name, updatedMessage, attr.sourceSpan));

      } else {
        throw new I18nError(
            attr.sourceSpan,
            `Cannot find message for id '${messageId}', content '${message.content}'.`);
      }
    });
    return res;
  }

  private _replaceInterpolationInAttr(attr: HtmlAttrAst, msg: HtmlAst[]): string {
    let split = this._parser.splitInterpolation(attr.value, attr.sourceSpan.toString());
    let exps = isPresent(split) ? split.expressions : [];

    let first = msg[0];
    let last = msg[msg.length - 1];

    let start = first.sourceSpan.start.offset;
    let end =
        last instanceof HtmlElementAst ? last.endSourceSpan.end.offset : last.sourceSpan.end.offset;
    let messageSubstring = this._messagesContent.substring(start, end);

    return this._replacePlaceholdersWithExpressions(messageSubstring, exps, attr.sourceSpan);
  };

  private _replacePlaceholdersWithExpressions(message: string, exps: string[],
                                              sourceSpan: ParseSourceSpan): string {
    let expMap = this._buildExprMap(exps);
    return RegExpWrapper.replaceAll(_PLACEHOLDER_EXPANDED_REGEXP, message, (match) => {
      let nameWithQuotes = match[2];
      let name = nameWithQuotes.substring(1, nameWithQuotes.length - 1);
      return this._convertIntoExpression(name, expMap, sourceSpan);
    });
  }

  private _buildExprMap(exps: string[]): Map<string, string> {
    let expMap = new Map<string, string>();
    let usedNames = new Map<string, number>();

    for (var i = 0; i < exps.length; i++) {
      let phName = getPhNameFromBinding(exps[i], i);
      expMap.set(dedupePhName(usedNames, phName), exps[i]);
    }
    return expMap;
  }

  private _convertIntoExpression(name: string, expMap: Map<string, string>,
                                 sourceSpan: ParseSourceSpan) {
    if (expMap.has(name)) {
      return `{{${expMap.get(name)}}}`;
    } else {
      throw new I18nError(sourceSpan, `Invalid interpolation name '${name}'`);
    }
  }
}

class _CreateNodeMapping implements HtmlAstVisitor {
  mapping: HtmlAst[] = [];

  visitElement(ast: HtmlElementAst, context: any): any {
    this.mapping.push(ast);
    htmlVisitAll(this, ast.children);
    return null;
  }

  visitAttr(ast: HtmlAttrAst, context: any): any { return null; }

  visitText(ast: HtmlTextAst, context: any): any {
    this.mapping.push(ast);
    return null;
  }

  visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }

  visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }

  visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
}
