/*
 * Copyright (C) 2018-2019 Paul Davis <paul@linuxaudiosystems.com>
 *
 * 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#pragma once

#include <vector>

#include <optional>

#include <glibmm/timer.h>

#include <ltc.h>

#include "pbd/properties.h"
#include "pbd/mutex.h"
#include "pbd/signals.h"
#include "pbd/statefuldestructible.h"

#include "temporal/time.h"

#include "ardour/libardour_visibility.h"
#include "ardour/region.h" /* for Properties::locked */
#include "ardour/types.h"

#include "midi++/parser.h"
#include "midi++/types.h"

namespace ARDOUR {

class Session;
class AudioEngine;
class Location;
class MidiPort;
class AudioPort;
class Port;

namespace Properties {
	LIBARDOUR_API extern PBD::PropertyDescriptor<bool>                         fr2997;
	LIBARDOUR_API extern PBD::PropertyDescriptor<bool>                         collect;
	LIBARDOUR_API extern PBD::PropertyDescriptor<bool>                         connected;
	LIBARDOUR_API extern PBD::PropertyDescriptor<bool>                         sclock_synced;
	LIBARDOUR_API extern PBD::PropertyDescriptor<ARDOUR::TransportRequestType> allowed_transport_requests;
}

struct LIBARDOUR_API SafeTime {
	/* This object uses memory fences to provide psuedo-atomic updating of
	 * non-atomic data. If after reading guard1 and guard2 with correct
	 * memory fencing they have the same value, then we know that the other
	 * members are all internally consistent.
	 *
	 * Traditionally, one might do this with a mutex, but this object
	 * provides lock-free write update. The reader might block while
	 * waiting for consistency, but this is extraordinarily unlikely. In
	 * this sense, the design is similar to a spinlock.
	 *
	 * any update starts by incrementing guard1, with a memory fence to
	 * ensure no reordering of this w.r.t later operations.
	 *
	 * then we update the "non-atomic" data members.
	 *
	 * then we update guard2, with another memory fence to prevent
	 * reordering.
	 *
	 * ergo, if guard1 == guard2, the update of the non-atomic members is
	 * complete and the values stored there are consistent.
	 */

	std::atomic<int> guard1;
	samplepos_t      position;
	samplepos_t      timestamp;
	double           speed;
	std::atomic<int> guard2;

	SafeTime ()
	{
		guard1.store (0);
		position  = 0;
		timestamp = 0;
		speed     = 0;
		guard2.store (0);
	}

	void reset ()
	{
		guard1.store (0);
		position  = 0;
		timestamp = 0;
		speed     = 0;
		guard2.store (0);
	}

	void update (samplepos_t p, samplepos_t t, double s)
	{
		guard1.fetch_add (1, std::memory_order_acquire);
		position  = p;
		timestamp = t;
		speed     = s;
		guard2.fetch_add (1, std::memory_order_acquire);
	}

	void safe_read (SafeTime& dst) const
	{
		int tries = 0;

		do {
			if (tries == 10) {
				std::cerr << "SafeTime: atomic read of current time failed, sleeping!" << std::endl;
				Glib::usleep (20);
				tries = 0;
			}
			dst.guard1.store (guard1.load (std::memory_order_seq_cst), std::memory_order_seq_cst);
			dst.position  = position;
			dst.timestamp = timestamp;
			dst.speed     = speed;
			dst.guard2.store (guard2.load (std::memory_order_seq_cst), std::memory_order_seq_cst);
			tries++;

		} while (dst.guard1.load (std::memory_order_seq_cst) != dst.guard2.load (std::memory_order_seq_cst));
	}
};

/**
 * @class TransportMaster
 *
 * @brief The TransportMaster interface can be used to sync ARDOURs tempo to an external source
 * like MTC, MIDI Clock, etc. as well as a single internal pseudo master we
 * call "UI" because it is controlled from any of the user interfaces for
 * Ardour (GUI, control surfaces, OSC, etc.)
 *
 */
class LIBARDOUR_API TransportMaster : public PBD::Stateful
{
public:
	TransportMaster (SyncSource t, std::string const& name);
	virtual ~TransportMaster ();

	static std::shared_ptr<TransportMaster> factory (SyncSource, std::string const&, bool removeable);
	static std::shared_ptr<TransportMaster> factory (XMLNode const&);

