// 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 'dart:ui' show VoidCallback;

import 'animated_value.dart';
import 'forces.dart';
import 'simulation_stepper.dart';

/// The status of an animation
enum PerformanceStatus {
  /// The animation is stopped at the beginning
  dismissed,

  /// The animation is running from beginning to end
  forward,

  /// The animation is running backwards, from end to beginning
  reverse,

  /// The animation is stopped at the end
  completed,
}

typedef void PerformanceStatusListener(PerformanceStatus status);

/// An interface that is implemented by [Performance] that exposes a
/// read-only view of the underlying performance. This is used by classes that
/// want to watch a performance but should not be able to change the
/// performance's state.
abstract class PerformanceView {
  const PerformanceView();

  /// Update the given variable according to the current progress of the performance
  void updateVariable(Animatable variable);
  /// Calls the listener every time the progress of the performance changes
  void addListener(VoidCallback listener);
  /// Stop calling the listener every time the progress of the performance changes
  void removeListener(VoidCallback listener);
  /// Calls listener every time the status of the performance changes
  void addStatusListener(PerformanceStatusListener listener);
  /// Stops calling the listener every time the status of the performance changes
  void removeStatusListener(PerformanceStatusListener listener);

  /// The current status of this animation.
  PerformanceStatus get status;

  /// The current direction of the animation.
  AnimationDirection get direction;

  /// The direction used to select the current curve.
  ///
  /// The curve direction is only reset when we hit the beginning or the end of
  /// the timeline to avoid discontinuities in the value of any variables this
  /// performance is used to animate.
  AnimationDirection get curveDirection;

  /// The current progress of this animation (a value from 0.0 to 1.0).
  /// This is the value that is used to update any variables when using updateVariable().
  double get progress;

  /// Whether this animation is stopped at the beginning
  bool get isDismissed => status == PerformanceStatus.dismissed;

  /// Whether this animation is stopped at the end
  bool get isCompleted => status == PerformanceStatus.completed;
}

class AlwaysCompletePerformance extends PerformanceView {
  const AlwaysCompletePerformance();

  void updateVariable(Animatable variable) {
    variable.setProgress(1.0, AnimationDirection.forward);
  }

  // this performance never changes state
  void addListener(VoidCallback listener) { }
  void removeListener(VoidCallback listener) { }
  void addStatusListener(PerformanceStatusListener listener) { }
  void removeStatusListener(PerformanceStatusListener listener) { }
  PerformanceStatus get status => PerformanceStatus.completed;
  AnimationDirection get direction => AnimationDirection.forward;
  AnimationDirection get curveDirection => AnimationDirection.forward;
  double get progress => 1.0;
}
const AlwaysCompletePerformance alwaysCompletePerformance = const AlwaysCompletePerformance();

class ReversePerformance extends PerformanceView {
  ReversePerformance(this.masterPerformance) {
    masterPerformance.addStatusListener(_statusChangeHandler);
  }

  final PerformanceView masterPerformance;

  void updateVariable(Animatable variable) {
    variable.setProgress(progress, curveDirection);
  }

  void addListener(VoidCallback listener) {
    masterPerformance.addListener(listener);
  }
  void removeListener(VoidCallback listener) {
    masterPerformance.removeListener(listener);
  }

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

  /// Calls listener every time the status of this performance changes
  void addStatusListener(PerformanceStatusListener listener) {
    _statusListeners.add(listener);
  }

  /// Stops calling the listener every time the status of this performance changes
  void removeStatusListener(PerformanceStatusListener listener) {
    _statusListeners.remove(listener);
  }

  void _statusChangeHandler(PerformanceStatus status) {
    status = _reverseStatus(status);
    List<PerformanceStatusListener> localListeners = new List<PerformanceStatusListener>.from(_statusListeners);
    for (PerformanceStatusListener listener in localListeners)
      listener(status);
  }

  PerformanceStatus get status => _reverseStatus(masterPerformance.status);
  AnimationDirection get direction => _reverseDirection(masterPerformance.direction);
  AnimationDirection get curveDirection => _reverseDirection(masterPerformance.curveDirection);
  double get progress => 1.0 - masterPerformance.progress;

  PerformanceStatus _reverseStatus(PerformanceStatus status) {
    switch (status) {
      case PerformanceStatus.forward: return PerformanceStatus.reverse;
      case PerformanceStatus.reverse: return PerformanceStatus.forward;
      case PerformanceStatus.completed: return PerformanceStatus.dismissed;
      case PerformanceStatus.dismissed: return PerformanceStatus.completed;
    }
  }

