/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import {Inject, Injectable, OpaqueToken, Optional, SecurityContext} from '@angular/core';

import {Console, MAX_INTERPOLATION_VALUES} from '../core_private';

import {ListWrapper, StringMapWrapper, SetWrapper,} from '../src/facade/collection';
import {RegExpWrapper, isPresent, StringWrapper, isBlank, isArray} from '../src/facade/lang';
import {BaseException} from '../src/facade/exceptions';
import {AST, Interpolation, ASTWithSource, TemplateBinding, RecursiveAstVisitor, BindingPipe, ParserError} from './expression_parser/ast';
import {Parser} from './expression_parser/parser';
import {CompileDirectiveMetadata, CompilePipeMetadata, CompileMetadataWithType, CompileTokenMetadata,} from './compile_metadata';
import {HtmlParser} from './html_parser';
import {splitNsName, mergeNsAndName} from './html_tags';
import {ParseSourceSpan, ParseError, ParseErrorLevel} from './parse_util';
import {InterpolationConfig} from './interpolation_config';

import {ElementAst, BoundElementPropertyAst, BoundEventAst, ReferenceAst, TemplateAst, TemplateAstVisitor, templateVisitAll, TextAst, BoundTextAst, EmbeddedTemplateAst, AttrAst, NgContentAst, PropertyBindingType, DirectiveAst, BoundDirectivePropertyAst, ProviderAst, ProviderAstType, VariableAst} from './template_ast';
import {CssSelector, SelectorMatcher} from './selector';

import {ElementSchemaRegistry} from './schema/element_schema_registry';
import {preparseElement, PreparsedElementType} from './template_preparser';

import {isStyleUrlResolvable} from './style_url_resolver';

import {HtmlAstVisitor, HtmlElementAst, HtmlAttrAst, HtmlTextAst, HtmlCommentAst, HtmlExpansionAst, HtmlExpansionCaseAst, htmlVisitAll} from './html_ast';

import {splitAtColon} from './util';
import {identifierToken, Identifiers} from './identifiers';

import {ProviderElementContext, ProviderViewContext} from './provider_parser';

