/**
 * @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 {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, ElementAst, EmbeddedTemplateAst, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, ReferenceAst, SafeMethodCall, SafePropertyRead, StaticSymbol, TemplateAst, identifierName, templateVisitAll, tokenReference} from '@angular/compiler';

import {AstPath as AstPathBase} from './ast_path';
import {TemplateInfo} from './common';
import {TemplateAstChildVisitor, TemplateAstPath} from './template_path';
import {BuiltinType, CompletionKind, Definition, DiagnosticKind, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './types';
import {inSpan, spanOf} from './utils';

export interface ExpressionDiagnosticsContext { event?: boolean; }

export function getExpressionDiagnostics(
    scope: SymbolTable, ast: AST, query: SymbolQuery,
    context: ExpressionDiagnosticsContext = {}): TypeDiagnostic[] {
  const analyzer = new AstType(scope, query, context);
  analyzer.getDiagnostics(ast);
  return analyzer.diagnostics;
}

export function getExpressionCompletions(
    scope: SymbolTable, ast: AST, position: number, query: SymbolQuery): Symbol[]|undefined {
  const path = new AstPath(ast, position);
  if (path.empty) return undefined;
  const tail = path.tail !;
  let result: SymbolTable|undefined = scope;

  function getType(ast: AST): Symbol { return new AstType(scope, query, {}).getType(ast); }

  // If the completion request is in a not in a pipe or property access then the global scope
  // (that is the scope of the implicit receiver) is the right scope as the user is typing the
  // beginning of an expression.
  tail.visit({
    visitBinary(ast) {},
    visitChain(ast) {},
    visitConditional(ast) {},
    visitFunctionCall(ast) {},
    visitImplicitReceiver(ast) {},
    visitInterpolation(ast) { result = undefined; },
    visitKeyedRead(ast) {},
    visitKeyedWrite(ast) {},
    visitLiteralArray(ast) {},
    visitLiteralMap(ast) {},
    visitLiteralPrimitive(ast) {},
    visitMethodCall(ast) {},
    visitPipe(ast) {
      if (position >= ast.exp.span.end &&
          (!ast.args || !ast.args.length || position < (<AST>ast.args[0]).span.start)) {
        // We are in a position a pipe name is expected.
        result = query.getPipes();
      }
    },
    visitPrefixNot(ast) {},
    visitPropertyRead(ast) {
      const receiverType = getType(ast.receiver);
      result = receiverType ? receiverType.members() : scope;
    },
    visitPropertyWrite(ast) {
      const receiverType = getType(ast.receiver);
      result = receiverType ? receiverType.members() : scope;
    },
    visitQuote(ast) {
      // For a quote, return the members of any (if there are any).
      result = query.getBuiltinType(BuiltinType.Any).members();
    },
    visitSafeMethodCall(ast) {
      const receiverType = getType(ast.receiver);
      result = receiverType ? receiverType.members() : scope;
    },
    visitSafePropertyRead(ast) {
      const receiverType = getType(ast.receiver);
      result = receiverType ? receiverType.members() : scope;
    },
  });

  return result && result.values();
}

export function getExpressionSymbol(
    scope: SymbolTable, ast: AST, position: number,
    query: SymbolQuery): {symbol: Symbol, span: Span}|undefined {
  const path = new AstPath(ast, position, /* excludeEmpty */ true);
  if (path.empty) return undefined;
  const tail = path.tail !;

  function getType(ast: AST): Symbol { return new AstType(scope, query, {}).getType(ast); }

  let symbol: Symbol|undefined = undefined;
  let span: Span|undefined = undefined;

  // If the completion request is in a not in a pipe or property access then the global scope
  // (that is the scope of the implicit receiver) is the right scope as the user is typing the
  // beginning of an expression.
  tail.visit({
    visitBinary(ast) {},
    visitChain(ast) {},
    visitConditional(ast) {},
    visitFunctionCall(ast) {},
    visitImplicitReceiver(ast) {},
    visitInterpolation(ast) {},
    visitKeyedRead(ast) {},
    visitKeyedWrite(ast) {},
    visitLiteralArray(ast) {},
    visitLiteralMap(ast) {},
    visitLiteralPrimitive(ast) {},
    visitMethodCall(ast) {
      const receiverType = getType(ast.receiver);
      symbol = receiverType && receiverType.members().get(ast.name);
      span = ast.span;
    },
    visitPipe(ast) {
      if (position >= ast.exp.span.end &&
          (!ast.args || !ast.args.length || position < (<AST>ast.args[0]).span.start)) {
        // We are in a position a pipe name is expected.
        const pipes = query.getPipes();
        if (pipes) {
          symbol = pipes.get(ast.name);
          span = ast.span;
        }
      }
    },
    visitPrefixNot(ast) {},
    visitPropertyRead(ast) {
      const receiverType = getType(ast.receiver);
      symbol = receiverType && receiverType.members().get(ast.name);
      span = ast.span;
    },
    visitPropertyWrite(ast) {
      const receiverType = getType(ast.receiver);
      symbol = receiverType && receiverType.members().get(ast.name);
      span = ast.span;
    },
    visitQuote(ast) {},
    visitSafeMethodCall(ast) {
      const receiverType = getType(ast.receiver);
      symbol = receiverType && receiverType.members().get(ast.name);
      span = ast.span;
    },
    visitSafePropertyRead(ast) {
      const receiverType = getType(ast.receiver);
      symbol = receiverType && receiverType.members().get(ast.name);
      span = ast.span;
    },
  });

  if (symbol && span) {
    return {symbol, span};
  }
}