	virtual void pre_process (pframes_t nframes, samplepos_t now, std::optional<samplepos_t>) = 0;

	/**
	 * This is the most important function to implement:
	 * Each process cycle, Session::follow_slave will call this method.
	 *  and after the method call they should
	 *
	 * Session::follow_slave will then try to follow the given
	 * <em>position</em> using a delay locked loop (DLL),
	 * starting with the first given transport speed.
	 * If the values of speed and position contradict each other,
	 * ARDOUR will always follow the position and disregard the speed.
	 * Although, a correct speed is important so that ARDOUR
	 * can sync to the master time source quickly.
	 *
	 * For background information on delay locked loops,
	 * see http://www.kokkinizita.net/papers/usingdll.pdf
	 *
	 * The method has the following precondition:
	 * <ul>
	 *   <li>
	 *       TransportMaster::ok() should return true, otherwise playback will stop
	 *       immediately and the method will not be called
	 *   </li>
	 *   <li>
	 *     when the references speed and position are passed into the TransportMaster
	 *     they are uninitialized
	 *   </li>
	 * </ul>
	 *
	 * After the method call the following postconditions should be met:
	 * <ul>
	 *    <li>
	 *       The first position value on transport start should be 0,
	 *       otherwise ARDOUR will try to locate to the new position
	 *       rather than move to it
	 *    </li>
	 *    <li>
	 *      the references speed and position should be assigned
	 *      to the TransportMasters current requested transport speed
	 *      and transport position.
	 *    </li>
	 *   <li>
	 *     TransportMaster::resolution() should be greater than the maximum distance of
	 *     ARDOURs transport position to the slaves requested transport position.
	 *   </li>
	 *   <li>TransportMaster::locked() should return true, otherwise Session::no_roll will be called</li>
	 *   <li>TransportMaster::starting() should be false, otherwise the transport will not move until it becomes true</li>
	 * </ul>
	 *
	 * @param speed - The transport speed requested
	 * @param position - The transport position requested
	 * @param lp last position (used for flywheel)
	 * @param when last timestamp (used for flywheel)
	 * @param now monotonic sample time
	 * @return - The return value is currently ignored (see Session::follow_slave)
	 */
	virtual bool speed_and_position (double& speed, samplepos_t& position, samplepos_t& lp, samplepos_t& when, samplepos_t now);

	virtual void reset (bool with_position) = 0;

	/**
	 * reports to ARDOUR whether the TransportMaster is currently synced to its external
	 * time source.
	 *
	 * @return - when returning false, the transport will stop rolling
	 */
	virtual bool locked () const = 0;

	/**
	 * reports to ARDOUR whether the slave is in a sane state
	 *
	 * @return - when returning false, the transport will be stopped and the slave
	 * disconnected from ARDOUR.
	 */
	virtual bool ok () const = 0;

	/**
	 * reports to ARDOUR whether it is possible to use this slave
	 *
	 * @return - true if the slave can be used.
	 *
	 * Only the JACK ("Engine") slave is ever likely to return false,
	 * if JACK is not being used for the Audio/MIDI backend.
	 */
	virtual bool usable () const
	{
		return true;
	}

	/**
	 * reports to ARDOUR whether the slave is in the process of starting
	 * to roll
	 *
	 * @return - when returning false, transport will not move until this method returns true
	 */
	virtual bool starting () const
	{
		return false;
	}

	/**
	 * @return - the timing resolution of the TransportMaster - If the distance of ARDOURs transport
	 * to the slave becomes greater than the resolution, sound will stop
	 */
	virtual samplecnt_t resolution () const = 0;

	/**
	 * @return - the expected update interval for the data source used by
	 * this transport master. Even if the data is effectively continuous,
	 * this number indicates how long it is between changes to the known
	 * position of the master.
	 */
	virtual samplecnt_t update_interval () const = 0;

	/**
	 * @return - when returning true, ARDOUR will wait for seekahead_distance() before transport
	 * starts rolling
	 */
	virtual bool requires_seekahead () const = 0;

	/**
	 * @return the number of samples that this slave wants to seek ahead. Relevant
	 * only if requires_seekahead() returns true.
	 */

	virtual samplecnt_t seekahead_distance () const
	{
		return 0;
	}

