/*! @file Link.hpp
 *  @copyright 2016, Ableton AG, Berlin. All rights reserved.
 *  @brief Library for cross-device shared tempo and quantized beat grid
 *
 *  @license:
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  If you would like to incorporate Link into a proprietary software application,
 *  please contact <link-devs@ableton.com>.
 */

#pragma once

#include <ableton/platforms/Config.hpp>
#include <chrono>
#include <mutex>

namespace ableton
{

/*! @class Link and BasicLink
 *  @brief Classes representing a participant in a Link session.
 *  The BasicLink type allows to customize the clock. The Link type
 *  uses the recommended platform-dependent representation of the
 *  system clock as defined in platforms/Config.hpp.
 *  It's preferred to use Link instead of BasicLink.
 *
 *  @discussion Each Link instance has its own session state which
 *  represents a beat timeline and a transport start/stop state. The
 *  timeline starts running from beat 0 at the initial tempo when
 *  constructed. The timeline always advances at a speed defined by
 *  its current tempo, even if transport is stopped. Synchronizing to the
 *  transport start/stop state of Link is optional for every peer.
 *  The transport start/stop state is only shared with other peers when
 *  start/stop synchronization is enabled.
 *
 *  A Link instance is initially disabled after construction, which
 *  means that it will not communicate on the network. Once enabled,
 *  a Link instance initiates network communication in an effort to
 *  discover other peers. When peers are discovered, they immediately
 *  become part of a shared Link session.
 *
 *  Each method of the Link type documents its thread-safety and
 *  realtime-safety properties. When a method is marked thread-safe,
 *  it means it is safe to call from multiple threads
 *  concurrently. When a method is marked realtime-safe, it means that
 *  it does not block and is appropriate for use in the thread that
 *  performs audio IO.
 *
 *  Link provides one session state capture/commit method pair for use
 *  in the audio thread and one for all other application contexts. In
 *  general, modifying the session state should be done in the audio
 *  thread for the most accurate timing results. The ability to modify
 *  the session state from application threads should only be used in
 *  cases where an application's audio thread is not actively running
 *  or if it doesn't generate audio at all. Modifying the Link session
 *  state from both the audio thread and an application thread
 *  concurrently is not advised and will potentially lead to unexpected
 *  behavior.
 *
 *  Only use the BasicLink class if the default platform clock does not
 *  fulfill other requirements of the client application. Please note this
 *  will require providing a custom Clock implementation. See the clock()
 *  documentation for details.
 */
template <typename Clock>
class BasicLink
{
public:
  class SessionState;

  /*! @brief Construct with an initial tempo. */
  BasicLink(double bpm);

  /*! @brief Link instances cannot be copied or moved */
  BasicLink(const BasicLink<Clock>&) = delete;
  BasicLink& operator=(const BasicLink<Clock>&) = delete;
  BasicLink(BasicLink<Clock>&&) = delete;
  BasicLink& operator=(BasicLink<Clock>&&) = delete;

  /*! @brief Is Link currently enabled?
   *  Thread-safe: yes
   *  Realtime-safe: yes
   */
  bool isEnabled() const;

  /*! @brief Enable/disable Link.
   *  Thread-safe: yes
   *  Realtime-safe: no
   */
  void enable(bool bEnable);

  /*! @brief: Is start/stop synchronization enabled?
   *  Thread-safe: yes
   *  Realtime-safe: no
   */
  bool isStartStopSyncEnabled() const;

  /*! @brief: Enable start/stop synchronization.
   *  Thread-safe: yes
   *  Realtime-safe: no
   */
  void enableStartStopSync(bool bEnable);

  /*! @brief How many peers are currently connected in a Link session?
   *  Thread-safe: yes
   *  Realtime-safe: yes
   */
  std::size_t numPeers() const;

  /*! @brief Register a callback to be notified when the number of
   *  peers in the Link session changes.
   *  Thread-safe: yes
   *  Realtime-safe: no
   *
   *  @discussion The callback is invoked on a Link-managed thread.
   *
   *  @param callback The callback signature is:
   *  void (std::size_t numPeers)
   */
  template <typename Callback>
  void setNumPeersCallback(Callback callback);

  /*! @brief Register a callback to be notified when the session
   *  tempo changes.
   *  Thread-safe: yes
   *  Realtime-safe: no
   *
   *  @discussion The callback is invoked on a Link-managed thread.
   *
   *  @param callback The callback signature is: void (double bpm)
   */
  template <typename Callback>
  void setTempoCallback(Callback callback);

