/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once

#include "ui/effects/animation_value.h"
#include "core/file_location.h"
#include "data/data_audio_msg_id.h"
#include "base/bytes.h"
#include "base/timer.h"

#include <QtCore/QTimer>

namespace Ui {
struct PreparedFileInformation;
} // namespace Ui

namespace Media {
struct ExternalSoundData;
struct ExternalSoundPart;
} // namespace Media

namespace Media {
namespace Streaming {
struct TimePoint;
} // namespace Streaming
} // namespace Media

namespace Webrtc {
struct DeviceResolvedId;
} // namespace Webrtc

namespace Media {
namespace Audio {

class Instance;

// Thread: Main.
void Start(not_null<Instance*> instance);
void Finish(not_null<Instance*> instance);

// Thread: Main. Locks: AudioMutex.
bool IsAttachedToDevice();

// Thread: Any. Must be locked: AudioMutex.
bool AttachToDevice();

// Thread: Any.
void ScheduleDetachFromDeviceSafe();
void ScheduleDetachIfNotUsedSafe();
void StopDetachIfNotUsedSafe();
bool SupportsSpeedControl();

} // namespace Audio

namespace Player {

constexpr auto kDefaultFrequency = 48000; // 48 kHz
constexpr auto kTogetherLimit = 4;
constexpr auto kWaveformSamplesCount = 100;

class Fader;
class Loaders;

[[nodiscard]] rpl::producer<AudioMsgId> Updated();

float64 ComputeVolume(AudioMsgId::Type type);

enum class State {
	Stopped = 0x01,
	StoppedAtEnd = 0x02,
	StoppedAtError = 0x03,
	StoppedAtStart = 0x04,

	Starting = 0x08,
	Playing = 0x10,
	Stopping = 0x18,
	Pausing = 0x20,
	Paused = 0x28,
	PausedAtEnd = 0x30,
	Resuming = 0x38,
};

inline bool IsStopped(State state) {
	return (state == State::Stopped)
		|| (state == State::StoppedAtEnd)
		|| (state == State::StoppedAtError)
		|| (state == State::StoppedAtStart);
}

inline bool IsStoppedOrStopping(State state) {
	return IsStopped(state) || (state == State::Stopping);
}

inline bool IsStoppedAtEnd(State state) {
	return (state == State::StoppedAtEnd);
}

inline bool IsPaused(State state) {
	return (state == State::Paused)
		|| (state == State::PausedAtEnd);
}

inline bool IsPausedOrPausing(State state) {
	return IsPaused(state) || (state == State::Pausing);
}

inline bool IsFading(State state) {
	return (state == State::Starting)
		|| (state == State::Stopping)
		|| (state == State::Pausing)
		|| (state == State::Resuming);
}

inline bool IsActive(State state) {
	return !IsStopped(state) && !IsPaused(state);
}

inline bool ShowPauseIcon(State state) {
	return !IsStoppedOrStopping(state)
		&& !IsPausedOrPausing(state);
}

struct TrackState {
	AudioMsgId id;
	State state = State::Stopped;
	int64 position = 0;
	int64 receivedTill = 0;
	int64 length = 0;
	int frequency = kDefaultFrequency;
	int fileHeaderSize = 0;
	bool waitingForData = false;
};

class Mixer final : public QObject {
	Q_OBJECT

public:
	explicit Mixer(not_null<Audio::Instance*> instance);

	void play(
		const AudioMsgId &audio,
		std::unique_ptr<ExternalSoundData> externalData,
		crl::time positionMs);
	void pause(const AudioMsgId &audio, bool fast = false);
	void resume(const AudioMsgId &audio, bool fast = false);
	void stop(const AudioMsgId &audio);
	void stop(const AudioMsgId &audio, State state);

	// External player audio stream interface.
	void feedFromExternal(ExternalSoundPart &&part);
	void forceToBufferExternal(const AudioMsgId &audioId);

