// 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:collection';
import 'dart:ui' as ui;

/// Slows down animations by this factor to help in development.
double timeDilation = 1.0;

/// A callback from the scheduler
///
/// The timeStamp is the number of milliseconds since the beginning of the
/// scheduler's epoch. Use timeStamp to determine how far to advance animation
/// timelines so that all the animations in the system are synchronized to a
/// common time base.
typedef void SchedulerCallback(Duration timeStamp);

typedef void SchedulerExceptionHandler(dynamic exception, StackTrace stack);
/// This callback is invoked whenever an exception is caught by the scheduler.
/// The 'exception' argument contains the object that was thrown, and the
/// 'stack' argument contains the stack trace. If the callback is set, it is
/// invoked instead of printing the information to the console.
SchedulerExceptionHandler debugSchedulerExceptionHandler;

/// Schedules callbacks to run in concert with the engine's animation system
class Scheduler {
  /// Requires clients to use the [scheduler] singleton
  Scheduler._() {
    ui.window.onBeginFrame = beginFrame;
  }

  bool _haveScheduledVisualUpdate = false;
  int _nextCallbackId = 0; // positive

  final List<SchedulerCallback> _persistentCallbacks = new List<SchedulerCallback>();
  Map<int, SchedulerCallback> _transientCallbacks = new LinkedHashMap<int, SchedulerCallback>();
  final Set<int> _removedIds = new Set<int>();
  final List<SchedulerCallback> _postFrameCallbacks = new List<SchedulerCallback>();

  bool _inFrame = false;

  int get transientCallbackCount => _transientCallbacks.length;

  /// Called by the engine to produce a new frame.
  ///
  /// This function first calls all the callbacks registered by
  /// [requestAnimationFrame], then calls all the callbacks registered by
  /// [addPersistentFrameCallback], which typically drive the rendering pipeline,
  /// and finally calls the callbacks registered by [requestPostFrameCallback].
  void beginFrame(Duration rawTimeStamp) {
    assert(!_inFrame);
    _inFrame = true;
    Duration timeStamp = new Duration(
        microseconds: (rawTimeStamp.inMicroseconds / timeDilation).round());
    _haveScheduledVisualUpdate = false;

    Map<int, SchedulerCallback> callbacks = _transientCallbacks;
    _transientCallbacks = new Map<int, SchedulerCallback>();
    callbacks.forEach((int id, SchedulerCallback callback) {
      if (!_removedIds.contains(id))
        invokeCallback(callback, timeStamp);
    });
    _removedIds.clear();

    for (SchedulerCallback callback in _persistentCallbacks)
      invokeCallback(callback, timeStamp);

    List<SchedulerCallback> localPostFrameCallbacks =
        new List<SchedulerCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();
    for (SchedulerCallback callback in localPostFrameCallbacks)
      invokeCallback(callback, timeStamp);

    _inFrame = false;
  }

  void invokeCallback(SchedulerCallback callback, Duration timeStamp) {
    assert(callback != null);
    try {
      callback(timeStamp);
    } catch (exception, stack) {
      if (debugSchedulerExceptionHandler != null) {
        debugSchedulerExceptionHandler(exception, stack);
      } else {
        print('-- EXCEPTION IN SCHEDULER CALLBACK --');
        print('$exception');
        print('Stack trace:');
        print('$stack');
      }
    }
  }

  /// Call callback every frame.
  void addPersistentFrameCallback(SchedulerCallback callback) {
    _persistentCallbacks.add(callback);
  }

  /// Schedule a callback for the next frame.
  ///
  /// The callback will be run prior to flushing the main rendering pipeline.
  /// Typically, requestAnimationFrame is used to throttle writes into the
  /// rendering pipeline until the system is ready to accept a new frame. For
  /// example, if you wanted to tick through an animation, you should use
  /// requestAnimation frame to determine when to tick the animation. The callback
  /// is passed a timeStamp that you can use to determine how far along the
  /// timeline to advance your animation.
  ///
  /// Callbacks in invoked in an arbitrary order.
  ///
  /// Returns an id that can be used to unschedule this callback.
  int requestAnimationFrame(SchedulerCallback callback) {
    _nextCallbackId += 1;
    _transientCallbacks[_nextCallbackId] = callback;
    ensureVisualUpdate();
    return _nextCallbackId;
  }

  /// Cancel the callback identified by id.
  void cancelAnimationFrame(int id) {
    assert(id > 0);
    _transientCallbacks.remove(id);
    _removedIds.add(id);
  }

  /// Schedule a callback for the end of this frame.
  ///
  /// If a frame is in progress, the callback will be run just after the main
  /// rendering pipeline has been flushed. In this case, order is preserved (the
  /// callbacks are run in registration order).
  ///
  /// If no frame is in progress, it will be called at the start of the next
  /// frame. In this case, the registration order is not preserved. Callbacks
  /// are called in an arbitrary order.
  void requestPostFrameCallback(SchedulerCallback callback) {
    _postFrameCallbacks.add(callback);
  }

  /// Ensure that a frame will be produced after this function is called.
  void ensureVisualUpdate() {
    if (_haveScheduledVisualUpdate)
      return;
    ui.window.scheduleFrame();
    _haveScheduledVisualUpdate = true;
  }
}

/// A singleton instance of Scheduler to coordinate all the callbacks.
final Scheduler scheduler = new Scheduler._();