	/**
	 * @return - when returning true, ARDOUR will use transport speed 1.0 no matter what
	 *           the slave returns
	 */
	virtual bool sample_clock_synced () const
	{
		return _sclock_synced;
	}
	virtual void set_sample_clock_synced (bool);

	/**
	 * @return - current time-delta between engine and sync-source
	 */
	virtual std::string delta_string () const
	{
		return "";
	}

	sampleoffset_t current_delta () const
	{
		return _current_delta;
	}

	/* this is intended to be used by a UI and polled from a timeout. it should
	   return a string describing the current position of the TC source. it
	   should NOT do any computation, but should use a cached value
	   of the TC source position.
	*/
	virtual std::string position_string () const = 0;

	virtual bool can_loop () const
	{
		return false;
	}

	virtual Location* loop_location () const
	{
		return 0;
	}
	bool has_loop () const
	{
		return loop_location () != 0;
	}

	SyncSource type () const
	{
		return _type;
	}
	TransportRequestSource request_type () const
	{
		switch (_type) {
			case Engine: /* also JACK */
				return TRS_Engine;
			case MTC:
				return TRS_MTC;
			case LTC:
				return TRS_LTC;
			case MIDIClock:
				break;
		}
		return TRS_MIDIClock;
	}

	std::string name () const
	{
		return _name;
	}
	void set_name (std::string const&);

	int      set_state (XMLNode const&, int);
	XMLNode& get_state () const;

	static const std::string state_node_name;
	static void              make_property_quarks ();

	virtual void set_session (Session*);

	std::shared_ptr<Port> port () const
	{
		return _port;
	}

	bool         check_collect ();
	virtual void set_collect (bool);
	bool         collect () const
	{
		return _collect;
	}

	/* called whenever the manager starts collecting (processing) this
	   transport master. Typically will re-initialize any state used to
	   deal with incoming data.
	*/
	virtual void init () = 0;

	virtual void check_backend () {}
	virtual bool allow_request (TransportRequestSource, TransportRequestType) const;
	std::string  allowed_request_string () const;

	TransportRequestType request_mask () const
	{
		return _request_mask;
	}
	void set_request_mask (TransportRequestType);

	/* this is set at construction, and not changeable later, so it is not
	 * a property
	 */

	bool removeable () const
	{
		return _removeable;
	}
	void set_removeable (bool yn)
	{
		_removeable = yn;
	}

	std::string display_name (bool sh /*ort*/ = true) const;

	virtual void unregister_port ();
	void         connect_port_using_state ();
	virtual void create_port () = 0;

protected:
	SyncSource                          _type;
	PBD::Property<std::string>          _name;
	Session*                            _session;
	sampleoffset_t                      _current_delta;
	bool                                _pending_collect;
	bool                                _removeable;
	PBD::Property<TransportRequestType> _request_mask; /* lists transport requests still accepted when we're in control */
	PBD::Property<bool>                 _sclock_synced;
	PBD::Property<bool>                 _collect;
	PBD::Property<bool>                 _connected;

	SafeTime current;

	/* DLL - chase incoming data */

	int transport_direction;
	int dll_initstate;

	double t0;
	double t1;
	double e2;
	double b, c;

	std::shared_ptr<Port> _port;

	mutable XMLNode port_node;

	virtual void connection_handler (std::weak_ptr<ARDOUR::Port>, std::string name1, std::weak_ptr<ARDOUR::Port>, std::string name2, bool yn);

	PBD::ScopedConnection port_connection;
	PBD::ScopedConnection backend_connection;

	virtual void register_properties ();

	virtual std::string format_delta_time (sampleoffset_t) const;
};

/** a helper class for any TransportMaster that receives its input via a MIDI
 * port
 */
class LIBARDOUR_API TransportMasterViaMIDI
{
public:
	virtual ~TransportMasterViaMIDI ();

	MIDI::Parser& transport_parser () { return parser; }
	std::shared_ptr<MidiPort> midi_port () const
	{
		return _midi_port;
	}
	std::shared_ptr<Port> create_midi_port (std::string const& port_name);

	virtual void set_session (Session*);

protected:
	TransportMasterViaMIDI () {};

	void resync_latency (bool);
	MIDI::Parser                parser;
	std::shared_ptr<MidiPort> _midi_port;

	virtual void parameter_changed (std::string const& p) {}