// Group 1 = "bind-"
// Group 2 = "var-"
// Group 3 = "let-"
// Group 4 = "ref-/#"
// Group 5 = "on-"
// Group 6 = "bindon-"
// Group 7 = "animate-/@"
// Group 8 = the identifier after "bind-", "var-/#", or "on-"
// Group 9 = identifier inside [()]
// Group 10 = identifier inside []
// Group 11 = identifier inside ()
const BIND_NAME_REGEXP =
    /^(?:(?:(?:(bind-)|(var-)|(let-)|(ref-|#)|(on-)|(bindon-)|(animate-|@))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g;

const TEMPLATE_ELEMENT = 'template';
const TEMPLATE_ATTR = 'template';
const TEMPLATE_ATTR_PREFIX = '*';
const CLASS_ATTR = 'class';

const PROPERTY_PARTS_SEPARATOR = '.';
const ATTRIBUTE_PREFIX = 'attr';
const CLASS_PREFIX = 'class';
const STYLE_PREFIX = 'style';

const TEXT_CSS_SELECTOR = CssSelector.parse('*')[0];

/**
 * Provides an array of {@link TemplateAstVisitor}s which will be used to transform
 * parsed templates before compilation is invoked, allowing custom expression syntax
 * and other advanced transformations.
 *
 * This is currently an internal-only feature and not meant for general use.
 */
export const TEMPLATE_TRANSFORMS: any = /*@ts2dart_const*/ new OpaqueToken('TemplateTransforms');

export class TemplateParseError extends ParseError {
  constructor(message: string, span: ParseSourceSpan, level: ParseErrorLevel) {
    super(span, message, level);
  }
}

export class TemplateParseResult {
  constructor(public templateAst?: TemplateAst[], public errors?: ParseError[]) {}
}

@Injectable()
export class TemplateParser {
  constructor(
      private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry,
      private _htmlParser: HtmlParser, private _console: Console,
      @Optional() @Inject(TEMPLATE_TRANSFORMS) public transforms: TemplateAstVisitor[]) {}

  parse(
      component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveMetadata[],
      pipes: CompilePipeMetadata[], templateUrl: string): TemplateAst[] {
    const result = this.tryParse(component, template, directives, pipes, templateUrl);
    const warnings = result.errors.filter(error => error.level === ParseErrorLevel.WARNING);
    const errors = result.errors.filter(error => error.level === ParseErrorLevel.FATAL);
    if (warnings.length > 0) {
      this._console.warn(`Template parse warnings:\n${warnings.join('\n')}`);
    }
    if (errors.length > 0) {
      const errorString = errors.join('\n');
      throw new BaseException(`Template parse errors:\n${errorString}`);
    }

    return result.templateAst;
  }

  tryParse(
      component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveMetadata[],
      pipes: CompilePipeMetadata[], templateUrl: string): TemplateParseResult {
    // TODO: bad ???
    let interpolationConfig: any;
    if (component.template) {
      interpolationConfig = InterpolationConfig.fromArray(component.template.interpolation);
    }
    const htmlAstWithErrors =
        this._htmlParser.parse(template, templateUrl, false, interpolationConfig);
    const errors: ParseError[] = htmlAstWithErrors.errors;
    let result: TemplateAst[];

    if (htmlAstWithErrors.rootNodes.length > 0) {
      const uniqDirectives = <CompileDirectiveMetadata[]>removeDuplicates(directives);
      const uniqPipes = <CompilePipeMetadata[]>removeDuplicates(pipes);
      const providerViewContext =
          new ProviderViewContext(component, htmlAstWithErrors.rootNodes[0].sourceSpan);
      const parseVisitor = new TemplateParseVisitor(
          providerViewContext, uniqDirectives, uniqPipes, this._exprParser, this._schemaRegistry);

      result = htmlVisitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT);
      errors.push(...parseVisitor.errors, ...providerViewContext.errors);
    } else {
      result = [];
    }

    this._assertNoReferenceDuplicationOnTemplate(result, errors);

    if (errors.length > 0) {
      return new TemplateParseResult(result, errors);
    }
    if (isPresent(this.transforms)) {
      this.transforms.forEach(
          (transform: TemplateAstVisitor) => { result = templateVisitAll(transform, result); });
    }
    return new TemplateParseResult(result, errors);
  }

  /** @internal */
  _assertNoReferenceDuplicationOnTemplate(result: TemplateAst[], errors: TemplateParseError[]):
      void {
    const existingReferences: string[] = [];

    result.filter(element => !!(<any>element).references)
        .forEach(element => (<any>element).references.forEach((reference: ReferenceAst) => {
          const name = reference.name;
          if (existingReferences.indexOf(name) < 0) {
            existingReferences.push(name);
          } else {
            const error = new TemplateParseError(
                `Reference "#${name}" is defined several times`, reference.sourceSpan,
                ParseErrorLevel.FATAL);
            errors.push(error);
          }
        }));
  }
}

class TemplateParseVisitor implements HtmlAstVisitor {
  selectorMatcher: SelectorMatcher;
  errors: TemplateParseError[] = [];
  directivesIndex = new Map<CompileDirectiveMetadata, number>();
  ngContentCount: number = 0;
  pipesByName: Map<string, CompilePipeMetadata>;
  private _interpolationConfig: InterpolationConfig;

  constructor(
      public providerViewContext: ProviderViewContext, directives: CompileDirectiveMetadata[],
      pipes: CompilePipeMetadata[], private _exprParser: Parser,
      private _schemaRegistry: ElementSchemaRegistry) {
    this.selectorMatcher = new SelectorMatcher();

    const tempMeta = providerViewContext.component.template;

    // TODO
    if (isPresent(tempMeta) && isPresent(tempMeta.interpolation)) {
      this._interpolationConfig = {
        start: tempMeta.interpolation[0],
        end: tempMeta.interpolation[1]
      };
    }

    ListWrapper.forEachWithIndex(
        directives, (directive: CompileDirectiveMetadata, index: number) => {
          const selector = CssSelector.parse(directive.selector);
          this.selectorMatcher.addSelectables(selector, directive);
          this.directivesIndex.set(directive, index);
        });

    this.pipesByName = new Map<string, CompilePipeMetadata>();
    pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe));
  }

  private _reportError(
      message: string, sourceSpan: ParseSourceSpan,
      level: ParseErrorLevel = ParseErrorLevel.FATAL) {
    this.errors.push(new TemplateParseError(message, sourceSpan, level));
  }

  private _reportParserErors(errors: ParserError[], sourceSpan: ParseSourceSpan) {
    for (let error of errors) {
      this._reportError(error.message, sourceSpan);
    }
  }

  private _parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
    const sourceInfo = sourceSpan.start.toString();
    try {
      const ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig);
      if (ast) this._reportParserErors(ast.errors, sourceSpan);
      this._checkPipes(ast, sourceSpan);
      if (isPresent(ast) &&
          (<Interpolation>ast.ast).expressions.length > MAX_INTERPOLATION_VALUES) {
        throw new BaseException(
            `Only support at most ${MAX_INTERPOLATION_VALUES} interpolation values!`);
      }
      return ast;
    } catch (e) {
      this._reportError(`${e}`, sourceSpan);
      return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
    }
  }

  private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
    const sourceInfo = sourceSpan.start.toString();
    try {
      const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
      if (ast) this._reportParserErors(ast.errors, sourceSpan);
      this._checkPipes(ast, sourceSpan);
      return ast;
    } catch (e) {
      this._reportError(`${e}`, sourceSpan);
      return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
    }
  }

  private _parseBinding(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
    const sourceInfo = sourceSpan.start.toString();
    try {
      const ast = this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig);
      if (ast) this._reportParserErors(ast.errors, sourceSpan);
      this._checkPipes(ast, sourceSpan);
      return ast;
    } catch (e) {
      this._reportError(`${e}`, sourceSpan);
      return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
    }
  }

  private _parseTemplateBindings(value: string, sourceSpan: ParseSourceSpan): TemplateBinding[] {
    const sourceInfo = sourceSpan.start.toString();
    try {
      const bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo);
      this._reportParserErors(bindingsResult.errors, sourceSpan);
      bindingsResult.templateBindings.forEach((binding) => {
        if (isPresent(binding.expression)) {
          this._checkPipes(binding.expression, sourceSpan);
        }
      });
      bindingsResult.warnings.forEach(
          (warning) => { this._reportError(warning, sourceSpan, ParseErrorLevel.WARNING); });
      return bindingsResult.templateBindings;
    } catch (e) {
      this._reportError(`${e}`, sourceSpan);
      return [];
    }
  }

  private _checkPipes(ast: ASTWithSource, sourceSpan: ParseSourceSpan) {
    if (isPresent(ast)) {
      const collector = new PipeCollector();
      ast.visit(collector);
      collector.pipes.forEach((pipeName) => {
        if (!this.pipesByName.has(pipeName)) {
          this._reportError(`The pipe '${pipeName}' could not be found`, sourceSpan);
        }
      });
    }
  }

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

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

  visitText(ast: HtmlTextAst, parent: ElementContext): any {
    const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
    const expr = this._parseInterpolation(ast.value, ast.sourceSpan);
    if (isPresent(expr)) {
      return new BoundTextAst(expr, ngContentIndex, ast.sourceSpan);
    } else {
      return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
    }
  }

  visitAttr(ast: HtmlAttrAst, contex: any): any {
    return new AttrAst(ast.name, ast.value, ast.sourceSpan);
  }

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

  visitElement(element: HtmlElementAst, parent: ElementContext): any {
    const nodeName = element.name;
    const preparsedElement = preparseElement(element);
    if (preparsedElement.type === PreparsedElementType.SCRIPT ||
        preparsedElement.type === PreparsedElementType.STYLE) {
      // Skipping <script> for security reasons
      // Skipping <style> as we already processed them
      // in the StyleCompiler
      return null;
    }
    if (preparsedElement.type === PreparsedElementType.STYLESHEET &&
        isStyleUrlResolvable(preparsedElement.hrefAttr)) {
      // Skipping stylesheets with either relative urls or package scheme as we already processed
      // them in the StyleCompiler
      return null;
    }

    const matchableAttrs: string[][] = [];
    const elementOrDirectiveProps: BoundElementOrDirectiveProperty[] = [];
    const elementOrDirectiveRefs: ElementOrDirectiveRef[] = [];
    const elementVars: VariableAst[] = [];
    const animationProps: BoundElementPropertyAst[] = [];
    const events: BoundEventAst[] = [];

    const templateElementOrDirectiveProps: BoundElementOrDirectiveProperty[] = [];
    const templateMatchableAttrs: string[][] = [];
    const templateElementVars: VariableAst[] = [];

    let hasInlineTemplates = false;
    const attrs: AttrAst[] = [];
    const lcElName = splitNsName(nodeName.toLowerCase())[1];
    const isTemplateElement = lcElName == TEMPLATE_ELEMENT;

    element.attrs.forEach(attr => {
      const hasBinding = this._parseAttr(
          isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, animationProps, events,
          elementOrDirectiveRefs, elementVars);
      const hasTemplateBinding = this._parseInlineTemplateBinding(
          attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateElementVars);

      if (hasTemplateBinding && hasInlineTemplates) {
        this._reportError(
            `Can't have multiple template bindings on one element. Use only one attribute named 'template' or prefixed with *`,
            attr.sourceSpan);
      }

      if (!hasBinding && !hasTemplateBinding) {
        // don't include the bindings as attributes as well in the AST
        attrs.push(this.visitAttr(attr, null));
        matchableAttrs.push([attr.name, attr.value]);
      }
      if (hasTemplateBinding) {
        hasInlineTemplates = true;
      }
    });

    const elementCssSelector = createElementCssSelector(nodeName, matchableAttrs);
    const directiveMetas = this._parseDirectives(this.selectorMatcher, elementCssSelector);
    const references: ReferenceAst[] = [];
    const directiveAsts = this._createDirectiveAsts(
        isTemplateElement, element.name, directiveMetas, elementOrDirectiveProps,
        elementOrDirectiveRefs, element.sourceSpan, references);
    const elementProps: BoundElementPropertyAst[] =
        this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directiveAsts)
            .concat(animationProps);
    const isViewRoot = parent.isTemplateElement || hasInlineTemplates;
    const providerContext = new ProviderElementContext(
        this.providerViewContext, parent.providerContext, isViewRoot, directiveAsts, attrs,
        references, element.sourceSpan);
    const children = htmlVisitAll(
        preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this, element.children,
        ElementContext.create(
            isTemplateElement, directiveAsts,
            isTemplateElement ? parent.providerContext : providerContext));
    providerContext.afterElement();
    // Override the actual selector when the `ngProjectAs` attribute is provided
    const projectionSelector = isPresent(preparsedElement.projectAs) ?
        CssSelector.parse(preparsedElement.projectAs)[0] :
        elementCssSelector;
    const ngContentIndex = parent.findNgContentIndex(projectionSelector);
    let parsedElement: TemplateAst;

    if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
      if (isPresent(element.children) && element.children.length > 0) {
        this._reportError(
            `<ng-content> element cannot have content. <ng-content> must be immediately followed by </ng-content>`,
            element.sourceSpan);
      }

      parsedElement = new NgContentAst(
          this.ngContentCount++, hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
    } else if (isTemplateElement) {
      this._assertAllEventsPublishedByDirectives(directiveAsts, events);
      this._assertNoComponentsNorElementBindingsOnTemplate(
          directiveAsts, elementProps, element.sourceSpan);

      parsedElement = new EmbeddedTemplateAst(
          attrs, events, references, elementVars, providerContext.transformedDirectiveAsts,
          providerContext.transformProviders, providerContext.transformedHasViewContainer, children,
          hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
    } else {
      this._assertOnlyOneComponent(directiveAsts, element.sourceSpan);
      const ngContentIndex =
          hasInlineTemplates ? null : parent.findNgContentIndex(projectionSelector);
      parsedElement = new ElementAst(
          nodeName, attrs, elementProps, events, references,
          providerContext.transformedDirectiveAsts, providerContext.transformProviders,
          providerContext.transformedHasViewContainer, children,
          hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
    }
    if (hasInlineTemplates) {
      const templateCssSelector =
          createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs);
      const templateDirectiveMetas =
          this._parseDirectives(this.selectorMatcher, templateCssSelector);
      const templateDirectiveAsts = this._createDirectiveAsts(
          true, element.name, templateDirectiveMetas, templateElementOrDirectiveProps, [],
          element.sourceSpan, []);
      const templateElementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts(
          element.name, templateElementOrDirectiveProps, templateDirectiveAsts);
      this._assertNoComponentsNorElementBindingsOnTemplate(
          templateDirectiveAsts, templateElementProps, element.sourceSpan);
      const templateProviderContext = new ProviderElementContext(
          this.providerViewContext, parent.providerContext, parent.isTemplateElement,
          templateDirectiveAsts, [], [], element.sourceSpan);
      templateProviderContext.afterElement();

      parsedElement = new EmbeddedTemplateAst(
          [], [], [], templateElementVars, templateProviderContext.transformedDirectiveAsts,
          templateProviderContext.transformProviders,
          templateProviderContext.transformedHasViewContainer, [parsedElement], ngContentIndex,
          element.sourceSpan);
    }
    return parsedElement;
  }

  private _parseInlineTemplateBinding(
      attr: HtmlAttrAst, targetMatchableAttrs: string[][],
      targetProps: BoundElementOrDirectiveProperty[], targetVars: VariableAst[]): boolean {
    let templateBindingsSource: string = null;
    if (this._normalizeAttributeName(attr.name) == TEMPLATE_ATTR) {
      templateBindingsSource = attr.value;
    } else if (attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) {
      const key = attr.name.substring(TEMPLATE_ATTR_PREFIX.length);  // remove the star
      templateBindingsSource = (attr.value.length == 0) ? key : key + ' ' + attr.value;
    }
    if (isPresent(templateBindingsSource)) {
      const bindings = this._parseTemplateBindings(templateBindingsSource, attr.sourceSpan);
      for (let i = 0; i < bindings.length; i++) {
        const binding = bindings[i];
        if (binding.keyIsVar) {
          targetVars.push(new VariableAst(binding.key, binding.name, attr.sourceSpan));
        } else if (isPresent(binding.expression)) {
          this._parsePropertyAst(
              binding.key, binding.expression, attr.sourceSpan, targetMatchableAttrs, targetProps);
        } else {
          targetMatchableAttrs.push([binding.key, '']);
          this._parseLiteralAttr(binding.key, null, attr.sourceSpan, targetProps);
        }
      }
      return true;
    }
    return false;
  }

  private _parseAttr(
      isTemplateElement: boolean, attr: HtmlAttrAst, targetMatchableAttrs: string[][],
      targetProps: BoundElementOrDirectiveProperty[],
      targetAnimationProps: BoundElementPropertyAst[], targetEvents: BoundEventAst[],
      targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean {
    const attrName = this._normalizeAttributeName(attr.name);
    const attrValue = attr.value;
    const bindParts = RegExpWrapper.firstMatch(BIND_NAME_REGEXP, attrName);
    let hasBinding = false;
    if (isPresent(bindParts)) {
      hasBinding = true;
      if (isPresent(bindParts[1])) {  // match: bind-prop
        this._parsePropertyOrAnimation(
            bindParts[8], attrValue, attr.sourceSpan, targetMatchableAttrs, targetProps,
            targetAnimationProps);

      } else if (isPresent(bindParts[2])) {  // match: var-name / var-name="iden"
        const identifier = bindParts[8];
        if (isTemplateElement) {
          this._reportError(
              `"var-" on <template> elements is deprecated. Use "let-" instead!`, attr.sourceSpan,
              ParseErrorLevel.WARNING);
          this._parseVariable(identifier, attrValue, attr.sourceSpan, targetVars);
        } else {
          this._reportError(
              `"var-" on non <template> elements is deprecated. Use "ref-" instead!`,
              attr.sourceSpan, ParseErrorLevel.WARNING);
          this._parseReference(identifier, attrValue, attr.sourceSpan, targetRefs);
        }

      } else if (isPresent(bindParts[3])) {  // match: let-name
        if (isTemplateElement) {
          const identifier = bindParts[8];
          this._parseVariable(identifier, attrValue, attr.sourceSpan, targetVars);
        } else {
          this._reportError(`"let-" is only supported on template elements.`, attr.sourceSpan);
        }

      } else if (isPresent(bindParts[4])) {  // match: ref- / #iden
        const identifier = bindParts[8];
        this._parseReference(identifier, attrValue, attr.sourceSpan, targetRefs);

      } else if (isPresent(bindParts[5])) {  // match: on-event
        this._parseEvent(
            bindParts[8], attrValue, attr.sourceSpan, targetMatchableAttrs, targetEvents);

      } else if (isPresent(bindParts[6])) {  // match: bindon-prop
        this._parsePropertyOrAnimation(
            bindParts[8], attrValue, attr.sourceSpan, targetMatchableAttrs, targetProps,
            targetAnimationProps);
        this._parseAssignmentEvent(
            bindParts[8], attrValue, attr.sourceSpan, targetMatchableAttrs, targetEvents);

      } else if (isPresent(bindParts[7])) {  // match: animate-name
        if (attrName[0] == '@' && isPresent(attrValue) && attrValue.length > 0) {
          this._reportError(
              `Assigning animation triggers via @prop="exp" attributes with an expression is deprecated. Use [@prop]="exp" instead!`,
              attr.sourceSpan, ParseErrorLevel.WARNING);
        }
        this._parseAnimation(
            bindParts[8], attrValue, attr.sourceSpan, targetMatchableAttrs, targetAnimationProps);
      } else if (isPresent(bindParts[9])) {  // match: [(expr)]
        this._parsePropertyOrAnimation(
            bindParts[9], attrValue, attr.sourceSpan, targetMatchableAttrs, targetProps,
            targetAnimationProps);
        this._parseAssignmentEvent(
            bindParts[9], attrValue, attr.sourceSpan, targetMatchableAttrs, targetEvents);

      } else if (isPresent(bindParts[10])) {  // match: [expr]
        this._parsePropertyOrAnimation(
            bindParts[10], attrValue, attr.sourceSpan, targetMatchableAttrs, targetProps,
            targetAnimationProps);

      } else if (isPresent(bindParts[11])) {  // match: (event)
        this._parseEvent(
            bindParts[11], attrValue, attr.sourceSpan, targetMatchableAttrs, targetEvents);
      }
    } else {
      hasBinding = this._parsePropertyInterpolation(
          attrName, attrValue, attr.sourceSpan, targetMatchableAttrs, targetProps);
    }
    if (!hasBinding) {
      this._parseLiteralAttr(attrName, attrValue, attr.sourceSpan, targetProps);
    }
    return hasBinding;
  }

  private _normalizeAttributeName(attrName: string): string {
    return attrName.toLowerCase().startsWith('data-') ? attrName.substring(5) : attrName;
  }

  private _parseVariable(
      identifier: string, value: string, sourceSpan: ParseSourceSpan, targetVars: VariableAst[]) {
    if (identifier.indexOf('-') > -1) {
      this._reportError(`"-" is not allowed in variable names`, sourceSpan);
    }

    targetVars.push(new VariableAst(identifier, value, sourceSpan));
  }

  private _parseReference(
      identifier: string, value: string, sourceSpan: ParseSourceSpan,
      targetRefs: ElementOrDirectiveRef[]) {
    if (identifier.indexOf('-') > -1) {
      this._reportError(`"-" is not allowed in reference names`, sourceSpan);
    }

    targetRefs.push(new ElementOrDirectiveRef(identifier, value, sourceSpan));
  }

  private _parsePropertyOrAnimation(
      name: string, expression: string, sourceSpan: ParseSourceSpan,
      targetMatchableAttrs: string[][], targetProps: BoundElementOrDirectiveProperty[],
      targetAnimationProps: BoundElementPropertyAst[]) {
    if (name[0] == '@') {
      this._parseAnimation(
          name.substr(1), expression, sourceSpan, targetMatchableAttrs, targetAnimationProps);
    } else {
      this._parsePropertyAst(
          name, this._parseBinding(expression, sourceSpan), sourceSpan, targetMatchableAttrs,
          targetProps);
    }
  }

  private _parseAnimation(
      name: string, expression: string, sourceSpan: ParseSourceSpan,
      targetMatchableAttrs: string[][], targetAnimationProps: BoundElementPropertyAst[]) {
    // This will occur when a @trigger is not paired with an expression.
    // For animations it is valid to not have an expression since */void
    // states will be applied by angular when the element is attached/detached
    if (!isPresent(expression) || expression.length == 0) {
      expression = 'null';
    }
    const ast = this._parseBinding(expression, sourceSpan);
    targetMatchableAttrs.push([name, ast.source]);
    targetAnimationProps.push(new BoundElementPropertyAst(
        name, PropertyBindingType.Animation, SecurityContext.NONE, ast, null, sourceSpan));
  }

  private _parsePropertyInterpolation(
      name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][],
      targetProps: BoundElementOrDirectiveProperty[]): boolean {
    const expr = this._parseInterpolation(value, sourceSpan);
    if (isPresent(expr)) {
      this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps);
      return true;
    }
    return false;
  }

  private _parsePropertyAst(
      name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan,
      targetMatchableAttrs: string[][], targetProps: BoundElementOrDirectiveProperty[]) {
    targetMatchableAttrs.push([name, ast.source]);
    targetProps.push(new BoundElementOrDirectiveProperty(name, ast, false, sourceSpan));
  }

  private _parseAssignmentEvent(
      name: string, expression: string, sourceSpan: ParseSourceSpan,
      targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
    this._parseEvent(
        `${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, targetEvents);
  }

  private _parseEvent(
      name: string, expression: string, sourceSpan: ParseSourceSpan,
      targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
    // long format: 'target: eventName'
    const parts = splitAtColon(name, [null, name]);
    const target = parts[0];
    const eventName = parts[1];
    const ast = this._parseAction(expression, sourceSpan);
    targetMatchableAttrs.push([name, ast.source]);
    targetEvents.push(new BoundEventAst(eventName, target, ast, sourceSpan));
    // Don't detect directives for event names for now,
    // so don't add the event name to the matchableAttrs
  }

  private _parseLiteralAttr(
      name: string, value: string, sourceSpan: ParseSourceSpan,
      targetProps: BoundElementOrDirectiveProperty[]) {
    targetProps.push(new BoundElementOrDirectiveProperty(
        name, this._exprParser.wrapLiteralPrimitive(value, ''), true, sourceSpan));
  }

  private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector):
      CompileDirectiveMetadata[] {
    // Need to sort the directives so that we get consistent results throughout,
    // as selectorMatcher uses Maps inside.
    // Also dedupe directives as they might match more than one time!
    const directives = ListWrapper.createFixedSize(this.directivesIndex.size);
    selectorMatcher.match(elementCssSelector, (selector, directive) => {
      directives[this.directivesIndex.get(directive)] = directive;
    });
    return directives.filter(dir => isPresent(dir));
  }

  private _createDirectiveAsts(
      isTemplateElement: boolean, elementName: string, directives: CompileDirectiveMetadata[],
      props: BoundElementOrDirectiveProperty[], elementOrDirectiveRefs: ElementOrDirectiveRef[],
      sourceSpan: ParseSourceSpan, targetReferences: ReferenceAst[]): DirectiveAst[] {
    const matchedReferences = new Set<string>();
    let component: CompileDirectiveMetadata = null;
    const directiveAsts = directives.map((directive: CompileDirectiveMetadata) => {
      if (directive.isComponent) {
        component = directive;
      }
      const hostProperties: BoundElementPropertyAst[] = [];
      const hostEvents: BoundEventAst[] = [];
      const directiveProperties: BoundDirectivePropertyAst[] = [];
      this._createDirectiveHostPropertyAsts(
          elementName, directive.hostProperties, sourceSpan, hostProperties);
      this._createDirectiveHostEventAsts(directive.hostListeners, sourceSpan, hostEvents);
      this._createDirectivePropertyAsts(directive.inputs, props, directiveProperties);
      elementOrDirectiveRefs.forEach((elOrDirRef) => {
        if ((elOrDirRef.value.length === 0 && directive.isComponent) ||
            (directive.exportAs == elOrDirRef.value)) {
          targetReferences.push(new ReferenceAst(
              elOrDirRef.name, identifierToken(directive.type), elOrDirRef.sourceSpan));
          matchedReferences.add(elOrDirRef.name);
        }
      });
      return new DirectiveAst(
          directive, directiveProperties, hostProperties, hostEvents, sourceSpan);
    });
    elementOrDirectiveRefs.forEach((elOrDirRef) => {
      if (elOrDirRef.value.length > 0) {
        if (!SetWrapper.has(matchedReferences, elOrDirRef.name)) {
          this._reportError(
              `There is no directive with "exportAs" set to "${elOrDirRef.value}"`,
              elOrDirRef.sourceSpan);
        }
      } else if (isBlank(component)) {
        let refToken: CompileTokenMetadata = null;
        if (isTemplateElement) {
          refToken = identifierToken(Identifiers.TemplateRef);
        }
        targetReferences.push(new ReferenceAst(elOrDirRef.name, refToken, elOrDirRef.sourceSpan));
      }
    });  // fix syntax highlighting issue: `
    return directiveAsts;
  }

  private _createDirectiveHostPropertyAsts(
      elementName: string, hostProps: {[key: string]: string}, sourceSpan: ParseSourceSpan,
      targetPropertyAsts: BoundElementPropertyAst[]) {
    if (isPresent(hostProps)) {
      StringMapWrapper.forEach(hostProps, (expression: string, propName: string) => {
        const exprAst = this._parseBinding(expression, sourceSpan);
        targetPropertyAsts.push(
            this._createElementPropertyAst(elementName, propName, exprAst, sourceSpan));
      });
    }
  }

  private _createDirectiveHostEventAsts(
      hostListeners: {[key: string]: string}, sourceSpan: ParseSourceSpan,
      targetEventAsts: BoundEventAst[]) {
    if (isPresent(hostListeners)) {
      StringMapWrapper.forEach(hostListeners, (expression: string, propName: string) => {
        this._parseEvent(propName, expression, sourceSpan, [], targetEventAsts);
      });
    }
  }

  private _createDirectivePropertyAsts(
      directiveProperties: {[key: string]: string}, boundProps: BoundElementOrDirectiveProperty[],
      targetBoundDirectiveProps: BoundDirectivePropertyAst[]) {
    if (isPresent(directiveProperties)) {
      const boundPropsByName = new Map<string, BoundElementOrDirectiveProperty>();
      boundProps.forEach(boundProp => {
        const prevValue = boundPropsByName.get(boundProp.name);
        if (isBlank(prevValue) || prevValue.isLiteral) {
          // give [a]="b" a higher precedence than a="b" on the same element
          boundPropsByName.set(boundProp.name, boundProp);
        }
      });

      StringMapWrapper.forEach(directiveProperties, (elProp: string, dirProp: string) => {
        const boundProp = boundPropsByName.get(elProp);

        // Bindings are optional, so this binding only needs to be set up if an expression is given.
        if (isPresent(boundProp)) {
          targetBoundDirectiveProps.push(new BoundDirectivePropertyAst(
              dirProp, boundProp.name, boundProp.expression, boundProp.sourceSpan));
        }
      });
    }
  }

  private _createElementPropertyAsts(
      elementName: string, props: BoundElementOrDirectiveProperty[],
      directives: DirectiveAst[]): BoundElementPropertyAst[] {
    const boundElementProps: BoundElementPropertyAst[] = [];
    const boundDirectivePropsIndex = new Map<string, BoundDirectivePropertyAst>();
    directives.forEach((directive: DirectiveAst) => {
      directive.inputs.forEach((prop: BoundDirectivePropertyAst) => {
        boundDirectivePropsIndex.set(prop.templateName, prop);
      });
    });
    props.forEach((prop: BoundElementOrDirectiveProperty) => {
      if (!prop.isLiteral && isBlank(boundDirectivePropsIndex.get(prop.name))) {
        boundElementProps.push(this._createElementPropertyAst(
            elementName, prop.name, prop.expression, prop.sourceSpan));
      }
    });
    return boundElementProps;
  }

  private _createElementPropertyAst(
      elementName: string, name: string, ast: AST,
      sourceSpan: ParseSourceSpan): BoundElementPropertyAst {
    let unit: string = null;
    let bindingType: PropertyBindingType;
    let boundPropertyName: string;
    const parts = name.split(PROPERTY_PARTS_SEPARATOR);
    let securityContext: SecurityContext;
    if (parts.length === 1) {
      var partValue = parts[0];
      if (partValue[0] == '@') {
        boundPropertyName = partValue.substr(1);
        bindingType = PropertyBindingType.Animation;
        securityContext = SecurityContext.NONE;
        this._reportError(
            `Assigning animation triggers within host data as attributes such as "@prop": "exp" is deprecated. Use "[@prop]": "exp" instead!`,
            sourceSpan, ParseErrorLevel.WARNING);
      } else {
        boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
        securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName);
        bindingType = PropertyBindingType.Property;
        if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName)) {
          this._reportError(
              `Can't bind to '${boundPropertyName}' since it isn't a known native property`,
              sourceSpan);
        }
      }
    } else {
      if (parts[0] == ATTRIBUTE_PREFIX) {
        boundPropertyName = parts[1];
        if (boundPropertyName.toLowerCase().startsWith('on')) {
          this._reportError(
              `Binding to event attribute '${boundPropertyName}' is disallowed ` +
                  `for security reasons, please use (${boundPropertyName.slice(2)})=...`,
              sourceSpan);
        }
        // NB: For security purposes, use the mapped property name, not the attribute name.
        securityContext = this._schemaRegistry.securityContext(
            elementName, this._schemaRegistry.getMappedPropName(boundPropertyName));
        let nsSeparatorIdx = boundPropertyName.indexOf(':');
        if (nsSeparatorIdx > -1) {
          let ns = boundPropertyName.substring(0, nsSeparatorIdx);
          let name = boundPropertyName.substring(nsSeparatorIdx + 1);
          boundPropertyName = mergeNsAndName(ns, name);
        }

        bindingType = PropertyBindingType.Attribute;
      } else if (parts[0] == CLASS_PREFIX) {
        boundPropertyName = parts[1];
        bindingType = PropertyBindingType.Class;
        securityContext = SecurityContext.NONE;
      } else if (parts[0] == STYLE_PREFIX) {
        unit = parts.length > 2 ? parts[2] : null;
        boundPropertyName = parts[1];
        bindingType = PropertyBindingType.Style;
        securityContext = SecurityContext.STYLE;
      } else {
        this._reportError(`Invalid property name '${name}'`, sourceSpan);
        bindingType = null;
        securityContext = null;
      }
    }

    return new BoundElementPropertyAst(
        boundPropertyName, bindingType, securityContext, ast, unit, sourceSpan);
  }


  private _findComponentDirectiveNames(directives: DirectiveAst[]): string[] {
    const componentTypeNames: string[] = [];
    directives.forEach(directive => {
      const typeName = directive.directive.type.name;
      if (directive.directive.isComponent) {
        componentTypeNames.push(typeName);
      }
    });
    return componentTypeNames;
  }

  private _assertOnlyOneComponent(directives: DirectiveAst[], sourceSpan: ParseSourceSpan) {
    const componentTypeNames = this._findComponentDirectiveNames(directives);
    if (componentTypeNames.length > 1) {
      this._reportError(`More than one component: ${componentTypeNames.join(',')}`, sourceSpan);
    }
  }

  private _assertNoComponentsNorElementBindingsOnTemplate(
      directives: DirectiveAst[], elementProps: BoundElementPropertyAst[],
      sourceSpan: ParseSourceSpan) {
    const componentTypeNames: string[] = this._findComponentDirectiveNames(directives);
    if (componentTypeNames.length > 0) {
      this._reportError(
          `Components on an embedded template: ${componentTypeNames.join(',')}`, sourceSpan);
    }
    elementProps.forEach(prop => {
      this._reportError(
          `Property binding ${prop.name} not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "directives" section.`,
          sourceSpan);
    });
  }

  private _assertAllEventsPublishedByDirectives(
      directives: DirectiveAst[], events: BoundEventAst[]) {
    const allDirectiveEvents = new Set<string>();
    directives.forEach(directive => {
      StringMapWrapper.forEach(directive.directive.outputs, (eventName: string) => {
        allDirectiveEvents.add(eventName);
      });
    });
    events.forEach(event => {
      if (isPresent(event.target) || !SetWrapper.has(allDirectiveEvents, event.name)) {
        this._reportError(
            `Event binding ${event.fullName} not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "directives" section.`,
            event.sourceSpan);
      }
    });
  }
}