interface ExpressionVisitor extends AstVisitor {
  visit?(ast: AST, context?: any): any;
}


// Consider moving to expression_parser/ast
class NullVisitor implements ExpressionVisitor {
  visitBinary(ast: Binary): void {}
  visitChain(ast: Chain): void {}
  visitConditional(ast: Conditional): void {}
  visitFunctionCall(ast: FunctionCall): void {}
  visitImplicitReceiver(ast: ImplicitReceiver): void {}
  visitInterpolation(ast: Interpolation): void {}
  visitKeyedRead(ast: KeyedRead): void {}
  visitKeyedWrite(ast: KeyedWrite): void {}
  visitLiteralArray(ast: LiteralArray): void {}
  visitLiteralMap(ast: LiteralMap): void {}
  visitLiteralPrimitive(ast: LiteralPrimitive): void {}
  visitMethodCall(ast: MethodCall): void {}
  visitPipe(ast: BindingPipe): void {}
  visitPrefixNot(ast: PrefixNot): void {}
  visitPropertyRead(ast: PropertyRead): void {}
  visitPropertyWrite(ast: PropertyWrite): void {}
  visitQuote(ast: Quote): void {}
  visitSafeMethodCall(ast: SafeMethodCall): void {}
  visitSafePropertyRead(ast: SafePropertyRead): void {}
}

export class TypeDiagnostic {
  constructor(public kind: DiagnosticKind, public message: string, public ast: AST) {}
}

// AstType calculatetype of the ast given AST element.
class AstType implements ExpressionVisitor {
  public diagnostics: TypeDiagnostic[];

  constructor(
      private scope: SymbolTable, private query: SymbolQuery,
      private context: ExpressionDiagnosticsContext) {}

  getType(ast: AST): Symbol { return ast.visit(this); }

  getDiagnostics(ast: AST): TypeDiagnostic[] {
    this.diagnostics = [];
    const type: Symbol = ast.visit(this);
    if (this.context.event && type.callable) {
      this.reportWarning('Unexpected callable expression. Expected a method call', ast);
    }
    return this.diagnostics;
  }