	LatencyRange midi_port_latency;

private:
	PBD::ScopedConnectionList session_connections;
};

class LIBARDOUR_API TimecodeTransportMaster : public TransportMaster
{
public:
	TimecodeTransportMaster (std::string const& name, SyncSource type);

	virtual Timecode::TimecodeFormat apparent_timecode_format () const = 0;

	bool apparent_timecode_format_valid () const {
		return timecode_format_valid;
	}

	bool fr2997 () const
	{
		return _fr2997;
	}
	void set_fr2997 (bool);

protected:
	void register_properties ();

	samplepos_t timecode_offset;
	bool        timecode_negative_offset;
	bool        timecode_format_valid;

private:
	PBD::Property<bool> _fr2997;
};

class LIBARDOUR_API MTC_TransportMaster : public TimecodeTransportMaster, public TransportMasterViaMIDI
{
public:
	MTC_TransportMaster (std::string const&);
	~MTC_TransportMaster ();

	void set_session (Session*);

	void pre_process (pframes_t nframes, samplepos_t now, std::optional<samplepos_t>);

	void unregister_port ();

	void reset (bool with_pos);
	bool locked () const;
	bool ok () const;
	void handle_locate (const MIDI::byte*);

	samplecnt_t update_interval () const;
	samplecnt_t resolution () const;
	bool        requires_seekahead () const
	{
		return false;
	}
	samplecnt_t seekahead_distance () const;
	void        init ();

	Timecode::TimecodeFormat apparent_timecode_format () const;
	std::string              position_string () const;
	std::string              delta_string () const;

	void create_port ();

private:
	PBD::ScopedConnectionList port_connections;
	bool                      can_notify_on_unknown_rate;

	static const int sample_tolerance;

	samplepos_t              mtc_frame; /* current time */
	double                   mtc_frame_dll;
	samplepos_t              last_inbound_frame; /* when we got it; audio clocked */
	MIDI::byte               last_mtc_fps_byte;
	samplepos_t              window_begin;
	samplepos_t              window_end;
	samplepos_t              first_mtc_timestamp;

	PBD::Mutex     reset_lock;
	uint32_t                 reset_pending;
	bool                     reset_position;
	int                      transport_direction;
	int                      busy_guard1;
	int                      busy_guard2;

	double                   quarter_frame_duration;
	Timecode::TimecodeFormat mtc_timecode;
	Timecode::Time           timecode;

	void queue_reset (bool with_pos);
	void maybe_reset ();

	void update_mtc_qtr (MIDI::Parser&, int, samplepos_t);
	void update_mtc_time (const MIDI::byte*, bool, samplepos_t);
	void update_mtc_status (MIDI::MTC_Status);
	void reset_window (samplepos_t);
	bool outside_window (samplepos_t) const;
	void init_mtc_dll (samplepos_t, double);
	void parse_timecode_offset ();
	void parameter_changed (std::string const& p);

	void connection_handler (std::weak_ptr<ARDOUR::Port>, std::string, std::weak_ptr<ARDOUR::Port>, std::string, bool);
};

class LIBARDOUR_API LTC_TransportMaster : public TimecodeTransportMaster
{
public:
	LTC_TransportMaster (std::string const&);
	~LTC_TransportMaster ();

	void set_session (Session*);

	void pre_process (pframes_t nframes, samplepos_t now, std::optional<samplepos_t>);

	void reset (bool with_pos);
	bool locked () const;
	bool ok () const;

	samplecnt_t update_interval () const;
	samplecnt_t resolution () const;
	bool        requires_seekahead () const
	{
		return false;
	}
	samplecnt_t seekahead_distance () const
	{
		return 0;
	}
	void init ();
	void set_sample_clock_synced (bool);

	Timecode::TimecodeFormat apparent_timecode_format () const;
	std::string              position_string () const;
	std::string              delta_string () const;

	void create_port ();

private:
	void parse_ltc (const pframes_t, Sample const*, samplecnt_t);
	void process_ltc (samplepos_t const);
	void init_dll (samplepos_t, int32_t);
	bool detect_discontinuity (LTCFrameExt*, int, bool);
	bool detect_ltc_fps (int, bool);
	bool equal_ltc_sample_time (LTCFrame* a, LTCFrame* b);
	void resync_xrun ();
	void resync_latency (bool);
	void parse_timecode_offset ();
	void parameter_changed (std::string const& p);
	void connection_handler (std::weak_ptr<ARDOUR::Port>, std::string, std::weak_ptr<ARDOUR::Port>, std::string, bool);