class NonBindableVisitor implements HtmlAstVisitor {
  visitElement(ast: HtmlElementAst, parent: ElementContext): ElementAst {
    const preparsedElement = preparseElement(ast);
    if (preparsedElement.type === PreparsedElementType.SCRIPT ||
        preparsedElement.type === PreparsedElementType.STYLE ||
        preparsedElement.type === PreparsedElementType.STYLESHEET) {
      // Skipping <script> for security reasons
      // Skipping <style> and stylesheets as we already processed them
      // in the StyleCompiler
      return null;
    }

    const attrNameAndValues = ast.attrs.map(attrAst => [attrAst.name, attrAst.value]);
    const selector = createElementCssSelector(ast.name, attrNameAndValues);
    const ngContentIndex = parent.findNgContentIndex(selector);
    const children = htmlVisitAll(this, ast.children, EMPTY_ELEMENT_CONTEXT);
    return new ElementAst(
        ast.name, htmlVisitAll(this, ast.attrs), [], [], [], [], [], false, children,
        ngContentIndex, ast.sourceSpan);
  }
  visitComment(ast: HtmlCommentAst, context: any): any { return null; }
  visitAttr(ast: HtmlAttrAst, context: any): AttrAst {
    return new AttrAst(ast.name, ast.value, ast.sourceSpan);
  }
  visitText(ast: HtmlTextAst, parent: ElementContext): TextAst {
    const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
    return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
  }
  visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; }
  visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
}

