import {
  CompileTypeMetadata,
  CompileDirectiveMetadata,
  CompileTemplateMetadata,
  CompileProviderMetadata,
  CompileTokenMetadata
} from './compile_metadata';
import {isPresent, isBlank, isArray} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {PromiseWrapper} from 'angular2/src/facade/async';

import {XHR} from 'angular2/src/compiler/xhr';
import {UrlResolver} from 'angular2/src/compiler/url_resolver';
import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver';
import {Injectable} from 'angular2/src/core/di';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';


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

import {preparseElement, PreparsedElement, PreparsedElementType} from './template_preparser';

@Injectable()
export class DirectiveNormalizer {
  constructor(private _xhr: XHR, private _urlResolver: UrlResolver,
              private _htmlParser: HtmlParser) {}

  normalizeDirective(directive: CompileDirectiveMetadata): Promise<CompileDirectiveMetadata> {
    if (!directive.isComponent) {
      // For non components there is nothing to be normalized yet.
      return PromiseWrapper.resolve(directive);
    }
    return this.normalizeTemplate(directive.type, directive.template)
        .then((normalizedTemplate: CompileTemplateMetadata) => new CompileDirectiveMetadata({
                type: directive.type,
                isComponent: directive.isComponent,
                selector: directive.selector,
                exportAs: directive.exportAs,
                changeDetection: directive.changeDetection,
                inputs: directive.inputs,
                outputs: directive.outputs,
                hostListeners: directive.hostListeners,
                hostProperties: directive.hostProperties,
                hostAttributes: directive.hostAttributes,
                lifecycleHooks: directive.lifecycleHooks,
                providers: directive.providers,
                viewProviders: directive.viewProviders,
                queries: directive.queries,
                viewQueries: directive.viewQueries,
                template: normalizedTemplate
              }));
  }

  normalizeTemplate(directiveType: CompileTypeMetadata,
                    template: CompileTemplateMetadata): Promise<CompileTemplateMetadata> {
    if (isPresent(template.template)) {
      return PromiseWrapper.resolve(this.normalizeLoadedTemplate(
          directiveType, template, template.template, template.baseUrl));
    } else if (isPresent(template.templateUrl)) {
      var sourceAbsUrl = this._urlResolver.resolve(template.baseUrl, template.templateUrl);
      return this._xhr.get(sourceAbsUrl)
          .then(templateContent => this.normalizeLoadedTemplate(directiveType, template,
                                                                templateContent, sourceAbsUrl));
    } else {
      throw new BaseException(`No template specified for component ${directiveType.name}`);
    }
  }

  normalizeLoadedTemplate(directiveType: CompileTypeMetadata, templateMeta: CompileTemplateMetadata,
                          template: string, templateAbsUrl: string): CompileTemplateMetadata {
    var rootNodesAndErrors = this._htmlParser.parse(template, directiveType.name);
    if (rootNodesAndErrors.errors.length > 0) {
      var errorString = rootNodesAndErrors.errors.join('\n');
      throw new BaseException(`Template parse errors:\n${errorString}`);
    }

    var visitor = new TemplatePreparseVisitor();
    htmlVisitAll(visitor, rootNodesAndErrors.rootNodes);
    var allStyles = templateMeta.styles.concat(visitor.styles);

    var allStyleAbsUrls =
        visitor.styleUrls.filter(isStyleUrlResolvable)
            .map(url => this._urlResolver.resolve(templateAbsUrl, url))
            .concat(templateMeta.styleUrls.filter(isStyleUrlResolvable)
                        .map(url => this._urlResolver.resolve(templateMeta.baseUrl, url)));

    var allResolvedStyles = allStyles.map(style => {
      var styleWithImports = extractStyleUrls(this._urlResolver, templateAbsUrl, style);
      styleWithImports.styleUrls.forEach(styleUrl => allStyleAbsUrls.push(styleUrl));
      return styleWithImports.style;
    });

    var encapsulation = templateMeta.encapsulation;
    if (encapsulation === ViewEncapsulation.Emulated && allResolvedStyles.length === 0 &&
        allStyleAbsUrls.length === 0) {
      encapsulation = ViewEncapsulation.None;
    }
    return new CompileTemplateMetadata({
      encapsulation: encapsulation,
      template: template,
      templateUrl: templateAbsUrl,
      styles: allResolvedStyles,
      styleUrls: allStyleAbsUrls,
      ngContentSelectors: visitor.ngContentSelectors
    });
  }
}

class TemplatePreparseVisitor implements HtmlAstVisitor {
  ngContentSelectors: string[] = [];
  styles: string[] = [];
  styleUrls: string[] = [];
  ngNonBindableStackCount: number = 0;

  visitElement(ast: HtmlElementAst, context: any): any {
    var preparsedElement = preparseElement(ast);
    switch (preparsedElement.type) {
      case PreparsedElementType.NG_CONTENT:
        if (this.ngNonBindableStackCount === 0) {
          this.ngContentSelectors.push(preparsedElement.selectAttr);
        }
        break;
      case PreparsedElementType.STYLE:
        var textContent = '';
        ast.children.forEach(child => {
          if (child instanceof HtmlTextAst) {
            textContent += (<HtmlTextAst>child).value;
          }
        });
        this.styles.push(textContent);
        break;
      case PreparsedElementType.STYLESHEET:
        this.styleUrls.push(preparsedElement.hrefAttr);
        break;
      default:
        // DDC reports this as error. See:
        // https://github.com/dart-lang/dev_compiler/issues/428
        break;
    }
    if (preparsedElement.nonBindable) {
      this.ngNonBindableStackCount++;
    }
    htmlVisitAll(this, ast.children);
    if (preparsedElement.nonBindable) {
      this.ngNonBindableStackCount--;
    }
    return null;
  }
  visitComment(ast: HtmlCommentAst, context: any): any { return null; }
  visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
  visitText(ast: HtmlTextAst, context: any): any { return null; }
  visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }

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