  visitBinary(ast: Binary): Symbol {
    // Treat undefined and null as other.
    function normalize(kind: BuiltinType, other: BuiltinType): BuiltinType {
      switch (kind) {
        case BuiltinType.Undefined:
        case BuiltinType.Null:
          return normalize(other, BuiltinType.Other);
      }
      return kind;
    }

    const leftType = this.getType(ast.left);
    const rightType = this.getType(ast.right);
    const leftRawKind = this.query.getTypeKind(leftType);
    const rightRawKind = this.query.getTypeKind(rightType);
    const leftKind = normalize(leftRawKind, rightRawKind);
    const rightKind = normalize(rightRawKind, leftRawKind);

    // The following swtich implements operator typing similar to the
    // type production tables in the TypeScript specification.
    // https://github.com/Microsoft/TypeScript/blob/v1.8.10/doc/spec.md#4.19
    const operKind = leftKind << 8 | rightKind;
    switch (ast.operation) {
      case '*':
      case '/':
      case '%':
      case '-':
      case '<<':
      case '>>':
      case '>>>':
      case '&':
      case '^':
      case '|':
        switch (operKind) {
          case BuiltinType.Any << 8 | BuiltinType.Any:
          case BuiltinType.Number << 8 | BuiltinType.Any:
          case BuiltinType.Any << 8 | BuiltinType.Number:
          case BuiltinType.Number << 8 | BuiltinType.Number:
            return this.query.getBuiltinType(BuiltinType.Number);
          default:
            let errorAst = ast.left;
            switch (leftKind) {
              case BuiltinType.Any:
              case BuiltinType.Number:
                errorAst = ast.right;
                break;
            }
            return this.reportError('Expected a numeric type', errorAst);
        }
      case '+':
        switch (operKind) {
          case BuiltinType.Any << 8 | BuiltinType.Any:
          case BuiltinType.Any << 8 | BuiltinType.Boolean:
          case BuiltinType.Any << 8 | BuiltinType.Number:
          case BuiltinType.Any << 8 | BuiltinType.Other:
          case BuiltinType.Boolean << 8 | BuiltinType.Any:
          case BuiltinType.Number << 8 | BuiltinType.Any:
          case BuiltinType.Other << 8 | BuiltinType.Any:
            return this.anyType;
          case BuiltinType.Any << 8 | BuiltinType.String:
          case BuiltinType.Boolean << 8 | BuiltinType.String:
          case BuiltinType.Number << 8 | BuiltinType.String:
          case BuiltinType.String << 8 | BuiltinType.Any:
          case BuiltinType.String << 8 | BuiltinType.Boolean:
          case BuiltinType.String << 8 | BuiltinType.Number:
          case BuiltinType.String << 8 | BuiltinType.String:
          case BuiltinType.String << 8 | BuiltinType.Other:
          case BuiltinType.Other << 8 | BuiltinType.String:
            return this.query.getBuiltinType(BuiltinType.String);
          case BuiltinType.Number << 8 | BuiltinType.Number:
            return this.query.getBuiltinType(BuiltinType.Number);
          case BuiltinType.Boolean << 8 | BuiltinType.Number:
          case BuiltinType.Other << 8 | BuiltinType.Number:
            return this.reportError('Expected a number type', ast.left);
          case BuiltinType.Number << 8 | BuiltinType.Boolean:
          case BuiltinType.Number << 8 | BuiltinType.Other:
            return this.reportError('Expected a number type', ast.right);
          default:
            return this.reportError('Expected operands to be a string or number type', ast);
        }
      case '>':
      case '<':
      case '<=':
      case '>=':
      case '==':
      case '!=':
      case '===':
      case '!==':
        switch (operKind) {
          case BuiltinType.Any << 8 | BuiltinType.Any:
          case BuiltinType.Any << 8 | BuiltinType.Boolean:
          case BuiltinType.Any << 8 | BuiltinType.Number:
          case BuiltinType.Any << 8 | BuiltinType.String:
          case BuiltinType.Any << 8 | BuiltinType.Other:
          case BuiltinType.Boolean << 8 | BuiltinType.Any:
          case BuiltinType.Boolean << 8 | BuiltinType.Boolean:
          case BuiltinType.Number << 8 | BuiltinType.Any:
          case BuiltinType.Number << 8 | BuiltinType.Number:
          case BuiltinType.String << 8 | BuiltinType.Any:
          case BuiltinType.String << 8 | BuiltinType.String:
          case BuiltinType.Other << 8 | BuiltinType.Any:
          case BuiltinType.Other << 8 | BuiltinType.Other:
            return this.query.getBuiltinType(BuiltinType.Boolean);
          default:
            return this.reportError('Expected the operants to be of similar type or any', ast);
        }
      case '&&':
        return rightType;
      case '||':
        return this.query.getTypeUnion(leftType, rightType);
    }

    return this.reportError(`Unrecognized operator ${ast.operation}`, ast);
  }