class BoundElementOrDirectiveProperty {
  constructor(
      public name: string, public expression: AST, public isLiteral: boolean,
      public sourceSpan: ParseSourceSpan) {}
}

class ElementOrDirectiveRef {
  constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
}

export function splitClasses(classAttrValue: string): string[] {
  return StringWrapper.split(classAttrValue.trim(), /\s+/g);
}

class ElementContext {
  static create(
      isTemplateElement: boolean, directives: DirectiveAst[],
      providerContext: ProviderElementContext): ElementContext {
    const matcher = new SelectorMatcher();
    let wildcardNgContentIndex: number = null;
    const component = directives.find(directive => directive.directive.isComponent);
    if (isPresent(component)) {
      const ngContentSelectors = component.directive.template.ngContentSelectors;
      for (let i = 0; i < ngContentSelectors.length; i++) {
        const selector = ngContentSelectors[i];
        if (StringWrapper.equals(selector, '*')) {
          wildcardNgContentIndex = i;
        } else {
          matcher.addSelectables(CssSelector.parse(ngContentSelectors[i]), i);
        }
      }
    }
    return new ElementContext(isTemplateElement, matcher, wildcardNgContentIndex, providerContext);
  }
  constructor(
      public isTemplateElement: boolean, private _ngContentIndexMatcher: SelectorMatcher,
      private _wildcardNgContentIndex: number, public providerContext: ProviderElementContext) {}