	// Thread: Main. Locks: AudioMutex.
	void setSpeedFromExternal(const AudioMsgId &audioId, float64 speed);

	Streaming::TimePoint getExternalSyncTimePoint(
		const AudioMsgId &audio) const;
	crl::time getExternalCorrectedTime(
		const AudioMsgId &id,
		crl::time frameMs,
		crl::time systemMs);

	void stopAndClear();

	TrackState currentState(AudioMsgId::Type type);

	// Thread: Main. Must be locked: AudioMutex.
	void prepareToCloseDevice();

	// Thread: Main. Must be locked: AudioMutex.
	void reattachIfNeeded();

	// Thread: Any. Must be locked: AudioMutex.
	void reattachTracks();

	// Thread: Any.
	void setSongVolume(float64 volume);
	float64 getSongVolume() const;
	void setVideoVolume(float64 volume);
	float64 getVideoVolume() const;

	void scheduleFaderCallback();

	~Mixer();

private Q_SLOTS:
	void onError(const AudioMsgId &audio);
	void onStopped(const AudioMsgId &audio);

	void onUpdated(const AudioMsgId &audio);

Q_SIGNALS:
	void updated(const AudioMsgId &audio);
	void stoppedOnError(const AudioMsgId &audio);
	void loaderOnStart(const AudioMsgId &audio, qint64 positionMs);
	void loaderOnCancel(const AudioMsgId &audio);

	void suppressSong();
	void unsuppressSong();
	void suppressAll(qint64 duration);

private:
	class Track {
	public:
		static constexpr int kBuffersCount = 3;

		// Thread: Any. Must be locked: AudioMutex.
		void reattach(AudioMsgId::Type type);

		// Thread: Main. Must be locked: AudioMutex.
		void detach();
		void clear();

		void started();

		bool isStreamCreated() const;
		void ensureStreamCreated(AudioMsgId::Type type);

		int getNotQueuedBufferIndex();

		// Thread: Main. Must be locked: AudioMutex.
		void setExternalData(std::unique_ptr<ExternalSoundData> data);

		void updateStatePosition();
		void updateWithSpeedPosition();

		[[nodiscard]] static int64 SpeedIndependentPosition(
			int64 position,
			float64 speed);
		[[nodiscard]] static int64 SpeedDependentPosition(
			int64 position,
			float64 speed);

		~Track();

		TrackState state;

		Core::FileLocation file;
		QByteArray data;

		int format = 0;
		bool loading = false;
		bool loaded = false;
		bool waitingForBuffer = false;

		// Speed dependent values.
		float64 speed = 1.;
		float64 nextSpeed = 1.;
		struct WithSpeed {
			int64 fineTunedPosition = 0;
			int64 position = 0;
			int64 length = 0;
			int64 bufferedPosition = 0;
			int64 bufferedLength = 0;
			int64 fadeStartPosition = 0;
			int samples[kBuffersCount] = { 0 };
			QByteArray buffered[kBuffersCount];
		};
		WithSpeed withSpeed;

		struct Stream {
			uint32 source = 0;
			uint32 buffers[kBuffersCount] = { 0 };
		};
		Stream stream;

		std::unique_ptr<ExternalSoundData> externalData;

		crl::time lastUpdateWhen = 0;
		crl::time lastUpdatePosition = 0;

	private:
		void createStream(AudioMsgId::Type type);
		void destroyStream();
		void resetStream();

	};

	bool fadedStop(AudioMsgId::Type type, bool *fadedStart = 0);
	void resetFadeStartPosition(AudioMsgId::Type type, int positionInBuffered = -1);
	bool checkCurrentALError(AudioMsgId::Type type);

	void externalSoundProgress(const AudioMsgId &audio);

	// Thread: Any. Must be locked: AudioMutex.
	void setStoppedState(Track *current, State state = State::Stopped);