  visitChain(ast: Chain) {
    if (this.diagnostics) {
      // If we are producing diagnostics, visit the children
      visitChildren(ast, this);
    }
    // The type of a chain is always undefined.
    return this.query.getBuiltinType(BuiltinType.Undefined);
  }

  visitConditional(ast: Conditional) {
    // The type of a conditional is the union of the true and false conditions.
    return this.query.getTypeUnion(this.getType(ast.trueExp), this.getType(ast.falseExp));
  }

  visitFunctionCall(ast: FunctionCall) {
    // The type of a function call is the return type of the selected signature.
    // The signature is selected based on the types of the arguments. Angular doesn't
    // support contextual typing of arguments so this is simpler than TypeScript's
    // version.
    const args = ast.args.map(arg => this.getType(arg));
    const target = this.getType(ast.target !);
    if (!target || !target.callable) return this.reportError('Call target is not callable', ast);
    const signature = target.selectSignature(args);
    if (signature) return signature.result;
    // TODO: Consider a better error message here.
    return this.reportError('Unable no compatible signature found for call', ast);
  }

  visitImplicitReceiver(ast: ImplicitReceiver): Symbol {
    const _this = this;
    // Return a pseudo-symbol for the implicit receiver.
    // The members of the implicit receiver are what is defined by the
    // scope passed into this class.
    return {
      name: '$implict',
      kind: 'component',
      language: 'ng-template',
      type: undefined,
      container: undefined,
      callable: false,
      public: true,
      definition: undefined,
      members(): SymbolTable{return _this.scope;},
      signatures(): Signature[]{return [];},
      selectSignature(types): Signature | undefined{return undefined;},
      indexed(argument): Symbol | undefined{return undefined;}
    };
  }

  visitInterpolation(ast: Interpolation): Symbol {
    // If we are producing diagnostics, visit the children.
    if (this.diagnostics) {
      visitChildren(ast, this);
    }
    return this.undefinedType;
  }

  visitKeyedRead(ast: KeyedRead): Symbol {
    const targetType = this.getType(ast.obj);
    const keyType = this.getType(ast.key);
    const result = targetType.indexed(keyType);
    return result || this.anyType;
  }

  visitKeyedWrite(ast: KeyedWrite): Symbol {
    // The write of a type is the type of the value being written.
    return this.getType(ast.value);
  }

  visitLiteralArray(ast: LiteralArray): Symbol {
    // A type literal is an array type of the union of the elements
    return this.query.getArrayType(
        this.query.getTypeUnion(...ast.expressions.map(element => this.getType(element))));
  }

  visitLiteralMap(ast: LiteralMap): Symbol {
    // If we are producing diagnostics, visit the children
    if (this.diagnostics) {
      visitChildren(ast, this);
    }
    // TODO: Return a composite type.
    return this.anyType;
  }

  visitLiteralPrimitive(ast: LiteralPrimitive) {
    // The type of a literal primitive depends on the value of the literal.
    switch (ast.value) {
      case true:
      case false:
        return this.query.getBuiltinType(BuiltinType.Boolean);
      case null:
        return this.query.getBuiltinType(BuiltinType.Null);
      case undefined:
        return this.query.getBuiltinType(BuiltinType.Undefined);
      default:
        switch (typeof ast.value) {
          case 'string':
            return this.query.getBuiltinType(BuiltinType.String);
          case 'number':
            return this.query.getBuiltinType(BuiltinType.Number);
          default:
            return this.reportError('Unrecognized primitive', ast);
        }
    }
  }