  findNgContentIndex(selector: CssSelector): number {
    const ngContentIndices: number[] = [];
    this._ngContentIndexMatcher.match(
        selector, (selector, ngContentIndex) => { ngContentIndices.push(ngContentIndex); });
    ListWrapper.sort(ngContentIndices);
    if (isPresent(this._wildcardNgContentIndex)) {
      ngContentIndices.push(this._wildcardNgContentIndex);
    }
    return ngContentIndices.length > 0 ? ngContentIndices[0] : null;
  }
}

function createElementCssSelector(elementName: string, matchableAttrs: string[][]): CssSelector {
  const cssSelector = new CssSelector();
  let elNameNoNs = splitNsName(elementName)[1];

  cssSelector.setElement(elNameNoNs);

  for (let i = 0; i < matchableAttrs.length; i++) {
    let attrName = matchableAttrs[i][0];
    let attrNameNoNs = splitNsName(attrName)[1];
    let attrValue = matchableAttrs[i][1];

    cssSelector.addAttribute(attrNameNoNs, attrValue);
    if (attrName.toLowerCase() == CLASS_ATTR) {
      const classes = splitClasses(attrValue);
      classes.forEach(className => cssSelector.addClassName(className));
    }
  }
  return cssSelector;
}

const EMPTY_ELEMENT_CONTEXT = new ElementContext(true, new SelectorMatcher(), null, null);
const NON_BINDABLE_VISITOR = new NonBindableVisitor();


export class PipeCollector extends RecursiveAstVisitor {
  pipes: Set<string> = new Set<string>();
  visitPipe(ast: BindingPipe, context: any): any {
    this.pipes.add(ast.name);
    ast.exp.visit(this);
    this.visitAll(ast.args, context);
    return null;
  }
}

function removeDuplicates(items: CompileMetadataWithType[]): CompileMetadataWithType[] {
  let res: CompileMetadataWithType[] = [];
  items.forEach(item => {
    let hasMatch =
        res.filter(
               r => r.type.name == item.type.name && r.type.moduleUrl == item.type.moduleUrl &&
                   r.type.runtime == item.type.runtime)
            .length > 0;
    if (!hasMatch) {
      res.push(item);
    }
  });
  return res;
}