#include <psemek/audio/engine.hpp>
#include <psemek/audio/constants.hpp>
#include <psemek/sdl2/init.hpp>
#include <psemek/log/log.hpp>
#include <psemek/prof/profiler.hpp>
#include <psemek/util/at_scope_exit.hpp>
#include <psemek/util/to_string.hpp>

#include <SDL2/SDL.h>

#include <vector>
#include <cmath>
#include <iomanip>

namespace psemek::sdl2
{

	namespace
	{

		struct audio_engine_impl
			: audio::engine
		{
			audio_engine_impl();
			~audio_engine_impl();

			audio::channel_ptr output() override;

		private:
			std::shared_ptr<void> sdl_init_;

			SDL_AudioDeviceID device_;

			std::vector<float> buffer_;
			bool thread_registered_ = false;

			audio::channel_ptr output_;

			template <typename SampleType>
			static void callback(void * userdata, std::uint8_t * stream, int len);
		};

		audio_engine_impl::audio_engine_impl()
			: sdl_init_(sdl2::init(SDL_INIT_AUDIO))
		{
			SDL_AudioSpec desired, obtained;
			desired.freq = audio::frequency;
			desired.channels = 2;
			desired.format = AUDIO_F32SYS;
			desired.samples = 256;
			desired.callback = &callback<float>;
			desired.userdata = this;
			if (device_ = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0); device_ == 0)
			{
				log::error() << "SDL_OpenAudioDevice failed: " << SDL_GetError();
				log::error() << "Fallback to int16 audio output";
				desired.format = AUDIO_S16SYS;
				desired.callback = &callback<std::int16_t>;
				if (device_ = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0); device_ == 0)
				{
					fail("SDL_OpenAudioDevice failed:");
				}
			}

			log::info() << "Initialized audio: " << static_cast<int>(obtained.channels) << " channels, " << obtained.freq << " Hz, " << obtained.samples << " samples " << (desired.format == AUDIO_F32SYS ? "f32" : "i16");

			buffer_.resize(obtained.samples * obtained.channels);
			output_ = std::make_shared<audio::channel>();
			SDL_PauseAudioDevice(device_, 0);
		}

		audio_engine_impl::~audio_engine_impl()
		{
			SDL_CloseAudioDevice(device_);
		}

		template <typename SampleType>
		void audio_engine_impl::callback(void * userdata, std::uint8_t * dst_u8, int len)
		{
			static std::string const profiler_str = "audio";
			prof::profiler prof(profiler_str);

			auto self = static_cast<audio_engine_impl *>(userdata);
			auto stream = self->output()->stream();
			SampleType * dst = reinterpret_cast<SampleType *>(dst_u8);

			if (!self->thread_registered_)
			{
				log::register_thread("audio");
				self->thread_registered_ = true;
			}

			std::size_t const size = len / sizeof(SampleType);

			if constexpr (std::is_same_v<SampleType, std::int16_t>)
			{
				std::size_t read = 0;
				if (stream)
					read = stream->read({self->buffer_.data(), size});
				std::fill(self->buffer_.data() + read, self->buffer_.data() + size, 0.f);

				for (auto s : self->buffer_)
					*dst++ = static_cast<std::int16_t>(std::max(std::min((65535.f * s - 1.f) / 2.f, 32767.f), -32768.f));
			}
			else
			{
				std::size_t read = 0;
				if (stream)
					read = stream->read({dst, size});
				std::fill(dst + read, dst + size, 0.f);
			}

			if (auto duration = prof.duration(); duration > (size * audio::inv_frequency / 2))
				log::warning() << "Audio can't keep up, callback took " << std::setprecision(5) << (1000.0 * duration) << " ms";
		}

		audio::channel_ptr audio_engine_impl::output()
		{
			return output_;
		}

	}

}

namespace psemek::audio
{

	std::unique_ptr<engine> make_engine()
	{
		return std::make_unique<sdl2::audio_engine_impl>();
	}

}