  visitMethodCall(ast: MethodCall) {
    return this.resolveMethodCall(this.getType(ast.receiver), ast);
  }

  visitPipe(ast: BindingPipe) {
    // The type of a pipe node is the return type of the pipe's transform method. The table returned
    // by getPipes() is expected to contain symbols with the corresponding transform method type.
    const pipe = this.query.getPipes().get(ast.name);
    if (!pipe) return this.reportError(`No pipe by the name ${ast.name} found`, ast);
    const expType = this.getType(ast.exp);
    const signature =
        pipe.selectSignature([expType].concat(ast.args.map(arg => this.getType(arg))));
    if (!signature) return this.reportError('Unable to resolve signature for pipe invocation', ast);
    return signature.result;
  }

  visitPrefixNot(ast: PrefixNot) {
    // The type of a prefix ! is always boolean.
    return this.query.getBuiltinType(BuiltinType.Boolean);
  }

  visitPropertyRead(ast: PropertyRead) {
    return this.resolvePropertyRead(this.getType(ast.receiver), ast);
  }

  visitPropertyWrite(ast: PropertyWrite) {
    // The type of a write is the type of the value being written.
    return this.getType(ast.value);
  }

  visitQuote(ast: Quote) {
    // The type of a quoted expression is any.
    return this.query.getBuiltinType(BuiltinType.Any);
  }

  visitSafeMethodCall(ast: SafeMethodCall) {
    return this.resolveMethodCall(this.query.getNonNullableType(this.getType(ast.receiver)), ast);
  }

  visitSafePropertyRead(ast: SafePropertyRead) {
    return this.resolvePropertyRead(this.query.getNonNullableType(this.getType(ast.receiver)), ast);
  }

  private _anyType: Symbol;
  private get anyType(): Symbol {
    let result = this._anyType;
    if (!result) {
      result = this._anyType = this.query.getBuiltinType(BuiltinType.Any);
    }
    return result;
  }

  private _undefinedType: Symbol;
  private get undefinedType(): Symbol {
    let result = this._undefinedType;
    if (!result) {
      result = this._undefinedType = this.query.getBuiltinType(BuiltinType.Undefined);
    }
    return result;
  }

  private resolveMethodCall(receiverType: Symbol, ast: SafeMethodCall|MethodCall) {
    if (this.isAny(receiverType)) {
      return this.anyType;
    }

    // The type of a method is the selected methods result type.
    const method = receiverType.members().get(ast.name);
    if (!method) return this.reportError(`Unknown method ${ast.name}`, ast);
    if (!method.type) return this.reportError(`Could not find a type for ${ast.name}`, ast);
    if (!method.type.callable) return this.reportError(`Member ${ast.name} is not callable`, ast);
    const signature = method.type.selectSignature(ast.args.map(arg => this.getType(arg)));
    if (!signature)
      return this.reportError(`Unable to resolve signature for call of method ${ast.name}`, ast);
    return signature.result;
  }

  private resolvePropertyRead(receiverType: Symbol, ast: SafePropertyRead|PropertyRead) {
    if (this.isAny(receiverType)) {
      return this.anyType;
    }

    // The type of a property read is the seelcted member's type.
    const member = receiverType.members().get(ast.name);
    if (!member) {
      let receiverInfo = receiverType.name;
      if (receiverInfo == '$implict') {
        receiverInfo =
            'The component declaration, template variable declarations, and element references do';
      } else {
        receiverInfo = `'${receiverInfo}' does`;
      }
      return this.reportError(
          `Identifier '${ast.name}' is not defined. ${receiverInfo} not contain such a member`,
          ast);
    }
    if (!member.public) {
      let receiverInfo = receiverType.name;
      if (receiverInfo == '$implict') {
        receiverInfo = 'the component';
      } else {
        receiverInfo = `'${receiverInfo}'`;
      }
      this.reportWarning(
          `Identifier '${ast.name}' refers to a private member of ${receiverInfo}`, ast);
    }
    return member.type;
  }

