#ifndef SOUNDDEVICE_HH
#define SOUNDDEVICE_HH

#include "EmuTime.hh"
#include "WavWriter.hh"
#include "static_string_view.hh"

#include <array>
#include <optional>
#include <string>
#include <string_view>

namespace openmsx {

class DeviceConfig;
class DynamicClock;
class MSXMixer;

class SoundDevice
{
public:
	static constexpr unsigned MAX_CHANNELS = 24;

	SoundDevice(const SoundDevice&) = delete;
	SoundDevice(SoundDevice&&) = delete;
	SoundDevice& operator=(const SoundDevice&) = delete;
	SoundDevice& operator=(SoundDevice&&) = delete;

	/** Get the unique name that identifies this sound device.
	  * Used to create setting names.
	  */
	[[nodiscard]] const std::string& getName() const { return name; }

	/** Gets a description of this sound device,
	  * to be presented to the user.
	  */
	[[nodiscard]] std::string_view getDescription() const { return description; }

	/** How many channels does this device have?
	 */
	[[nodiscard]] unsigned getNumChannels() const { return numChannels; }

	/** Are the individual channels of this device stereo?
	 */
	[[nodiscard]] bool hasStereoChannels() const {
		return stereo == 2;
	}

	/** Is the full output of this device stereo?
	  */
	[[nodiscard]] bool isStereo() const {
		return hasStereoChannels() || !balanceCenter;
	}

	/** Gets this device its 'amplification factor'.
	  *
	  * Each sample generated by the 'updateBuffer' method will get
	  * multiplied by this factor during mixing (see MSXMixer).
	  *
	  * This factor is the combination of:
	  *  - a fixed per device factor (see getAmplificationFactorImpl()).
	  *  - a MSX software controlled volume setting (e.g. used in
	  *    Carnivore2 emulation).
	  * The influence of the different volume settings is not part of this
	  * factor.
	  */
	struct AmplificationFactors {
		float left;
		float right;
	};
	[[nodiscard]] AmplificationFactors getAmplificationFactor() const {
		auto f = getAmplificationFactorImpl();
		return {.left = f * softwareVolumeLeft, .right = f * softwareVolumeRight};
	}

	/** Change the 'software volume' of this sound device.
	  *
	  * Some devices like 'Carnivore2' include sound chips (e.g. SCC and
	  * fm-pac) and allow the MSX software to configure the volume of
	  * those chips. To be clear: these are not the regular per channel
	  * volume registers of the SCC/YM2413 sound chips, but another
	  * register that controls the total output of those chips.
	  *
	  * This method allows to change that per-chip volume.
	  */
	void setSoftwareVolume(float volume, EmuTime time);
	void setSoftwareVolume(float left, float right, EmuTime time);

	void recordChannel(unsigned channel, const std::string& filename);
	void muteChannel  (unsigned channel, bool muted);

	/** Change the balance of a single channel.
	 * @param channel [0, numChannels)
	 * @param balance [-1, +1], -1: fully left, 0: center, +1: fully right
	 *                 The sum of the energy of the left and right channel is kept constant.
	 * NOTE: after calling setBalance() a number of times you must also call postSetBalance() once.
	 */
	void setBalance(unsigned channel, float balance);
	virtual void postSetBalance();

	/** Query the last generated audio signal for a specific channel.
	  * The length of this buffer is fixed (for a specific sound device),
	  * see getLastBufferSize().
	  * It's possible that no data is available yet. In that case an empty
	  * buffer will be returned. For example this will be the case the first
	  * time you call this method. Collecting this data is not free, so by
	  * default we don't do it.
	  * Requesting this data will automatically start the
	  * collecting-process. So, some time later, when you call this method
	  * again, there is a good chance that it will succeed.
	  * And similarly, if you stop calling this method for some time, we
	  * will automatically stop collecting this data.
	  * This method is meant to be used by the GUI. There it's acceptable
	  * that the first few frames no data is available yet.
	  */
	[[nodiscard]] std::span<const float> getLastBuffer(unsigned channel);

	/** The samples returned by 'getLastBuffer()' have this sample rate.
	  */
	[[nodiscard]] float getNativeSampleRate() const { return float(inputSampleRate); }

	/** getLastBuffer() with return buffers containing this many samples.
	  * This number depends on the native-sample-rate of the device. For all
	  * devices it will be (approximately) represent the same time duration.
	  */
	[[nodiscard]] unsigned getLastMonoBufferSize() const {
		// Tuned so that both the ImGui waveform and spectrum viewer
		// look 'nice' (and aren't too expensive to calculate).
		return inputSampleRate / 7; // ~0.14 seconds
	}
	[[nodiscard]] unsigned getLastBufferSize() const {
		return getLastMonoBufferSize() * stereo;
	}

protected:
	/** Constructor.
	  * @param mixer The Mixer object
	  * @param name Name for this device, will be made unique
	  * @param description Description for this sound device
	  * @param numChannels The number of channels for this device
	  * @param inputRate The sample rate of this sound device
	  * @param stereo Is this a stereo device
	  */
	SoundDevice(MSXMixer& mixer, std::string_view name, static_string_view description,
	            unsigned numChannels, unsigned inputRate, bool stereo);
	~SoundDevice();