  AnimationDirection _reverseDirection(AnimationDirection direction) {
    switch (direction) {
      case AnimationDirection.forward: return AnimationDirection.reverse;
      case AnimationDirection.reverse: return AnimationDirection.forward;
    }
  }
}


/// A timeline that can be reversed and used to update [Animatable]s.
///
/// 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 Performance extends PerformanceView {
  Performance({ this.duration, double progress, this.debugLabel }) {
    _timeline = new SimulationStepper(_tick);
    if (progress != null)
      _timeline.value = progress.clamp(0.0, 1.0);
  }

  /// A label that is used in the toString() output. Intended to aid with
  /// identifying performance instances in debug output.
  final String debugLabel;

  /// Returns a [PerformanceView] for this performance,
  /// so that a pointer to this object can be passed around without
  /// allowing users of that pointer to mutate the Performance state.
  PerformanceView get view => this;

  /// The length of time this performance should last
  Duration duration;

  SimulationStepper _timeline;
  AnimationDirection get direction => _direction;
  AnimationDirection _direction;
  AnimationDirection get curveDirection => _curveDirection;
  AnimationDirection _curveDirection;

  /// If non-null, animate with this timing instead of a linear timing
  AnimationTiming timing;

  /// The progress of this performance along the timeline
  ///
  /// Note: Setting this value stops the current animation.
  double get progress => _timeline.value.clamp(0.0, 1.0);
  void set progress(double t) {
    stop();
    _timeline.value = t.clamp(0.0, 1.0);
    _checkStatusChanged();
  }

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

  /// Whether this animation is currently animating in either the forward or reverse direction
  bool get isAnimating => _timeline.isAnimating;

  PerformanceStatus get status {
    if (!isAnimating && progress == 1.0)
      return PerformanceStatus.completed;
    if (!isAnimating && progress == 0.0)
      return PerformanceStatus.dismissed;
    return _direction == AnimationDirection.forward ?
        PerformanceStatus.forward :
        PerformanceStatus.reverse;
  }

  /// Update the given varaible according to the current progress of this performance
  void updateVariable(Animatable variable) {
    variable.setProgress(_curvedProgress, _curveDirection);
  }

  /// Start running this animation forwards (towards the end)
  Future forward() => play(AnimationDirection.forward);

  /// Start running this animation in reverse (towards the beginning)
  Future reverse() => play(AnimationDirection.reverse);

  /// Start running this animation in the given direction
  Future play([AnimationDirection direction = AnimationDirection.forward]) {
    _direction = direction;
    return resume();
  }

  /// Start running this animation in the most recent direction
  Future resume() {
    return _animateTo(_direction == AnimationDirection.forward ? 1.0 : 0.0);
  }

  /// Stop running this animation
  void stop() {
    _timeline.stop();
  }

  /// Start running this animation according to the given physical parameters
  ///
  /// 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 ? AnimationDirection.reverse : AnimationDirection.forward;
    return _timeline.animateWith(force.release(progress, velocity));
  }

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

  /// Calls the listener every time the progress of this performance changes
  void addListener(VoidCallback listener) {
    _listeners.add(listener);
  }

  /// Stop calling the listener every time the progress of this performance changes
  void removeListener(VoidCallback listener) {
    _listeners.remove(listener);
  }

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

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

  /// Calls listener every time the status of this performance changes
  void addStatusListener(PerformanceStatusListener listener) {
    _statusListeners.add(listener);
  }

  /// Stops calling the listener every time the status of this performance changes
  void removeStatusListener(PerformanceStatusListener listener) {
    _statusListeners.remove(listener);
  }

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

  void _updateCurveDirection() {
    if (status != _lastStatus) {
      if (_lastStatus == PerformanceStatus.dismissed || _lastStatus == PerformanceStatus.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();
    didTick(t);
  }

  void didTick(double t) {
    _notifyListeners();
    _checkStatusChanged();
  }

  String toString() {
    if (debugLabel != null)
      return '$runtimeType at $progress for $debugLabel';
    return '$runtimeType at $progress';
  }
}

/// An animation performance with an animated variable with a concrete type
class ValuePerformance<T> extends Performance {
  ValuePerformance({ this.variable, Duration duration, double progress }) :
    super(duration: duration, progress: progress);

  AnimatedValue<T> variable;
  T get value => variable.value;

  void didTick(double t) {
    if (variable != null)
      variable.setProgress(_curvedProgress, _curveDirection);
    super.didTick(t);
  }
}
