// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:sky/animation/animated_value.dart';
import 'package:sky/animation/direction.dart';
import 'package:sky/animation/forces.dart';
import 'package:sky/animation/timeline.dart';

export 'package:sky/animation/direction.dart' show Direction;

enum AnimationStatus {
  dismissed, // stoped at 0
  forward,   // animating from 0 => 1
  reverse,   // animating from 1 => 0
  completed, // stopped at 1
}

// This class manages a "performance" - a collection of values that change
// based on a timeline. For example, a performance may handle an animation
// of a menu opening by sliding and fading in (changing Y value and opacity)
// over .5 seconds. The performance can move forwards (present) or backwards
// (dismiss). A consumer may also take direct control of the timeline by
// manipulating |progress|, or |fling| the timeline causing a physics-based
// simulation to take over the progression.
class AnimationPerformance {
  AnimationPerformance({AnimatedVariable variable, this.duration}) :
    _variable = variable {
    _timeline = new Timeline(_tick);
  }

  AnimatedVariable _variable;
  Duration duration;

  AnimatedVariable get variable => _variable;
  void set variable(AnimatedVariable v) { _variable = v; }

  // Advances from 0 to 1. On each tick, we'll update our variable's values.
  Timeline _timeline;
  Timeline get timeline => _timeline;

  Direction _direction;
  Direction get direction => _direction;

  // This controls which curve we use for variables with different curves in
  // the forward/reverse directions. Curve direction is only reset when we hit
  // 0 or 1, to avoid discontinuities.
  Direction _curveDirection;
  Direction get curveDirection => _curveDirection;

  AnimationTiming timing;

  // If non-null, animate with this force instead of a tween animation.
  Force attachedForce;

  void addVariable(AnimatedVariable newVariable) {
    if (variable == null) {
      variable = newVariable;
    } else if (variable is AnimatedList) {
      (variable as AnimatedList).variables.add(newVariable);
    } else {
      variable = new AnimatedList([variable, newVariable]);
    }
  }

  double get progress => timeline.value;
  void set progress(double t) {
    // TODO(mpcomplete): should this affect |direction|?
    stop();
    timeline.value = t.clamp(0.0, 1.0);
    _checkStatusChanged();
  }

  double get curvedProgress {
    return timing != null ? timing.transform(progress, curveDirection) : progress;
  }

  bool get isDismissed => status == AnimationStatus.dismissed;
  bool get isCompleted => status == AnimationStatus.completed;
  bool get isAnimating => timeline.isAnimating;

  AnimationStatus get status {
    if (!isAnimating && progress == 1.0)
      return AnimationStatus.completed;
    if (!isAnimating && progress == 0.0)
      return AnimationStatus.dismissed;
    return direction == Direction.forward ?
        AnimationStatus.forward :
        AnimationStatus.reverse;
  }

  void updateVariable(AnimatedVariable variable) {
    variable.setProgress(curvedProgress, curveDirection);
  }

  Future play([Direction direction = Direction.forward]) {
    _direction = direction;
    return resume();
  }
  Future forward() => play(Direction.forward);
  Future reverse() => play(Direction.reverse);
  Future resume() {
    if (attachedForce != null) {
      return fling(velocity: _direction == Direction.forward ? 1.0 : -1.0,
                   force: attachedForce);
    }
    return _animateTo(direction == Direction.forward ? 1.0 : 0.0);
  }

  void stop() {
    timeline.stop();
  }

  // Flings the timeline with an optional force (defaults to a critically
  // damped spring) and initial velocity. If velocity is positive, the
  // animation will complete, otherwise it will dismiss.
  Future fling({double velocity: 1.0, Force force}) {
    if (force == null)
      force = kDefaultSpringForce;
    _direction = velocity < 0.0 ? Direction.reverse : Direction.forward;
    return timeline.fling(force.release(progress, velocity));
  }

  final List<Function> _listeners = new List<Function>();

  void addListener(Function listener) {
    _listeners.add(listener);
  }

  void removeListener(Function listener) {
    _listeners.remove(listener);
  }

  void _notifyListeners() {
    List<Function> localListeners = new List<Function>.from(_listeners);
    for (Function listener in localListeners)
      listener();
  }

  final List<Function> _statusListeners = new List<Function>();

  void addStatusListener(Function listener) {
    _statusListeners.add(listener);
  }

  void removeStatusListener(Function listener) {
    _statusListeners.remove(listener);
  }

  AnimationStatus _lastStatus = AnimationStatus.dismissed;
  void _checkStatusChanged() {
    AnimationStatus currentStatus = status;
    if (currentStatus != _lastStatus) {
      List<Function> localListeners = new List<Function>.from(_statusListeners);
      for (Function listener in localListeners)
        listener(currentStatus);
    }
    _lastStatus = currentStatus;
  }

  void _updateCurveDirection() {
    if (status != _lastStatus) {
      if (_lastStatus == AnimationStatus.dismissed || _lastStatus == AnimationStatus.completed)
        _curveDirection = direction;
    }
  }

  Future _animateTo(double target) {
    Duration remainingDuration = duration * (target - timeline.value).abs();
    timeline.stop();
    if (remainingDuration == Duration.ZERO)
      return new Future.value();
    return timeline.animateTo(target, duration: remainingDuration);
  }

  void _tick(double t) {
    _updateCurveDirection();
    if (variable != null)
      variable.setProgress(curvedProgress, curveDirection);
    _notifyListeners();
    _checkStatusChanged();
  }
}

// Simple helper class for an animation with a single value.
class ValueAnimation<T> extends AnimationPerformance {
  ValueAnimation({AnimatedValue<T> variable, Duration duration}) :
    super(variable: variable, duration: duration);

  AnimatedValue<T> get variable => _variable as AnimatedValue<T>;
  void set variable(AnimatedValue<T> v) { _variable = v; }

  T get value => variable.value;
}