	/** Get amplification/attenuation factor for this device.
	  * Normally the outputBuffer() method should scale the output to
	  * the range [-1.0..+1.0]. But sometimes it's more convenient to
	  * generate another output range. In later stages the output
	  * is anyway still multiplied by some factor. This method tells which
	  * factor should be used to scale the output to the correct range.
	  * The default implementation returns '1.0 / 32768.0'.
	  */
	[[nodiscard]] virtual float getAmplificationFactorImpl() const;

	/**
	 * Registers this sound device with the Mixer.
	 * Call this method when the sound device is ready to start receiving
	 * calls to updateBuffer, so after all initialisation is done.
	 * @param config Configuration data for this sound device.
	 */
	void registerSound(const DeviceConfig& config);

	/**
	 * Unregisters this sound device with the Mixer.
	 * Call this method before any deallocation is done.
	 */
	void unregisterSound();

	/** @see Mixer::updateStream */
	void updateStream(EmuTime time);

	void setInputRate(unsigned sampleRate) { inputSampleRate = sampleRate; }
	[[nodiscard]] unsigned getInputRate() const { return inputSampleRate; }

public: // Will be called by Mixer:
	/**
	 * When a SoundDevice registers itself with the Mixer, the Mixer sets
	 * the required sampleRate through this method. All sound devices share
	 * a common sampleRate.
	 */
	virtual void setOutputRate(unsigned hostSampleRate, double speed) = 0;

	/** Generate sample data
	  * @param length The number of required samples
	  * @param buffer This buffer should be filled
	  * @param time current time
	  * @result false iff the output is empty. IOW filling the buffer with
	  *         zeros or returning false has the same effect, but the latter
	  *         can be more efficient
	  *
	  * This method is regularly called from the Mixer, it should return a
	  * pointer to a buffer filled with the required number of samples.
	  * Samples are always floats, later they are converted to the systems
	  * native format (e.g. 16-bit signed).
	  *
	  * Note: To enable various optimizations (like SSE), this method can
	  * fill the output buffer with up to 3 extra samples. Those extra
	  * samples should be ignored, though the caller must make sure the
	  * buffer has enough space to hold them.
	  */
	[[nodiscard]] virtual bool updateBuffer(size_t length, float* buffer,
	                                        EmuTime time) = 0;

protected:
	/** Adds a number of samples that all have the same value.
	  * Can be used to synthesize segments of a square wave.
	  * @param buffer Pointer to the position in a sample buffer where the
	  *               samples should be added. This pointer is updated to
	  *               the position right after the written samples.
	  * @param value Sample value (amplitude).
	  * @param num The number of samples.
	  */
	static void addFill(float*& buffer, float value, unsigned num);

	/** Abstract method to generate the actual sound data.
	  * @param buffers An array of pointer to buffers. Each buffer must
	  *                be big enough to hold 'num' samples.
	  * @param num The number of samples.
	  *
	  * This method should fill each buffer with sound data that
	  * corresponds to one channel of the sound device. The same channel
	  * should each time be written to the same buffer (needed for record).
	  *
	  * If a certain channel is muted it is allowed to set the buffer
	  * pointer to nullptr. This has exactly the same effect as filling the
	  * buffer completely with zeros, but it can be more efficient.
	  */
	virtual void generateChannels(std::span<float*> buffers, unsigned num) = 0;

	/** Calls generateChannels() and combines the output to a single
	  * channel.
	  * @param dataOut Output buffer, must be big enough to hold
	  *                'samples' number of samples
	  * @param samples The number of samples
	  * @result true iff at least one channel was unmuted
	  *
	  * Note: To enable various optimizations (like SSE), this method can
	  * fill the output buffer with up to 3 extra samples. Those extra
	  * samples should be ignored, though the caller must make sure the
	  * buffer has enough space to hold them.
	  */
	[[nodiscard]] bool mixChannels(float* dataOut, size_t samples);

	/** See MSXMixer::getHostSampleClock(). */
	[[nodiscard]] const DynamicClock& getHostSampleClock() const;
	[[nodiscard]] double getEffectiveSpeed() const;

private:
	struct Balance { // amplitude multiplication factors
		float left, right;
	};

	MSXMixer& mixer;
	const std::string name;
	const static_string_view description;

	std::array<std::optional<Wav16Writer>, MAX_CHANNELS> writer;

	float softwareVolumeLeft = 1.0f;
	float softwareVolumeRight = 1.0f;
	unsigned inputSampleRate;
	const unsigned numChannels;
	const unsigned stereo;
	unsigned numRecordChannels = 0;
	std::array<Balance,  MAX_CHANNELS> channelBalance;
	std::array<bool, MAX_CHANNELS> channelMuted;
	bool balanceCenter = true;

	// When channel data needs to be collected separately (e.g. because
	// we're recording the channel, or because we want to present it in the
	// GUI), we store that data in this struct.
	struct PerChannelBuffer {
		std::vector<float> buffer; // dynamically grows when needed
		unsigned stopIdx = 0; // buffer only contains valid data at indices [0, stopIdx)
		unsigned requestCounter = 0; // != 0 means we're interested in collecting this data
		unsigned silent = 999999; // how many 0 samples are at the end of the buffer
	};
	std::array<PerChannelBuffer, MAX_CHANNELS> channelBuffers = {};
};

} // namespace openmsx

#endif
