/**
 * @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 {ɵANY_STATE, ɵDEFAULT_STATE, ɵEMPTY_STATE} from '@angular/core';

import {isPresent} from '../facade/lang';
import {Identifiers, createIdentifier} from '../identifiers';
import * as o from '../output/output_ast';

import {AnimationAst, AnimationAstVisitor, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStateTransitionFnExpression, AnimationStepAst, AnimationStylesAst} from './animation_ast';

export class AnimationEntryCompileResult {
  constructor(public name: string, public statements: o.Statement[], public fnExp: o.Expression) {}
}

export class AnimationCompiler {
  compile(factoryNamePrefix: string, parsedAnimations: AnimationEntryAst[]):
      AnimationEntryCompileResult[] {
    return parsedAnimations.map(entry => {
      const factoryName = `${factoryNamePrefix}_${entry.name}`;
      const visitor = new _AnimationBuilder(entry.name, factoryName);
      return visitor.build(entry);
    });
  }
}

const _ANIMATION_FACTORY_ELEMENT_VAR = o.variable('element');
const _ANIMATION_DEFAULT_STATE_VAR = o.variable('defaultStateStyles');
const _ANIMATION_FACTORY_VIEW_VAR = o.variable('view');
const _ANIMATION_FACTORY_VIEW_CONTEXT = _ANIMATION_FACTORY_VIEW_VAR.prop('animationContext');
const _ANIMATION_FACTORY_RENDERER_VAR = _ANIMATION_FACTORY_VIEW_VAR.prop('renderer');
const _ANIMATION_CURRENT_STATE_VAR = o.variable('currentState');
const _ANIMATION_NEXT_STATE_VAR = o.variable('nextState');
const _ANIMATION_PLAYER_VAR = o.variable('player');
const _ANIMATION_TIME_VAR = o.variable('totalTime');
const _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles');
const _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles');
const _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles');
const _PREVIOUS_ANIMATION_PLAYERS = o.variable('previousPlayers');
const _EMPTY_MAP = o.literalMap([]);
const _EMPTY_ARRAY = o.literalArr([]);

class _AnimationBuilder implements AnimationAstVisitor {
  private _fnVarName: string;
  private _statesMapVarName: string;
  private _statesMapVar: any;

  constructor(public animationName: string, factoryName: string) {
    this._fnVarName = factoryName + '_factory';
    this._statesMapVarName = factoryName + '_states';
    this._statesMapVar = o.variable(this._statesMapVarName);
  }

  visitAnimationStyles(ast: AnimationStylesAst, context: _AnimationBuilderContext): o.Expression {
    const stylesArr: any[] = [];
    if (context.isExpectingFirstStyleStep) {
      stylesArr.push(_ANIMATION_START_STATE_STYLES_VAR);
      context.isExpectingFirstStyleStep = false;
    }

    ast.styles.forEach(entry => {
      const entries =
          Object.keys(entry).map((key): [string, o.Expression] => [key, o.literal(entry[key])]);
      stylesArr.push(o.literalMap(entries, null, true));
    });

    return o.importExpr(createIdentifier(Identifiers.AnimationStyles)).instantiate([
      o.importExpr(createIdentifier(Identifiers.collectAndResolveStyles)).callFn([
        _ANIMATION_COLLECTED_STYLES, o.literalArr(stylesArr)
      ])
    ]);
  }

  visitAnimationKeyframe(ast: AnimationKeyframeAst, context: _AnimationBuilderContext):
      o.Expression {
    return o.importExpr(createIdentifier(Identifiers.AnimationKeyframe)).instantiate([
      o.literal(ast.offset), ast.styles.visit(this, context)
    ]);
  }

  visitAnimationStep(ast: AnimationStepAst, context: _AnimationBuilderContext): o.Expression {
    if (context.endStateAnimateStep === ast) {
      return this._visitEndStateAnimation(ast, context);
    }

    const startingStylesExpr = ast.startingStyles.visit(this, context);
    const keyframeExpressions =
        ast.keyframes.map(keyframeEntry => keyframeEntry.visit(this, context));
    return this._callAnimateMethod(
        ast, startingStylesExpr, o.literalArr(keyframeExpressions), context);
  }

  /** @internal */
  _visitEndStateAnimation(ast: AnimationStepAst, context: _AnimationBuilderContext): o.Expression {
    const startingStylesExpr = ast.startingStyles.visit(this, context);
    const keyframeExpressions = ast.keyframes.map(keyframe => keyframe.visit(this, context));
    const keyframesExpr =
        o.importExpr(createIdentifier(Identifiers.balanceAnimationKeyframes)).callFn([
          _ANIMATION_COLLECTED_STYLES, _ANIMATION_END_STATE_STYLES_VAR,
          o.literalArr(keyframeExpressions)
        ]);

    return this._callAnimateMethod(ast, startingStylesExpr, keyframesExpr, context);
  }

  /** @internal */
  _callAnimateMethod(
      ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any,
      context: _AnimationBuilderContext) {
    let previousStylesValue: o.Expression = _EMPTY_ARRAY;
    if (context.isExpectingFirstAnimateStep) {
      previousStylesValue = _PREVIOUS_ANIMATION_PLAYERS;
      context.isExpectingFirstAnimateStep = false;
    }
    context.totalTransitionTime += ast.duration + ast.delay;
    return _ANIMATION_FACTORY_RENDERER_VAR.callMethod('animate', [
      _ANIMATION_FACTORY_ELEMENT_VAR, startingStylesExpr, keyframesExpr, o.literal(ast.duration),
      o.literal(ast.delay), o.literal(ast.easing), previousStylesValue
    ]);
  }

  visitAnimationSequence(ast: AnimationSequenceAst, context: _AnimationBuilderContext):
      o.Expression {
    const playerExprs = ast.steps.map(step => step.visit(this, context));
    return o.importExpr(createIdentifier(Identifiers.AnimationSequencePlayer)).instantiate([
      o.literalArr(playerExprs)
    ]);
  }

  visitAnimationGroup(ast: AnimationGroupAst, context: _AnimationBuilderContext): o.Expression {
    const playerExprs = ast.steps.map(step => step.visit(this, context));
    return o.importExpr(createIdentifier(Identifiers.AnimationGroupPlayer)).instantiate([
      o.literalArr(playerExprs)
    ]);
  }

  visitAnimationStateDeclaration(
      ast: AnimationStateDeclarationAst, context: _AnimationBuilderContext): void {
    const flatStyles: {[key: string]: string | number} = {};
    _getStylesArray(ast).forEach(
        entry => { Object.keys(entry).forEach(key => { flatStyles[key] = entry[key]; }); });
    context.stateMap.registerState(ast.stateName, flatStyles);
  }

  visitAnimationStateTransition(
      ast: AnimationStateTransitionAst, context: _AnimationBuilderContext): any {
    const steps = ast.animation.steps;
    const lastStep = steps[steps.length - 1];
    if (_isEndStateAnimateStep(lastStep)) {
      context.endStateAnimateStep = <AnimationStepAst>lastStep;
    }

    context.totalTransitionTime = 0;
    context.isExpectingFirstStyleStep = true;
    context.isExpectingFirstAnimateStep = true;

    const stateChangePreconditions: o.Expression[] = [];

    ast.stateChanges.forEach(stateChange => {
      if (stateChange instanceof AnimationStateTransitionFnExpression) {
        stateChangePreconditions.push(o.importExpr({reference: stateChange.fn}).callFn([
          _ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR
        ]));
      } else {
        stateChangePreconditions.push(
            _compareToAnimationStateExpr(_ANIMATION_CURRENT_STATE_VAR, stateChange.fromState)
                .and(_compareToAnimationStateExpr(_ANIMATION_NEXT_STATE_VAR, stateChange.toState)));

        if (stateChange.fromState != ɵANY_STATE) {
          context.stateMap.registerState(stateChange.fromState);
        }

        if (stateChange.toState != ɵANY_STATE) {
          context.stateMap.registerState(stateChange.toState);
        }
      }
    });

    const animationPlayerExpr = ast.animation.visit(this, context);

    const reducedStateChangesPrecondition = stateChangePreconditions.reduce((a, b) => a.or(b));
    const precondition =
        _ANIMATION_PLAYER_VAR.equals(o.NULL_EXPR).and(reducedStateChangesPrecondition);

    const animationStmt = _ANIMATION_PLAYER_VAR.set(animationPlayerExpr).toStmt();
    const totalTimeStmt = _ANIMATION_TIME_VAR.set(o.literal(context.totalTransitionTime)).toStmt();

    return new o.IfStmt(precondition, [animationStmt, totalTimeStmt]);
  }

  visitAnimationEntry(ast: AnimationEntryAst, context: _AnimationBuilderContext): any {
    // visit each of the declarations first to build the context state map
    ast.stateDeclarations.forEach(def => def.visit(this, context));

    // this should always be defined even if the user overrides it
    context.stateMap.registerState(ɵDEFAULT_STATE, {});

    const statements: o.Statement[] = [];
    statements.push(_PREVIOUS_ANIMATION_PLAYERS
                        .set(_ANIMATION_FACTORY_VIEW_CONTEXT.callMethod(
                            'getAnimationPlayers',
                            [
                              _ANIMATION_FACTORY_ELEMENT_VAR,
                              _ANIMATION_NEXT_STATE_VAR.equals(o.literal(ɵEMPTY_STATE))
                                  .conditional(o.NULL_EXPR, o.literal(this.animationName))
                            ]))
                        .toDeclStmt());

    statements.push(_ANIMATION_COLLECTED_STYLES.set(_EMPTY_MAP).toDeclStmt());
    statements.push(_ANIMATION_PLAYER_VAR.set(o.NULL_EXPR).toDeclStmt());
    statements.push(_ANIMATION_TIME_VAR.set(o.literal(0)).toDeclStmt());

    statements.push(
        _ANIMATION_DEFAULT_STATE_VAR.set(this._statesMapVar.key(o.literal(ɵDEFAULT_STATE)))
            .toDeclStmt());

    statements.push(
        _ANIMATION_START_STATE_STYLES_VAR.set(this._statesMapVar.key(_ANIMATION_CURRENT_STATE_VAR))
            .toDeclStmt());

    statements.push(new o.IfStmt(
        _ANIMATION_START_STATE_STYLES_VAR.equals(o.NULL_EXPR),
        [_ANIMATION_START_STATE_STYLES_VAR.set(_ANIMATION_DEFAULT_STATE_VAR).toStmt()]));

    statements.push(
        _ANIMATION_END_STATE_STYLES_VAR.set(this._statesMapVar.key(_ANIMATION_NEXT_STATE_VAR))
            .toDeclStmt());

    statements.push(new o.IfStmt(
        _ANIMATION_END_STATE_STYLES_VAR.equals(o.NULL_EXPR),
        [_ANIMATION_END_STATE_STYLES_VAR.set(_ANIMATION_DEFAULT_STATE_VAR).toStmt()]));

    const RENDER_STYLES_FN = o.importExpr(createIdentifier(Identifiers.renderStyles));

    ast.stateTransitions.forEach(transAst => statements.push(transAst.visit(this, context)));

    // this check ensures that the animation factory always returns a player
    // so that the onDone callback can be used for tracking
    statements.push(new o.IfStmt(
        _ANIMATION_PLAYER_VAR.equals(o.NULL_EXPR),
        [_ANIMATION_PLAYER_VAR
             .set(o.importExpr(createIdentifier(Identifiers.NoOpAnimationPlayer)).instantiate([]))
             .toStmt()]));

    // once complete we want to apply the styles on the element
    // since the destination state's values should persist once
    // the animation sequence has completed.
    statements.push(
        _ANIMATION_PLAYER_VAR
            .callMethod(
                'onDone',
                [o
                     .fn([],
                         [
                           _ANIMATION_PLAYER_VAR.callMethod('destroy', []).toStmt(),
                           RENDER_STYLES_FN
                               .callFn([
                                 _ANIMATION_FACTORY_ELEMENT_VAR, _ANIMATION_FACTORY_RENDERER_VAR,
                                 o.importExpr(
                                      createIdentifier(Identifiers.prepareFinalAnimationStyles))
                                     .callFn(
                                         [
                                           _ANIMATION_START_STATE_STYLES_VAR,
                                           _ANIMATION_END_STATE_STYLES_VAR
                                         ])
                               ])
                               .toStmt()
                         ])])
            .toStmt());

    statements.push(o.importExpr(createIdentifier(Identifiers.AnimationSequencePlayer))
                        .instantiate([_PREVIOUS_ANIMATION_PLAYERS])
                        .callMethod('destroy', [])
                        .toStmt());

    // before we start any animation we want to clear out the starting
    // styles from the element's style property (since they were placed
    // there at the end of the last animation
    statements.push(RENDER_STYLES_FN
                        .callFn([
                          _ANIMATION_FACTORY_ELEMENT_VAR, _ANIMATION_FACTORY_RENDERER_VAR,
                          o.importExpr(createIdentifier(Identifiers.clearStyles))
                              .callFn([_ANIMATION_START_STATE_STYLES_VAR])
                        ])
                        .toStmt());

    statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT
                        .callMethod(
                            'queueAnimation',
                            [
                              _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName),
                              _ANIMATION_PLAYER_VAR
                            ])
                        .toStmt());

    statements.push(new o.ReturnStatement(
        o.importExpr(createIdentifier(Identifiers.AnimationTransition)).instantiate([
          _ANIMATION_PLAYER_VAR, _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName),
          _ANIMATION_CURRENT_STATE_VAR, _ANIMATION_NEXT_STATE_VAR, _ANIMATION_TIME_VAR
        ])));

    return o.fn(
        [
          new o.FnParam(
              _ANIMATION_FACTORY_VIEW_VAR.name,
              o.importType(createIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])),
          new o.FnParam(_ANIMATION_FACTORY_ELEMENT_VAR.name, o.DYNAMIC_TYPE),
          new o.FnParam(_ANIMATION_CURRENT_STATE_VAR.name, o.DYNAMIC_TYPE),
          new o.FnParam(_ANIMATION_NEXT_STATE_VAR.name, o.DYNAMIC_TYPE)
        ],
        statements, o.importType(createIdentifier(Identifiers.AnimationTransition)));
  }

  build(ast: AnimationAst): AnimationEntryCompileResult {
    const context = new _AnimationBuilderContext();
    const fnStatement = ast.visit(this, context).toDeclStmt(this._fnVarName);
    const fnVariable = o.variable(this._fnVarName);

    const lookupMap: any[] = [];
    Object.keys(context.stateMap.states).forEach(stateName => {
      const value = context.stateMap.states[stateName];
      let variableValue = _EMPTY_MAP;
      if (isPresent(value)) {
        const styleMap: any[] = [];
        Object.keys(value).forEach(key => { styleMap.push([key, o.literal(value[key])]); });
        variableValue = o.literalMap(styleMap, null, true);
      }
      lookupMap.push([stateName, variableValue]);
    });

    const compiledStatesMapStmt =
        this._statesMapVar.set(o.literalMap(lookupMap, null, true)).toDeclStmt();
    const statements: o.Statement[] = [compiledStatesMapStmt, fnStatement];

    return new AnimationEntryCompileResult(this.animationName, statements, fnVariable);
  }
}