  private reportError(message: string, ast: AST): Symbol {
    if (this.diagnostics) {
      this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Error, message, ast));
    }
    return this.anyType;
  }

  private reportWarning(message: string, ast: AST): Symbol {
    if (this.diagnostics) {
      this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Warning, message, ast));
    }
    return this.anyType;
  }

  private isAny(symbol: Symbol): boolean {
    return !symbol || this.query.getTypeKind(symbol) == BuiltinType.Any ||
        (!!symbol.type && this.isAny(symbol.type));
  }
}

class AstPath extends AstPathBase<AST> {
  constructor(ast: AST, public position: number, excludeEmpty: boolean = false) {
    super(new AstPathVisitor(position, excludeEmpty).buildPath(ast).path);
  }
}

class AstPathVisitor extends NullVisitor {
  public path: AST[] = [];

  constructor(private position: number, private excludeEmpty: boolean) { super(); }

  visit(ast: AST) {
    if ((!this.excludeEmpty || ast.span.start < ast.span.end) && inSpan(this.position, ast.span)) {
      this.path.push(ast);
      visitChildren(ast, this);
    }
  }

  buildPath(ast: AST): AstPathVisitor {
    // We never care about the ASTWithSource node and its visit() method calls its ast's visit so
    // the visit() method above would never see it.
    if (ast instanceof ASTWithSource) {
      ast = ast.ast;
    }
    this.visit(ast);
    return this;
  }
}

// TODO: Consider moving to expression_parser/ast
function visitChildren(ast: AST, visitor: ExpressionVisitor) {
  function visit(ast: AST) { visitor.visit && visitor.visit(ast) || ast.visit(visitor); }

  function visitAll<T extends AST>(asts: T[]) { asts.forEach(visit); }

  ast.visit({
    visitBinary(ast) {
      visit(ast.left);
      visit(ast.right);
    },
    visitChain(ast) { visitAll(ast.expressions); },
    visitConditional(ast) {
      visit(ast.condition);
      visit(ast.trueExp);
      visit(ast.falseExp);
    },
    visitFunctionCall(ast) {
      if (ast.target) {
        visit(ast.target);
      }
      visitAll(ast.args);
    },
    visitImplicitReceiver(ast) {},
    visitInterpolation(ast) { visitAll(ast.expressions); },
    visitKeyedRead(ast) {
      visit(ast.obj);
      visit(ast.key);
    },
    visitKeyedWrite(ast) {
      visit(ast.obj);
      visit(ast.key);
      visit(ast.obj);
    },
    visitLiteralArray(ast) { visitAll(ast.expressions); },
    visitLiteralMap(ast) {},
    visitLiteralPrimitive(ast) {},
    visitMethodCall(ast) {
      visit(ast.receiver);
      visitAll(ast.args);
    },
    visitPipe(ast) {
      visit(ast.exp);
      visitAll(ast.args);
    },
    visitPrefixNot(ast) { visit(ast.expression); },
    visitPropertyRead(ast) { visit(ast.receiver); },
    visitPropertyWrite(ast) {
      visit(ast.receiver);
      visit(ast.value);
    },
    visitQuote(ast) {},
    visitSafeMethodCall(ast) {
      visit(ast.receiver);
      visitAll(ast.args);
    },
    visitSafePropertyRead(ast) { visit(ast.receiver); },
  });
}

export function getExpressionScope(
    info: TemplateInfo, path: TemplateAstPath, includeEvent: boolean): SymbolTable {
  let result = info.template.members;
  const references = getReferences(info);
  const variables = getVarDeclarations(info, path);
  const events = getEventDeclaration(info, path, includeEvent);
  if (references.length || variables.length || events.length) {
    const referenceTable = info.template.query.createSymbolTable(references);
    const variableTable = info.template.query.createSymbolTable(variables);
    const eventsTable = info.template.query.createSymbolTable(events);
    result =
        info.template.query.mergeSymbolTable([result, referenceTable, variableTable, eventsTable]);
  }
  return result;
}