	LTCDecoder*    decoder;
	double         samples_per_ltc_frame;
	Timecode::Time timecode;
	LTCFrameExt    prev_frame;
	bool           fps_detected;

	samplecnt_t    monotonic_cnt;
	uint64_t       frames_since_reset;
	int            delayedlocked;

	int            ltc_detect_fps_cnt;
	int            ltc_detect_fps_max;
	bool           sync_lock_broken;
	double         samples_per_timecode_frame;

	PBD::ScopedConnection     port_connection;
	PBD::ScopedConnectionList session_connections;
	LatencyRange              ltc_slave_latency;


	struct Biquad {
		void reset () { z1 = z2 = 0;}
		float  z1, z2;
		double a1, a2, b0, b1, b2;
	};

	bool   _filter_enable;
	Biquad _lpf;
	Biquad _hpf;
};

class LIBARDOUR_API MIDIClock_TransportMaster : public TransportMaster, public TransportMasterViaMIDI
{
public:
	MIDIClock_TransportMaster (std::string const& name, int ppqn = 24);

	/// Constructor for unit tests
	~MIDIClock_TransportMaster ();

	void set_session (Session*);

	void unregister_port ();

	void pre_process (pframes_t nframes, samplepos_t now, std::optional<samplepos_t>);

	void rebind (MidiPort&);

	void reset (bool with_pos);
	bool locked () const;
	bool ok () const;

	samplecnt_t update_interval () const;
	samplecnt_t resolution () const;
	bool        requires_seekahead () const
	{
		return false;
	}
	void init ();

	std::string position_string () const;
	std::string delta_string () const;

	float bpm () const
	{
		return _bpm;
	}

	void create_port ();

protected:
	PBD::ScopedConnectionList port_connections;

	/// pulses per quarter note for one MIDI clock sample (default 24)
	int ppqn;

	/// the duration of one ppqn in sample time
	double one_ppqn_in_samples;

	/// the timestamp of the first MIDI clock message
	samplepos_t first_timestamp;

	/// the time stamp and should-be transport position of the last inbound MIDI clock message
	samplepos_t last_timestamp;
	double      should_be_position;

	/// the number of midi clock messages received (zero-based)
	/// since start
	long midi_clock_count;

	/// a DLL to track MIDI clock

	double _speed;
	bool   _running;
	double _bpm;

	void start (MIDI::Parser& parser, samplepos_t timestamp);
	void contineu (MIDI::Parser& parser, samplepos_t timestamp); // we can't use continue because it is a C++ keyword
	void stop (MIDI::Parser& parser, samplepos_t timestamp);
	void position (MIDI::Parser& parser, MIDI::byte* message, size_t size, samplepos_t timestamp);

	void        calculate_one_ppqn_in_samples_at (samplepos_t time);
	samplepos_t calculate_song_position (uint16_t song_position_in_sixteenth_notes);
	void        calculate_filter_coefficients (double qpm);
	void        update_midi_clock (MIDI::Parser& parser, samplepos_t timestamp);

	void connection_handler (std::weak_ptr<ARDOUR::Port>, std::string, std::weak_ptr<ARDOUR::Port>, std::string, bool);
};

class LIBARDOUR_API Engine_TransportMaster : public TransportMaster
{
public:
	Engine_TransportMaster (AudioEngine&);
	~Engine_TransportMaster ();

	void pre_process (pframes_t nframes, samplepos_t now, std::optional<samplepos_t>);
	bool speed_and_position (double& speed, samplepos_t& pos, samplepos_t&, samplepos_t&, samplepos_t);

	bool starting () const
	{
		return _starting;
	}
	void        reset (bool with_position);
	bool        locked () const;
	bool        ok () const;
	bool        usable () const;
	samplecnt_t update_interval () const;
	samplecnt_t resolution () const
	{
		return 1;
	}
	bool requires_seekahead () const
	{
		return false;
	}
	bool sample_clock_synced () const
	{
		return true;
	}
	void init ();
	void check_backend ();
	bool allow_request (TransportRequestSource, TransportRequestType) const;

	std::string position_string () const;
	std::string delta_string () const;

	void create_port () {}

private:
	AudioEngine& engine;
	bool         _starting;
};

}