  /*! brief: Register a callback to be notified when the state of
   *  start/stop isPlaying changes.
   *  Thread-safe: yes
   *  Realtime-safe: no
   *
   *  @discussion The callback is invoked on a Link-managed thread.
   *
   *  @param callback The callback signature is:
   *  void (bool isPlaying)
   */
  template <typename Callback>
  void setStartStopCallback(Callback callback);

  /*! @brief The clock used by Link.
   *  Thread-safe: yes
   *  Realtime-safe: yes
   *
   *  @discussion The Clock type is a platform-dependent representation
   *  of the system clock. It exposes a micros() method, which is a
   *  normalized representation of the current system time in
   *  std::chrono::microseconds.
   */
  Clock clock() const;

  /*! @brief Capture the current Link Session State from the audio thread.
   *  Thread-safe: no
   *  Realtime-safe: yes
   *
   *  @discussion This method should ONLY be called in the audio thread
   *  and must not be accessed from any other threads. The returned
   *  object stores a snapshot of the current Link Session State, so it
   *  should be captured and used in a local scope. Storing the
   *  Session State for later use in a different context is not advised
   *  because it will provide an outdated view.
   */
  SessionState captureAudioSessionState() const;

  /*! @brief Commit the given Session State to the Link session from the
   *  audio thread.
   *  Thread-safe: no
   *  Realtime-safe: yes
   *
   *  @discussion This method should ONLY be called in the audio
   *  thread. The given Session State will replace the current Link
   *  state. Modifications will be communicated to other peers in the
   *  session.
   */
  void commitAudioSessionState(SessionState state);

  /*! @brief Capture the current Link Session State from an application
   *  thread.
   *  Thread-safe: yes
   *  Realtime-safe: no
   *
   *  @discussion Provides a mechanism for capturing the Link Session
   *  State from an application thread (other than the audio thread).
   *  The returned Session State stores a snapshot of the current Link
   *  state, so it should be captured and used in a local scope.
   *  Storing the it for later use in a different context is not
   *  advised because it will provide an outdated view.
   */
  SessionState captureAppSessionState() const;

  /*! @brief Commit the given Session State to the Link session from an
   *  application thread.
   *  Thread-safe: yes
   *  Realtime-safe: no
   *
   *  @discussion The given Session State will replace the current Link
   *  Session State. Modifications of the Session State will be
   *  communicated to other peers in the session.
   */
  void commitAppSessionState(SessionState state);

  /*! @class SessionState
   *  @brief Representation of a timeline and the start/stop state
   *
   *  @discussion A SessionState object is intended for use in a local scope within
   *  a single thread - none of its methods are thread-safe. All of its methods are
   *  non-blocking, so it is safe to use from a realtime thread.
   *  It provides functions to observe and manipulate the timeline and start/stop
   *  state.
   *
   *  The timeline is a representation of a mapping between time and beats for varying
   *  quanta.
   *  The start/stop state represents the user intention to start or stop transport at
   *  a specific time. Start stop synchronization is an optional feature that allows to
   *  share the user request to start or stop transport between a subgroup of peers in
   *  a Link session. When observing a change of start/stop state, audio playback of a
   *  peer should be started or stopped the same way it would have happened if the user
   *  had requested that change at the according time locally. The start/stop state can
   *  only be changed by the user. This means that the current local start/stop state
   *  persists when joining or leaving a Link session. After joining a Link session
   *  start/stop change requests will be communicated to all connected peers.
   */
  class SessionState
  {
  public:
    SessionState(const link::ApiState state, const bool bRespectQuantum);

    /*! @brief: The tempo of the timeline, in Beats Per Minute.
     *
     *  @discussion This is a stable value that is appropriate for display
     *  to the user. Beat time progress will not necessarily match this tempo
     *  exactly because of clock drift compensation.
     */
    double tempo() const;

    /*! @brief: Set the timeline tempo to the given bpm value, taking
     *  effect at the given time.
     */
    void setTempo(double bpm, std::chrono::microseconds atTime);

    /*! @brief: Get the beat value corresponding to the given time
     *  for the given quantum.
     *
     *  @discussion: The magnitude of the resulting beat value is
     *  unique to this Link instance, but its phase with respect to
     *  the provided quantum is shared among all session
     *  peers. For non-negative beat values, the following
     *  property holds: fmod(beatAtTime(t, q), q) == phaseAtTime(t, q)
     */
    double beatAtTime(std::chrono::microseconds time, double quantum) const;

    /*! @brief: Get the session phase at the given time for the given
     *  quantum.
     *
     *  @discussion: The result is in the interval [0, quantum). The
     *  result is equivalent to fmod(beatAtTime(t, q), q) for
     *  non-negative beat values. This method is convenient if the
     *  client is only interested in the phase and not the beat
     *  magnitude. Also, unlike fmod, it handles negative beat values
     *  correctly.
     */
    double phaseAtTime(std::chrono::microseconds time, double quantum) const;

