/**
 * @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 {ApplicationRef} from '../application_ref';
import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection';
import {Injector, THROW_IF_NOT_FOUND} from '../di/injector';
import {isPresent} from '../facade/lang';
import {WtfScopeFn, wtfCreateScope, wtfLeave} from '../profile/profile';
import {DirectRenderer, RenderComponentType, RenderDebugInfo, Renderer} from '../render/api';

import {AnimationViewContext} from './animation_view_context';
import {ComponentRef} from './component_factory';
import {DebugContext, StaticNodeDebugInfo} from './debug_context';
import {ElementInjector} from './element_injector';
import {ExpressionChangedAfterItHasBeenCheckedError, ViewDestroyedError, ViewWrappedError} from './errors';
import {ViewContainer} from './view_container';
import {ViewRef_} from './view_ref';
import {ViewType} from './view_type';
import {ViewUtils, addToArray} from './view_utils';

const _scope_check: WtfScopeFn = wtfCreateScope(`AppView#check(ascii id)`);

/**
 * @experimental
 */
const EMPTY_CONTEXT = new Object();

const UNDEFINED = new Object();

/**
 * Cost of making objects: http://jsperf.com/instantiate-size-of-object
 *
 */
export abstract class AppView<T> {
  ref: ViewRef_<T>;
  lastRootNode: any;
  allNodes: any[];
  disposables: Function[];
  viewContainer: ViewContainer;
  // This will be set if a view is directly attached to an ApplicationRef
  // and not to a view container.
  appRef: ApplicationRef;

  numberOfChecks: number = 0;

  renderer: Renderer;

  private _hasExternalHostElement: boolean;
  private _hostInjector: Injector;
  private _hostProjectableNodes: any[][];
  private _animationContext: AnimationViewContext;
  private _directRenderer: DirectRenderer;

  public context: T;

  constructor(
      public clazz: any, public componentType: RenderComponentType, public type: ViewType,
      public viewUtils: ViewUtils, public parentView: AppView<any>, public parentIndex: number,
      public parentElement: any, public cdMode: ChangeDetectorStatus,
      public declaredViewContainer: ViewContainer = null) {
    this.ref = new ViewRef_(this, viewUtils.animationQueue);
    if (type === ViewType.COMPONENT || type === ViewType.HOST) {
      this.renderer = viewUtils.renderComponent(componentType);
    } else {
      this.renderer = parentView.renderer;
    }
    this._directRenderer = (this.renderer as any).directRenderer;
  }

  get animationContext(): AnimationViewContext {
    if (!this._animationContext) {
      this._animationContext = new AnimationViewContext(this.viewUtils.animationQueue);
    }
    return this._animationContext;
  }

  get destroyed(): boolean { return this.cdMode === ChangeDetectorStatus.Destroyed; }

  create(context: T) {
    this.context = context;
    return this.createInternal(null);
  }

  createHostView(rootSelectorOrNode: string|any, hostInjector: Injector, projectableNodes: any[][]):
      ComponentRef<any> {
    this.context = <any>EMPTY_CONTEXT;
    this._hasExternalHostElement = isPresent(rootSelectorOrNode);
    this._hostInjector = hostInjector;
    this._hostProjectableNodes = projectableNodes;
    return this.createInternal(rootSelectorOrNode);
  }

  /**
   * Overwritten by implementations.
   * Returns the ComponentRef for the host element for ViewType.HOST.
   */
  createInternal(rootSelectorOrNode: string|any): ComponentRef<any> { return null; }

  /**
   * Overwritten by implementations.
   */
  createEmbeddedViewInternal(templateNodeIndex: number): AppView<any> { return null; }

  init(lastRootNode: any, allNodes: any[], disposables: Function[]) {
    this.lastRootNode = lastRootNode;
    this.allNodes = allNodes;
    this.disposables = disposables;
    if (this.type === ViewType.COMPONENT) {
      this.dirtyParentQueriesInternal();
    }
  }

  injectorGet(token: any, nodeIndex: number, notFoundValue: any = THROW_IF_NOT_FOUND): any {
    let result = UNDEFINED;
    let view: AppView<any> = this;
    while (result === UNDEFINED) {
      if (isPresent(nodeIndex)) {
        result = view.injectorGetInternal(token, nodeIndex, UNDEFINED);
      }
      if (result === UNDEFINED && view.type === ViewType.HOST) {
        result = view._hostInjector.get(token, notFoundValue);
      }
      nodeIndex = view.parentIndex;
      view = view.parentView;
    }
    return result;
  }

  /**
   * Overwritten by implementations
   */
  injectorGetInternal(token: any, nodeIndex: number, notFoundResult: any): any {
    return notFoundResult;
  }

  injector(nodeIndex: number): Injector { return new ElementInjector(this, nodeIndex); }

  detachAndDestroy() {
    if (this.viewContainer) {
      this.viewContainer.detachView(this.viewContainer.nestedViews.indexOf(this));
    } else if (this.appRef) {
      this.appRef.detachView(this.ref);
    } else if (this._hasExternalHostElement) {
      this.detach();
    }
    this.destroy();
  }