class _AnimationBuilderContext {
  stateMap = new _AnimationBuilderStateMap();
  endStateAnimateStep: AnimationStepAst = null;
  isExpectingFirstStyleStep = false;
  isExpectingFirstAnimateStep = false;
  totalTransitionTime = 0;
}

class _AnimationBuilderStateMap {
  private _states: {[key: string]: {[prop: string]: string | number}} = {};
  get states() { return this._states; }
  registerState(name: string, value: {[prop: string]: string | number} = null): void {
    const existingEntry = this._states[name];
    if (!existingEntry) {
      this._states[name] = value;
    }
  }
}

function _compareToAnimationStateExpr(value: o.Expression, animationState: string): o.Expression {
  const emptyStateLiteral = o.literal(ɵEMPTY_STATE);
  switch (animationState) {
    case ɵEMPTY_STATE:
      return value.equals(emptyStateLiteral);

    case ɵANY_STATE:
      return o.literal(true);

    default:
      return value.equals(o.literal(animationState));
  }
}

function _isEndStateAnimateStep(step: AnimationAst): boolean {
  // the final animation step is characterized by having only TWO
  // keyframe values and it must have zero styles for both keyframes
  if (step instanceof AnimationStepAst && step.duration > 0 && step.keyframes.length == 2) {
    const styles1 = _getStylesArray(step.keyframes[0])[0];
    const styles2 = _getStylesArray(step.keyframes[1])[0];
    return Object.keys(styles1).length === 0 && Object.keys(styles2).length === 0;
  }
  return false;
}

function _getStylesArray(obj: any): {[key: string]: any}[] {
  return obj.styles.styles;
}