	Track *trackForType(AudioMsgId::Type type, int index = -1); // -1 uses currentIndex(type)
	const Track *trackForType(AudioMsgId::Type type, int index = -1) const;
	int *currentIndex(AudioMsgId::Type type);
	const int *currentIndex(AudioMsgId::Type type) const;

	const not_null<Audio::Instance*> _instance;

	int _audioCurrent = 0;
	Track _audioTracks[kTogetherLimit];

	int _songCurrent = 0;
	Track _songTracks[kTogetherLimit];

	Track _videoTrack;

	QAtomicInt _volumeVideo;
	QAtomicInt _volumeSong;

	friend class Fader;
	friend class Loaders;

	QThread _faderThread, _loaderThread;
	Fader *_fader;
	Loaders *_loader;

	rpl::lifetime _lifetime;

};

Mixer *mixer();

class Fader : public QObject {
	Q_OBJECT

public:
	Fader(QThread *thread);

	void songVolumeChanged();
	void videoVolumeChanged();

Q_SIGNALS:
	void error(const AudioMsgId &audio);
	void playPositionUpdated(const AudioMsgId &audio);
	void audioStopped(const AudioMsgId &audio);
	void needToPreload(const AudioMsgId &audio);

public Q_SLOTS:
	void onInit();
	void onTimer();

	void onSuppressSong();
	void onUnsuppressSong();
	void onSuppressAll(qint64 duration);

private:
	enum {
		EmitError = 0x01,
		EmitStopped = 0x02,
		EmitPositionUpdated = 0x04,
		EmitNeedToPreload = 0x08,
	};
	int32 updateOnePlayback(Mixer::Track *track, bool &hasPlaying, bool &hasFading, float64 volumeMultiplier, bool volumeChanged);
	void setStoppedState(Mixer::Track *track, State state = State::Stopped);

	QTimer _timer;

	bool _volumeChangedSong = false;
	bool _volumeChangedVideo = false;

	bool _suppressAll = false;
	bool _suppressAllAnim = false;
	bool _suppressSong = false;
	bool _suppressSongAnim = false;
	anim::value _suppressVolumeAll;
	anim::value _suppressVolumeSong;
	crl::time _suppressAllStart = 0;
	crl::time _suppressAllEnd = 0;
	crl::time _suppressSongStart = 0;

};

[[nodiscard]] Ui::PreparedFileInformation PrepareForSending(
	const QString &fname,
	const QByteArray &data);

namespace internal {

// Thread: Any. Must be locked: AudioMutex.
bool CheckAudioDeviceConnected();

// Thread: Main. Locks: AudioMutex.
void DetachFromDevice(not_null<Audio::Instance*> instance);
bool DetachIfDeviceChanged(
	not_null<Audio::Instance*> instance,
	const Webrtc::DeviceResolvedId &nowDeviceId);

// Thread: Any.
QMutex *audioPlayerMutex();

// Thread: Any.
bool audioCheckError();

} // namespace internal

} // namespace Player
} // namespace Media

VoiceWaveform audioCountWaveform(const Core::FileLocation &file, const QByteArray &data);

namespace Media {
namespace Audio {

TG_FORCE_INLINE uint16 ReadOneSample(uchar data) {
	return qAbs((static_cast<int16>(data) - 0x80) * 0x100);
}

TG_FORCE_INLINE uint16 ReadOneSample(int16 data) {
	return qAbs(data);
}

template <typename SampleType, typename Callback>
void IterateSamples(bytes::const_span bytes, Callback &&callback) {
	auto samplesPointer = reinterpret_cast<const SampleType*>(bytes.data());
	auto samplesCount = bytes.size() / sizeof(SampleType);
	auto samplesData = gsl::make_span(samplesPointer, samplesCount);
	for (auto sampleData : samplesData) {
		callback(ReadOneSample(sampleData));
	}
}

} // namespace Audio
} // namespace Media