function getEventDeclaration(info: TemplateInfo, path: TemplateAstPath, includeEvent?: boolean) {
  let result: SymbolDeclaration[] = [];
  if (includeEvent) {
    // TODO: Determine the type of the event parameter based on the Observable<T> or EventEmitter<T>
    // of the event.
    result = [{
      name: '$event',
      kind: 'variable',
      type: info.template.query.getBuiltinType(BuiltinType.Any)
    }];
  }
  return result;
}

function getReferences(info: TemplateInfo): SymbolDeclaration[] {
  const result: SymbolDeclaration[] = [];

  function processReferences(references: ReferenceAst[]) {
    for (const reference of references) {
      let type: Symbol|undefined = undefined;
      if (reference.value) {
        type = info.template.query.getTypeSymbol(tokenReference(reference.value));
      }
      result.push({
        name: reference.name,
        kind: 'reference',
        type: type || info.template.query.getBuiltinType(BuiltinType.Any),
        get definition() { return getDefintionOf(info, reference); }
      });
    }
  }

  const visitor = new class extends TemplateAstChildVisitor {
    visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
      super.visitEmbeddedTemplate(ast, context);
      processReferences(ast.references);
    }
    visitElement(ast: ElementAst, context: any): any {
      super.visitElement(ast, context);
      processReferences(ast.references);
    }
  };

  templateVisitAll(visitor, info.templateAst);

  return result;
}

function getVarDeclarations(info: TemplateInfo, path: TemplateAstPath): SymbolDeclaration[] {
  const result: SymbolDeclaration[] = [];

  let current = path.tail;
  while (current) {
    if (current instanceof EmbeddedTemplateAst) {
      for (const variable of current.variables) {
        const name = variable.name;

        // Find the first directive with a context.
        const context =
            current.directives
                .map(d => info.template.query.getTemplateContext(d.directive.type.reference))
                .find(c => !!c);

        // Determine the type of the context field referenced by variable.value.
        let type: Symbol|undefined = undefined;
        if (context) {
          const value = context.get(variable.value);
          if (value) {
            type = value.type !;
            let kind = info.template.query.getTypeKind(type);
            if (kind === BuiltinType.Any || kind == BuiltinType.Unbound) {
              // The any type is not very useful here. For special cases, such as ngFor, we can do
              // better.
              type = refinedVariableType(type, info, current);
            }
          }
        }
        if (!type) {
          type = info.template.query.getBuiltinType(BuiltinType.Any);
        }
        result.push({
          name,
          kind: 'variable', type, get definition() { return getDefintionOf(info, variable); }
        });
      }
    }
    current = path.parentOf(current);
  }

  return result;
}

function refinedVariableType(
    type: Symbol, info: TemplateInfo, templateElement: EmbeddedTemplateAst): Symbol {
  // Special case the ngFor directive
  const ngForDirective = templateElement.directives.find(d => {
    const name = identifierName(d.directive.type);
    return name == 'NgFor' || name == 'NgForOf';
  });
  if (ngForDirective) {
    const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf');
    if (ngForOfBinding) {
      const bindingType =
          new AstType(info.template.members, info.template.query, {}).getType(ngForOfBinding.value);
      if (bindingType) {
        const result = info.template.query.getElementType(bindingType);
        if (result) {
          return result;
        }
      }
    }
  }

  // We can't do better, just return the original type.
  return type;
}

function getDefintionOf(info: TemplateInfo, ast: TemplateAst): Definition|undefined {
  if (info.fileName) {
    const templateOffset = info.template.span.start;
    return [{
      fileName: info.fileName,
      span: {
        start: ast.sourceSpan.start.offset + templateOffset,
        end: ast.sourceSpan.end.offset + templateOffset
      }
    }];
  }
}