  destroy() {
    if (this.cdMode === ChangeDetectorStatus.Destroyed) {
      return;
    }
    const hostElement = this.type === ViewType.COMPONENT ? this.parentElement : null;
    if (this.disposables) {
      for (let i = 0; i < this.disposables.length; i++) {
        this.disposables[i]();
      }
    }
    this.destroyInternal();
    this.dirtyParentQueriesInternal();

    if (this._animationContext) {
      this._animationContext.onAllActiveAnimationsDone(
          () => this.renderer.destroyView(hostElement, this.allNodes));
    } else {
      this.renderer.destroyView(hostElement, this.allNodes);
    }

    this.cdMode = ChangeDetectorStatus.Destroyed;
  }

  /**
   * Overwritten by implementations
   */
  destroyInternal(): void {}

  /**
   * Overwritten by implementations
   */
  detachInternal(): void {}

  detach(): void {
    this.detachInternal();
    if (this._animationContext) {
      this._animationContext.onAllActiveAnimationsDone(() => this._renderDetach());
    } else {
      this._renderDetach();
    }
    if (this.declaredViewContainer && this.declaredViewContainer !== this.viewContainer &&
        this.declaredViewContainer.projectedViews) {
      const projectedViews = this.declaredViewContainer.projectedViews;
      const index = projectedViews.indexOf(this);
      // perf: pop is faster than splice!
      if (index >= projectedViews.length - 1) {
        projectedViews.pop();
      } else {
        projectedViews.splice(index, 1);
      }
    }
    this.appRef = null;
    this.viewContainer = null;
    this.dirtyParentQueriesInternal();
  }

  private _renderDetach() {
    if (this._directRenderer) {
      this.visitRootNodesInternal(this._directRenderer.remove, null);
    } else {
      this.renderer.detachView(this.flatRootNodes);
    }
  }

  attachToAppRef(appRef: ApplicationRef) {
    if (this.viewContainer) {
      throw new Error('This view is already attached to a ViewContainer!');
    }
    this.appRef = appRef;
    this.dirtyParentQueriesInternal();
  }

  attachAfter(viewContainer: ViewContainer, prevView: AppView<any>) {
    if (this.appRef) {
      throw new Error('This view is already attached directly to the ApplicationRef!');
    }
    this._renderAttach(viewContainer, prevView);
    this.viewContainer = viewContainer;
    if (this.declaredViewContainer && this.declaredViewContainer !== viewContainer) {
      if (!this.declaredViewContainer.projectedViews) {
        this.declaredViewContainer.projectedViews = [];
      }
      this.declaredViewContainer.projectedViews.push(this);
    }
    this.dirtyParentQueriesInternal();
  }

  moveAfter(viewContainer: ViewContainer, prevView: AppView<any>) {
    this._renderAttach(viewContainer, prevView);
    this.dirtyParentQueriesInternal();
  }

  private _renderAttach(viewContainer: ViewContainer, prevView: AppView<any>) {
    const prevNode = prevView ? prevView.lastRootNode : viewContainer.nativeElement;
    if (this._directRenderer) {
      const nextSibling = this._directRenderer.nextSibling(prevNode);
      if (nextSibling) {
        this.visitRootNodesInternal(this._directRenderer.insertBefore, nextSibling);
      } else {
        const parentElement = this._directRenderer.parentElement(prevNode);
        if (parentElement) {
          this.visitRootNodesInternal(this._directRenderer.appendChild, parentElement);
        }
      }
    } else {
      this.renderer.attachViewAfter(prevNode, this.flatRootNodes);
    }
  }

  get changeDetectorRef(): ChangeDetectorRef { return this.ref; }

  get flatRootNodes(): any[] {
    const nodes: any[] = [];
    this.visitRootNodesInternal(addToArray, nodes);
    return nodes;
  }

  projectNodes(parentElement: any, ngContentIndex: number) {
    if (this._directRenderer) {
      this.visitProjectedNodes(ngContentIndex, this._directRenderer.appendChild, parentElement);
    } else {
      const nodes: any[] = [];
      this.visitProjectedNodes(ngContentIndex, addToArray, nodes);
      this.renderer.projectNodes(parentElement, nodes);
    }
  }

  visitProjectedNodes<C>(ngContentIndex: number, cb: (node: any, ctx: C) => void, c: C): void {
    switch (this.type) {
      case ViewType.EMBEDDED:
        this.parentView.visitProjectedNodes(ngContentIndex, cb, c);
        break;
      case ViewType.COMPONENT:
        if (this.parentView.type === ViewType.HOST) {
          const nodes = this.parentView._hostProjectableNodes[ngContentIndex] || [];
          for (let i = 0; i < nodes.length; i++) {
            cb(nodes[i], c);
          }
        } else {
          this.parentView.visitProjectableNodesInternal(this.parentIndex, ngContentIndex, cb, c);
        }
        break;
    }
  }

  /**
   * Overwritten by implementations
   */
  visitRootNodesInternal<C>(cb: (node: any, ctx: C) => void, c: C): void {}

