/**
 * @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 {AnimateTimings, AnimationAnimateMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationMetadataType, AnimationSequenceMetadata, AnimationStateMetadata, AnimationStyleMetadata, AnimationTransitionMetadata, sequence} from '@angular/animations';

import {normalizeStyles, parseTimeExpression} from '../util';

import {AnimationDslVisitor, visitAnimationNode} from './animation_dsl_visitor';

export type StyleTimeTuple = {
  startTime: number; endTime: number;
};

/*
 * [Validation]
 * The visitor code below will traverse the animation AST generated by the animation verb functions
 * (the output is a tree of objects) and attempt to perform a series of validations on the data. The
 * following corner-cases will be validated:
 *
 * 1. Overlap of animations
 * Given that a CSS property cannot be animated in more than one place at the same time, it's
 * important that this behaviour is detected and validated. The way in which this occurs is that
 * each time a style property is examined, a string-map containing the property will be updated with
 * the start and end times for when the property is used within an animation step.
 *
 * If there are two or more parallel animations that are currently running (these are invoked by the
 * group()) on the same element then the validator will throw an error. Since the start/end timing
 * values are collected for each property then if the current animation step is animating the same
 * property and its timing values fall anywhere into the window of time that the property is
 * currently being animated within then this is what causes an error.
 *
 * 2. Timing values
 * The validator will validate to see if a timing value of `duration delay easing` or
 * `durationNumber` is valid or not.
 *
 * (note that upon validation the code below will replace the timing data with an object containing
 * {duration,delay,easing}.
 *
 * 3. Offset Validation
 * Each of the style() calls are allowed to have an offset value when placed inside of keyframes().
 * Offsets within keyframes() are considered valid when:
 *
 *   - No offsets are used at all
 *   - Each style() entry contains an offset value
 *   - Each offset is between 0 and 1
 *   - Each offset is greater to or equal than the previous one
 *
 * Otherwise an error will be thrown.
 */
export function validateAnimationSequence(ast: AnimationMetadata) {
  const normalizedAst =
      Array.isArray(ast) ? sequence(<AnimationMetadata[]>ast) : <AnimationMetadata>ast;
  return new AnimationValidatorVisitor().validate(normalizedAst);
}

export class AnimationValidatorVisitor implements AnimationDslVisitor {
  validate(ast: AnimationMetadata): string[] {
    const context = new AnimationValidatorContext();
    visitAnimationNode(this, ast, context);
    return context.errors;
  }

  visitState(ast: AnimationStateMetadata, context: any): any {
    // these values are not visited in this AST
  }

  visitTransition(ast: AnimationTransitionMetadata, context: any): any {
    // these values are not visited in this AST
  }

  visitSequence(ast: AnimationSequenceMetadata, context: AnimationValidatorContext): any {
    ast.steps.forEach(step => visitAnimationNode(this, step, context));
  }

  visitGroup(ast: AnimationGroupMetadata, context: AnimationValidatorContext): any {
    const currentTime = context.currentTime;
    let furthestTime = 0;
    ast.steps.forEach(step => {
      context.currentTime = currentTime;
      visitAnimationNode(this, step, context);
      furthestTime = Math.max(furthestTime, context.currentTime);
    });
    context.currentTime = furthestTime;
  }

  visitAnimate(ast: AnimationAnimateMetadata, context: AnimationValidatorContext): any {
    // we reassign the timings here so that they are not reparsed each
    // time an animation occurs
    context.currentAnimateTimings = ast.timings =
        parseTimeExpression(<string|number>ast.timings, context.errors);

    const astType = ast.styles && ast.styles.type;
    if (astType == AnimationMetadataType.KeyframeSequence) {
      this.visitKeyframeSequence(<AnimationKeyframesSequenceMetadata>ast.styles, context);
    } else {
      context.currentTime +=
          context.currentAnimateTimings.duration + context.currentAnimateTimings.delay;
      if (astType == AnimationMetadataType.Style) {
        this.visitStyle(<AnimationStyleMetadata>ast.styles, context);
      }
    }

    context.currentAnimateTimings = null;
  }

  visitStyle(ast: AnimationStyleMetadata, context: AnimationValidatorContext): any {
    const styleData = normalizeStyles(ast.styles);
    const timings = context.currentAnimateTimings;
    let endTime = context.currentTime;
    let startTime = context.currentTime;
    if (timings && startTime > 0) {
      startTime -= timings.duration + timings.delay;
    }
    Object.keys(styleData).forEach(prop => {
      const collectedEntry = context.collectedStyles[prop];
      let updateCollectedStyle = true;
      if (collectedEntry) {
        if (startTime != endTime && startTime >= collectedEntry.startTime &&
            endTime <= collectedEntry.endTime) {
          context.errors.push(
              `The CSS property "${prop}" that exists between the times of "${collectedEntry.startTime}ms" and "${collectedEntry.endTime}ms" is also being animated in a parallel animation between the times of "${startTime}ms" and "${endTime}ms"`);
          updateCollectedStyle = false;
        }

        // we always choose the smaller start time value since we
        // want to have a record of the entire animation window where
        // the style property is being animated in between
        startTime = collectedEntry.startTime;
      }
      if (updateCollectedStyle) {
        context.collectedStyles[prop] = {startTime, endTime};
      }
    });
  }

  visitKeyframeSequence(
      ast: AnimationKeyframesSequenceMetadata, context: AnimationValidatorContext): any {
    let totalKeyframesWithOffsets = 0;
    const offsets: number[] = [];
    let offsetsOutOfOrder = false;
    let keyframesOutOfRange = false;
    let previousOffset: number = 0;
    ast.steps.forEach(step => {
      const styleData = normalizeStyles(step.styles);
      let offset = 0;
      if (styleData.hasOwnProperty('offset')) {
        totalKeyframesWithOffsets++;
        offset = <number>styleData['offset'];
      }
      keyframesOutOfRange = keyframesOutOfRange || offset < 0 || offset > 1;
      offsetsOutOfOrder = offsetsOutOfOrder || offset < previousOffset;
      previousOffset = offset;
      offsets.push(offset);
    });

    if (keyframesOutOfRange) {
      context.errors.push(`Please ensure that all keyframe offsets are between 0 and 1`);
    }

    if (offsetsOutOfOrder) {
      context.errors.push(`Please ensure that all keyframe offsets are in order`);
    }

    const length = ast.steps.length;
    let generatedOffset = 0;
    if (totalKeyframesWithOffsets > 0 && totalKeyframesWithOffsets < length) {
      context.errors.push(`Not all style() steps within the declared keyframes() contain offsets`);
    } else if (totalKeyframesWithOffsets == 0) {
      generatedOffset = 1 / length;
    }

    const limit = length - 1;
    const currentTime = context.currentTime;
    const animateDuration = context.currentAnimateTimings !.duration;
    ast.steps.forEach((step, i) => {
      const offset = generatedOffset > 0 ? (i == limit ? 1 : (generatedOffset * i)) : offsets[i];
      const durationUpToThisFrame = offset * animateDuration;
      context.currentTime =
          currentTime + context.currentAnimateTimings !.delay + durationUpToThisFrame;
      context.currentAnimateTimings !.duration = durationUpToThisFrame;
      this.visitStyle(step, context);
    });
  }
}

export class AnimationValidatorContext {
  public errors: string[] = [];
  public currentTime: number = 0;
  public currentAnimateTimings: AnimateTimings|null;
  public collectedStyles: {[propName: string]: StyleTimeTuple} = {};
}