    /*! @brief: Get the time at which the given beat occurs for the
     *  given quantum.
     *
     *  @discussion: The inverse of beatAtTime, assuming a constant
     *  tempo. beatAtTime(timeAtBeat(b, q), q) === b.
     */
    std::chrono::microseconds timeAtBeat(double beat, double quantum) const;

    /*! @brief: Attempt to map the given beat to the given time in the
     *  context of the given quantum.
     *
     *  @discussion: This method behaves differently depending on the
     *  state of the session. If no other peers are connected,
     *  then this instance is in a session by itself and is free to
     *  re-map the beat/time relationship whenever it pleases. In this
     *  case, beatAtTime(time, quantum) == beat after this method has
     *  been called.
     *
     *  If there are other peers in the session, this instance
     *  should not abruptly re-map the beat/time relationship in the
     *  session because that would lead to beat discontinuities among
     *  the other peers. In this case, the given beat will be mapped
     *  to the next time value greater than the given time with the
     *  same phase as the given beat.
     *
     *  This method is specifically designed to enable the concept of
     *  "quantized launch" in client applications. If there are no other
     *  peers in the session, then an event (such as starting
     *  transport) happens immediately when it is requested. If there
     *  are other peers, however, we wait until the next time at which
     *  the session phase matches the phase of the event, thereby
     *  executing the event in-phase with the other peers in the
     *  session. The client only needs to invoke this method to
     *  achieve this behavior and should not need to explicitly check
     *  the number of peers.
     */
    void requestBeatAtTime(double beat, std::chrono::microseconds time, double quantum);

    /*! @brief: Rudely re-map the beat/time relationship for all peers
     *  in a session.
     *
     *  @discussion: DANGER: This method should only be needed in
     *  certain special circumstances. Most applications should not
     *  use it. It is very similar to requestBeatAtTime except that it
     *  does not fall back to the quantizing behavior when it is in a
     *  session with other peers. Calling this method will
     *  unconditionally map the given beat to the given time and
     *  broadcast the result to the session. This is very anti-social
     *  behavior and should be avoided.
     *
     *  One of the few legitimate uses of this method is to
     *  synchronize a Link session with an external clock source. By
     *  periodically forcing the beat/time mapping according to an
     *  external clock source, a peer can effectively bridge that
     *  clock into a Link session. Much care must be taken at the
     *  application layer when implementing such a feature so that
     *  users do not accidentally disrupt Link sessions that they may
     *  join.
     */
    void forceBeatAtTime(double beat, std::chrono::microseconds time, double quantum);

    /*! @brief: Set if transport should be playing or stopped, taking effect
     *  at the given time.
     */
    void setIsPlaying(bool isPlaying, std::chrono::microseconds time);

    /*! @brief: Is transport playing? */
    bool isPlaying() const;

    /*! @brief: Get the time at which a transport start/stop occurs */
    std::chrono::microseconds timeForIsPlaying() const;

    /*! @brief: Convenience function to attempt to map the given beat to the time
     *  when transport is starting to play in context of the given quantum.
     *  This function evaluates to a no-op if isPlaying() equals false.
     */
    void requestBeatAtStartPlayingTime(double beat, double quantum);

    /*! @brief: Convenience function to start or stop transport at a given time and
     *  attempt to map the given beat to this time in context of the given quantum.
     */
    void setIsPlayingAndRequestBeatAtTime(bool isPlaying,
                                          std::chrono::microseconds time,
                                          double beat,
                                          double quantum);

  private:
    friend BasicLink<Clock>;
    link::ApiState mOriginalState;
    link::ApiState mState;
    bool mbRespectQuantum;
  };

private:
  using Controller = ableton::link::Controller<link::PeerCountCallback,
                                               link::TempoCallback,
                                               link::StartStopStateCallback,
                                               Clock,
                                               link::platform::Random,
                                               link::platform::IoContext>;

  std::mutex mCallbackMutex;
  link::PeerCountCallback mPeerCountCallback = [](std::size_t) {};
  link::TempoCallback mTempoCallback = [](link::Tempo) {};
  link::StartStopStateCallback mStartStopCallback = [](bool) {};
  Clock mClock;
  Controller mController;
};

class Link : public BasicLink<link::platform::Clock>
{
public:
  using Clock = link::platform::Clock;

  Link(double bpm)
    : BasicLink(bpm)
  {
  }
};

} // namespace ableton

#include <ableton/Link.ipp>