  /**
   * Overwritten by implementations
   */
  visitProjectableNodesInternal<C>(
      nodeIndex: number, ngContentIndex: number, cb: (node: any, ctx: C) => void, c: C): void {}

  /**
   * Overwritten by implementations
   */
  dirtyParentQueriesInternal(): void {}

  internalDetectChanges(throwOnChange: boolean): void {
    if (this.cdMode !== ChangeDetectorStatus.Detached) {
      this.detectChanges(throwOnChange);
    }
  }

  detectChanges(throwOnChange: boolean): void {
    const s = _scope_check(this.clazz);
    if (this.cdMode === ChangeDetectorStatus.Checked ||
        this.cdMode === ChangeDetectorStatus.Errored)
      return;
    if (this.cdMode === ChangeDetectorStatus.Destroyed) {
      this.throwDestroyedError('detectChanges');
    }
    this.detectChangesInternal(throwOnChange);
    if (this.cdMode === ChangeDetectorStatus.CheckOnce) this.cdMode = ChangeDetectorStatus.Checked;

    this.numberOfChecks++;
    wtfLeave(s);
  }

  /**
   * Overwritten by implementations
   */
  detectChangesInternal(throwOnChange: boolean): void {}

  markAsCheckOnce(): void { this.cdMode = ChangeDetectorStatus.CheckOnce; }

  markPathToRootAsCheckOnce(): void {
    let c: AppView<any> = this;
    while (isPresent(c) && c.cdMode !== ChangeDetectorStatus.Detached) {
      if (c.cdMode === ChangeDetectorStatus.Checked) {
        c.cdMode = ChangeDetectorStatus.CheckOnce;
      }
      if (c.type === ViewType.COMPONENT) {
        c = c.parentView;
      } else {
        c = c.viewContainer ? c.viewContainer.parentView : null;
      }
    }
  }

  eventHandler<E, R>(cb: (eventName: string, event?: E) => R): (eventName: string, event?: E) => R {
    return cb;
  }

  throwDestroyedError(details: string): void { throw new ViewDestroyedError(details); }
}

export class DebugAppView<T> extends AppView<T> {
  private _currentDebugContext: DebugContext = null;

  constructor(
      clazz: any, componentType: RenderComponentType, type: ViewType, viewUtils: ViewUtils,
      parentView: AppView<any>, parentIndex: number, parentNode: any, cdMode: ChangeDetectorStatus,
      public staticNodeDebugInfos: StaticNodeDebugInfo[],
      declaredViewContainer: ViewContainer = null) {
    super(
        clazz, componentType, type, viewUtils, parentView, parentIndex, parentNode, cdMode,
        declaredViewContainer);
  }

  create(context: T) {
    this._resetDebug();
    try {
      return super.create(context);
    } catch (e) {
      this._rethrowWithContext(e);
      throw e;
    }
  }

  createHostView(
      rootSelectorOrNode: string|any, injector: Injector,
      projectableNodes: any[][] = null): ComponentRef<any> {
    this._resetDebug();
    try {
      return super.createHostView(rootSelectorOrNode, injector, projectableNodes);
    } catch (e) {
      this._rethrowWithContext(e);
      throw e;
    }
  }

  injectorGet(token: any, nodeIndex: number, notFoundResult?: any): any {
    this._resetDebug();
    try {
      return super.injectorGet(token, nodeIndex, notFoundResult);
    } catch (e) {
      this._rethrowWithContext(e);
      throw e;
    }
  }

  detach(): void {
    this._resetDebug();
    try {
      super.detach();
    } catch (e) {
      this._rethrowWithContext(e);
      throw e;
    }
  }

  destroy() {
    this._resetDebug();
    try {
      super.destroy();
    } catch (e) {
      this._rethrowWithContext(e);
      throw e;
    }
  }

  detectChanges(throwOnChange: boolean): void {
    this._resetDebug();
    try {
      super.detectChanges(throwOnChange);
    } catch (e) {
      this._rethrowWithContext(e);
      throw e;
    }
  }

  private _resetDebug() { this._currentDebugContext = null; }

  debug(nodeIndex: number, rowNum: number, colNum: number): DebugContext {
    return this._currentDebugContext = new DebugContext(this, nodeIndex, rowNum, colNum);
  }

  private _rethrowWithContext(e: any) {
    if (!(e instanceof ViewWrappedError)) {
      if (!(e instanceof ExpressionChangedAfterItHasBeenCheckedError)) {
        this.cdMode = ChangeDetectorStatus.Errored;
      }
      if (isPresent(this._currentDebugContext)) {
        throw new ViewWrappedError(e, this._currentDebugContext);
      }
    }
  }

  eventHandler<E, R>(cb: (eventName: string, event?: E) => R): (eventName: string, event?: E) => R {
    const superHandler = super.eventHandler(cb);
    return (eventName: string, event?: any) => {
      this._resetDebug();
      try {
        return superHandler.call(this, eventName, event);
      } catch (e) {
        this._rethrowWithContext(e);
        throw e;
      }
    };
  }
}
