/*
* SACD Decoder plugin
* Copyright (c) 2011-2023 Maxim V.Anisiutkin <maxim.anisiutkin@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "autoproxy_output.h"
#include "sacd_preferences.h"

static output_entry::ptr        g_output_entry_ptr{ nullptr };
static autoproxy_instantiate_t  g_autoproxy_instantiate{ nullptr };
static autoproxy_output_entry_t g_autoproxy_output_entry;

static bool g_dop{ false };

static const GUID g_compatibility_guids[] = {
	{ 0x5dc2447c, 0x140a, 0x498c, { 0xa2, 0xa4, 0x60, 0xef, 0xfe, 0xd2, 0x54, 0x15 } }, // foo_out_asio2
	{ 0x74b6e5fc, 0x8bfc, 0x4067, { 0x82, 0xb8, 0x5e, 0x17, 0xef, 0x07, 0xd3, 0xb3 } }, // foo_out_wasap2
};

static const bool g_compatibility_mode(output_entry::ptr output_entry_ptr) {
	auto output_entry_guid = output_entry_ptr->get_guid();
	for (auto& guid : g_compatibility_guids) {
		if (guid == output_entry_guid) {
			return true;
		}
	}
	return false;
}

static bool g_copy_vme(void** dst, void** src) {
	DWORD protect;
	if (!VirtualProtect(dst, sizeof(*dst), PAGE_READWRITE, &protect)) {
		return false;
	}
	*dst = *src;
	VirtualProtect(dst, sizeof(*dst), protect, &protect);
	return true;
}

static bool g_hook(bool p_state) {
	if (g_output_entry_ptr.is_valid()) {
		if (g_autoproxy_instantiate) {
			auto vmt_ptr = *reinterpret_cast<void***>(g_output_entry_ptr.get_ptr());
			g_copy_vme(&vmt_ptr[VME_instantiate], &(void*&)g_autoproxy_instantiate);
			g_autoproxy_instantiate = nullptr;
		}
		g_output_entry_ptr = nullptr;
	}
	if (p_state && g_output_entry_ptr.is_empty()) {
		outputCoreConfig_t output_config;
		output_manager_v2::get()->getCoreConfig(output_config);
		if (!output_entry::g_find(output_config.m_output, g_output_entry_ptr)) {
			return false;
		}
		auto vmt_ptr = *reinterpret_cast<void***>(g_output_entry_ptr.get_ptr());
		g_copy_vme(&(void*&)g_autoproxy_instantiate, &vmt_ptr[VME_instantiate]);
		autoproxy_instantiate_t instantiate_ptr = &autoproxy_output_entry_t::instantiate;
		g_copy_vme(&vmt_ptr[VME_instantiate], &(void*&)instantiate_ptr);
	}
	return true;
}

void autoproxy_output_entry_t::instantiate(service_ptr_t<output>& p_out, const GUID& p_device, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth) {
	if (g_compatibility_mode(g_output_entry_ptr)) {
		auto vmt_ptr = *reinterpret_cast<void***>(g_output_entry_ptr.get_ptr());
		g_copy_vme(&vmt_ptr[VME_instantiate], &(void*&)g_autoproxy_instantiate);
		g_output_entry_ptr->instantiate(p_out, p_device, p_buffer_length, p_dither, p_bitdepth);
		autoproxy_instantiate_t instantiate_ptr = &autoproxy_output_entry_t::instantiate;
		g_copy_vme(&vmt_ptr[VME_instantiate], &(void*&)instantiate_ptr);
	}
	else {
 		(g_autoproxy_output_entry.*g_autoproxy_instantiate)(p_out, p_device, p_buffer_length, p_dither, p_bitdepth);
	}
	if (p_out.is_empty()) {
		return;
	}
	auto version{ 0 };
	service_ptr_t<output>    service_output{ p_out };
	service_ptr_t<output_v2> service_output_v2;
	service_ptr_t<output_v3> service_output_v3;
	service_ptr_t<output_v4> service_output_v4;
	service_ptr_t<output_v5> service_output_v5;
	p_out.release();
	if (service_output->cast(service_output_v5)) {
		if (fb2k::service_new<autoproxy_service_t<autoproxy_output_v5>>(service_output, p_buffer_length, p_dither, p_bitdepth)->cast(p_out)) {
			version = 5;
		}
	}
	else if (service_output->cast(service_output_v4)) {
		if (fb2k::service_new<autoproxy_service_t<autoproxy_output_v4>>(service_output, p_buffer_length, p_dither, p_bitdepth)->cast(p_out)) {
			version = 4;
		}
	}
	else if (service_output->cast(service_output_v3)) {
		if (fb2k::service_new<autoproxy_service_t<autoproxy_output_v3>>(service_output, p_buffer_length, p_dither, p_bitdepth)->cast(p_out)) {
			version = 3;
		}
	}
	else if (service_output->cast(service_output_v2)) {
		if (fb2k::service_new<autoproxy_service_t<autoproxy_output_v2>>(service_output, p_buffer_length, p_dither, p_bitdepth)->cast(p_out)) {
			version = 2;
		}
	}
	else {
		if (fb2k::service_new<autoproxy_service_t<autoproxy_output>>(service_output, p_buffer_length, p_dither, p_bitdepth)->cast(p_out)) {
			version = 1;
		}
	}
	if (CSACDPreferences::get_trace()) {
		console::printf(
			"new service_impl_t<autoproxy_output%s>(device = \"%s : %s\", buffer_length = %ss, dither = %s, bitdepth = %d)",
			(version == 5) ? "_v5" : (version == 4) ? "_v4" : (version == 3) ? "_v3" : (version == 2) ? "_v2" : (version == 1) ? "" : "_v?",
			g_output_entry_ptr->get_name(),
			g_output_entry_ptr->get_device_name(p_device).c_str(),
			format_float(p_buffer_length, 0, 3).toString(),
			p_dither ? "true" : "false",
			p_bitdepth
		);
	}
}

class autoproxy_change_callback_t : public output_config_change_callback {
public:
	virtual void outputConfigChanged() {
		g_hook(true);
		CSACDPreferences::update();
	}
};

static autoproxy_change_callback_t g_autoproxy_change_callback;

class autoproxy_initquit_t : public initquit {
public:
	virtual void on_init() {
		g_hook(true);
		output_manager_v2::get()->addCallback(&g_autoproxy_change_callback);
	}
	virtual void on_quit() {
		output_manager_v2::get()->removeCallback(&g_autoproxy_change_callback);
		g_hook(false);
	}
};

static initquit_factory_t<autoproxy_initquit_t> g_initquit_sacd_factory;

FOOGUIDDECL const GUID autoproxy_output::class_guid = output::class_guid;

autoproxy_output::autoproxy_output(service_ptr_t<output>& p_output, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth) {
	m_output = p_output;
	m_buffer_length = p_buffer_length;
	m_dsd_playback = false;
	m_latency = 0;
	m_volume_dB = 0;
	m_flushed = false;
	m_paused = false;
	if (m_output.is_empty()) {
		throw_exception_with_message<exception_io>("autoproxy_output::autoproxy_output() => Output driver is not instantiated");
	}
	CSACDPreferences::load();
	m_transition = CSACDPreferences::get_transition();
	m_trace = CSACDPreferences::get_trace();
	service_enum_t<dsd_processor_service> dsddsp_enum;
	service_ptr_t<dsd_processor_service> dsddsp_temp;
	while (dsddsp_enum.next(dsddsp_temp)) {
		if (dsddsp_temp->get_guid() == CSACDPreferences::get_dsd_processor()) {
			m_dsddsp = dsddsp_temp;
			if (m_trace) {
				console::printf("autoproxy_output::autoproxy_output() => Use DSD Processor [name = \"%s\"]", m_dsddsp->get_name());
			}
			break;
		}
	}
	m_dsd_stream->set_accept_data(true);
}

double autoproxy_output::get_latency() {
	m_latency = m_output->get_latency();
	/*
	if (m_trace) {
		static int show_idx{ 0 };
		if (show_idx++ % 256 == 0)
			console::printf("autoproxy_output::get_latency() => %s", format_float(m_latency, 0, 3).toString());
	}
	*/
	return m_latency;
}

void autoproxy_output::process_samples(const audio_chunk& p_chunk) {
	audio_chunk_impl dop_chunk;
	auto pcm_spec = p_chunk.get_spec();
	if (!pcm_spec.is_valid()) {
		throw_exception_with_message<exception_io_data>("autoproxy_output::process_samples() => Invalid audio stream specifications");
	}
	if (m_dsd_stream->is_streaming() != m_dsd_playback) {
		m_dsd_playback = m_dsd_stream->is_streaming();
		if (m_trace) {
			check_dsd_stream(true);
			console::printf("autoproxy_output::process_samples() => Switch to %s playback", m_dsd_playback ? "DSD" : "PCM");
		}
	}
	if (m_trace) {
		check_dsd_stream(false);
	}
	if (m_dsd_playback) {
		// DSD playback
		while (m_dsd_stream->get_chunk_count() > 0) {
			auto dsd_chunk = m_dsd_stream->get_first_chunk();
			audio_chunk_impl out_chunk;
			if (pick_dsd_processor(dsd_chunk.get_spec(), pcm_spec)) {
				t_size inp_samples = dsd_chunk.get_sample_count();
				t_size out_samples;
				auto out_data = m_dsddsp->run(dsd_chunk.get_data(), inp_samples, &out_samples);
				m_dop_converter.set_inp_spec(m_dsddsp_out_spec);
				m_dop_converter.dsd_to_dop(out_data, out_samples, out_chunk);
			}
			else {
				m_dop_converter.set_inp_spec(dsd_chunk.get_spec());
				m_dop_converter.dsd_to_dop(dsd_chunk.get_data(), dsd_chunk.get_sample_count(), out_chunk);
			}
			m_dsd_stream->remove_first_chunk();
			if (dop_chunk.is_empty()) {
				dop_chunk = out_chunk;
				if (m_dsd_stream->get_chunk_count() <= DSD_CHUNKS_TRESHOLD) {
					break;
				}
			}
			else {
				if (dop_chunk.get_spec() != out_chunk.get_spec()) {
					break;
				}
				dop_chunk.set_data_size(dop_chunk.get_data_size() + out_chunk.get_data_size());
				memcpy(dop_chunk.get_data() + dop_chunk.get_channel_count() * dop_chunk.get_sample_count(), out_chunk.get_data(), out_chunk.get_channel_count() * out_chunk.get_sample_count() * sizeof(audio_sample));
				dop_chunk.set_sample_count(dop_chunk.get_sample_count() + out_chunk.get_sample_count());
			}
		}
		if (dop_chunk.get_sample_count() > 0) {
			process_output(true, dop_chunk);
		}
	}
	else {
		if (pcm_spec.sampleRate >= 176400 && m_dop_converter.is_dop(p_chunk)) {
			// DoP playback
			auto dsd_spec{ pcm_spec };
			dsd_spec.sampleRate *= 16;
			array_t<t_uint8> inp_data;
			m_dop_converter.dop_to_dsd(p_chunk, inp_data);
			if (pick_dsd_processor(dsd_spec, pcm_spec)) {
				t_size inp_samples = inp_data.get_size() / dsd_spec.chanCount;
				t_size out_samples;
				auto out_data = m_dsddsp->run(inp_data.get_ptr(), inp_samples, &out_samples);
				m_dop_converter.set_inp_spec(m_dsddsp_out_spec);
				m_dop_converter.dsd_to_dop(out_data, out_samples, dop_chunk);
				process_output(true, dop_chunk);
			}
			else {
				m_out_spec = dsd_spec;
				process_output(true, p_chunk);
			}
		}
		else {
			// PCM playback
			static audio_chunk::spec_t null_spec;
			if (pick_dsd_processor(null_spec, pcm_spec)) {
				t_size inp_samples = p_chunk.get_sample_count();
				t_size out_samples;
				auto out_data = m_dsddsp->run(p_chunk.get_data(), inp_samples, &out_samples);
				m_dop_converter.set_inp_spec(m_dsddsp_out_spec);
				m_dop_converter.dsd_to_dop(out_data, out_samples, dop_chunk);
				process_output(true, dop_chunk);
			}
			else {
				m_out_spec = pcm_spec;
				process_output(false, p_chunk);
			}
		}
	}															  
	if (m_backtrace.add_chunk(m_out_spec, p_chunk.get_duration())) {
		if (m_trace) {
			console::printf("autoproxy_output::process_samples(channels = %d, sample_rate = %d, channel_config = 0x%08x)",
				m_out_spec.chanCount, m_out_spec.sampleRate, m_out_spec.chanMask
			);
		}
	}
}

void autoproxy_output::update(bool& p_ready) {
	if (m_backtrace.get_spec(m_now_playing_spec, m_latency)) {
		volume_adjust();
	}
	m_output->update(p_ready);
	/*
	if (m_trace) {
		console::printf("autoproxy_output::update(%s)", p_ready ? "true" : "false");
	}
	*/
}

void autoproxy_output::pause(bool p_state) {
	m_output->pause(p_state);
	m_paused = p_state;
	if (m_trace) {
		check_dsd_stream(true);
		console::printf("autoproxy_output::pause(%s)", p_state ? "true" : "false");
	}
}

void autoproxy_output::flush() {
	m_backtrace.flush();
	m_dsd_stream->flush();
	m_output->flush();
	m_flushed = true;
	if (m_trace) {
		check_dsd_stream(true);
		console::printf("autoproxy_output::flush()");
	}
}

void autoproxy_output::force_play() {
	m_output->force_play();
	if (m_trace) {
		check_dsd_stream(true);
		console::printf("autoproxy_output::force_play()");
	}
}

void autoproxy_output::volume_set(double p_val_dB) {
	if (m_trace) {
		console::printf("autoproxy_output::volume_set(%s)", format_float(p_val_dB, 0, 3).toString());
	}
	m_volume_dB = p_val_dB;
	volume_adjust();
}

void autoproxy_output::process_output(bool p_dop, const audio_chunk& p_chunk) {
	if (m_flushed) {
		m_flushed = false;
		g_dop = p_dop;
	}
	auto add_silence{ false };
	if (g_dop != p_dop) {
		g_dop = p_dop;
		add_silence = true;
	}
	if (add_silence) {
		add_silence = false;
		audio_chunk_impl first_chunk;
		first_chunk.set_spec(p_chunk.get_spec());
		first_chunk.set_silence_seconds(m_transition);
		if (p_dop) {
			m_dop_converter.set_silence(first_chunk);
		}
		first_chunk.append(p_chunk);
		m_output->process_samples(first_chunk);
		if (m_trace) {
			console::printf("autoproxy_output::process_output(transition = %s)", format_float(m_transition, 0, 3).toString());
		}
	}
	else {
		m_output->process_samples(p_chunk);
	}
}

bool autoproxy_output::pick_dsd_processor(const audio_chunk::spec_t& p_dsd_spec, const audio_chunk::spec_t& p_pcm_spec) {
	auto inp_spec = p_dsd_spec;
	auto out_spec = p_pcm_spec;
	if (inp_spec.sampleRate && inp_spec.chanCount) {
		out_spec.sampleRate = p_dsd_spec.sampleRate;
	}
	else {
		inp_spec = p_pcm_spec;
	}
	if (m_dsddsp.is_valid()) {
		if (m_dsddsp->is_active()) {
			out_spec.sampleRate = m_dsddsp_out_spec.sampleRate;
		}
		if (m_dsddsp->is_changed() || inp_spec != m_inp_spec || out_spec != m_out_spec) {
			m_dsddsp_out_spec = out_spec;
			auto ok = m_dsddsp->start(inp_spec.chanCount, inp_spec.sampleRate, inp_spec.chanMask, m_dsddsp_out_spec.chanCount, m_dsddsp_out_spec.sampleRate, m_dsddsp_out_spec.chanMask);
			if (m_trace) {
				console::printf("autoproxy_output::process_samples() => Start DSD Processor [channels: %d -> %d, samplerate: %d -> %d, channel_config: 0x%08u -> 0x%08u] %s",
					inp_spec.chanCount, m_dsddsp_out_spec.chanCount,
					inp_spec.sampleRate, m_dsddsp_out_spec.sampleRate,
					inp_spec.chanMask, m_dsddsp_out_spec.chanMask,
					ok ? "running" : "not running"
				);
			}
			m_dsddsp->set_volume(m_volume_dB);
			if (m_dsddsp->is_active()) {
				out_spec.sampleRate = m_dsddsp_out_spec.sampleRate;
			}
		}
	}
	if (inp_spec != m_inp_spec) {
		m_inp_spec = inp_spec;
		if (m_trace) {
			console::printf("autoproxy_output::process_samples() => Input stream [channels: %d, samplerate: %d, channel_config: 0x%08u]",
				inp_spec.chanCount, inp_spec.sampleRate, inp_spec.chanMask
			);
		}
	}
	if (out_spec != m_out_spec) {
		m_out_spec = out_spec;
		m_dop_converter.set_out_spec(m_out_spec);
		if (m_trace) {
			console::printf("autoproxy_output::process_samples() => Output stream [channels: %d, samplerate: %d, channel_config: 0x%08u]",
				out_spec.chanCount, out_spec.sampleRate, out_spec.chanMask
			);
		}
	}
	return m_dsddsp.is_valid() && m_dsddsp->is_active();
}

void autoproxy_output::volume_adjust() {
	auto vol_dB = (m_now_playing_spec.sampleRate < 2822400) ? m_volume_dB : 0.0;
	m_output->volume_set(vol_dB);
	if (m_dsddsp.is_valid()) {
		m_dsddsp->set_volume(m_volume_dB);
	}
	if (m_trace) {
		console::printf("autoproxy_output::volume_adjust(samplerate = %d, latency = %s, volume = %s)",
			m_now_playing_spec.sampleRate, format_float(m_latency, 0, 3).toString(), format_float(vol_dB, 0, 3).toString()
		);
	}
}

void autoproxy_output::check_dsd_stream(bool p_update) {
	static size_t max_chunks = 0;
	auto chunks = m_dsd_stream->get_chunk_count();
	if (p_update || (chunks > max_chunks)) {
		max_chunks = chunks;
		console::printf("autoproxy_output::check_dsd_stream(%s) => DSD stream contains %d chunks and %d samples", p_update ? "true" : "false", chunks, m_dsd_stream->get_sample_count());
	}
}

void autoproxy_output::shutdown() {
	m_dsd_stream->set_accept_data(false);
	m_dsd_stream->flush();
	if (m_dsddsp.is_valid()) {
		m_dsddsp->stop();
		if (m_trace) {
			console::printf("autoproxy_output::~autoproxy_output() => Stop DSD Processor");
		}
	}
	if (m_trace) {
		console::printf("autoproxy_output::~autoproxy_output()");
	}
}

FOOGUIDDECL const GUID autoproxy_output_v2::class_guid = output_v2::class_guid;

autoproxy_output_v2::autoproxy_output_v2(service_ptr_t<output>& p_output, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth) : autoproxy_output(p_output, p_buffer_length, p_dither, p_bitdepth) {
	p_output->cast(m_output_v2);
	m_track_marks = false;
}

bool autoproxy_output_v2::want_track_marks() {
	if (m_output_v2.is_valid()) {
		m_track_marks = m_output_v2->want_track_marks();
	}
	else {
		m_track_marks = false;
	}
	if (m_trace) {
		console::printf("autoproxy_output_v2::want_track_marks() => %s", m_track_marks ? "true" : "false");
	}
	return m_track_marks;
}

void autoproxy_output_v2::on_track_mark() {
	if (m_output_v2.is_valid()) {
		m_output_v2->on_track_mark();
	}
	if (m_trace) {
		check_dsd_stream(true);
		console::printf("autoproxy_output_v2::on_track_mark()");
	}
}

void autoproxy_output_v2::enable_fading(bool p_state) {
	if (m_output_v2.is_valid()) {
		m_output_v2->enable_fading(p_state);
	}
	if (m_trace) {
		console::printf("autoproxy_output_v2::enable_fading(%s)", p_state ? "true" : "false");
	}
}

void autoproxy_output_v2::flush_changing_track() {
	m_backtrace.flush();
	m_dsd_stream->flush();
	if (m_output_v2.is_valid()) {
		m_output_v2->flush_changing_track();
	}
	else {
		m_output->flush();
	}
	if (m_trace) {
		check_dsd_stream(true);
		console::printf("autoproxy_output_v2::flush_changing_track()");
	}
}

FOOGUIDDECL const GUID autoproxy_output_v3::class_guid = output_v3::class_guid;

autoproxy_output_v3::autoproxy_output_v3(service_ptr_t<output>& p_output, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth) : autoproxy_output_v2(p_output, p_buffer_length, p_dither, p_bitdepth) {
	p_output->cast(m_output_v3);
}

unsigned autoproxy_output_v3::get_forced_sample_rate() {
	unsigned forced_sample_rate{ 0 };
	if (m_output_v3.is_valid()) {
		forced_sample_rate = m_output_v3->get_forced_sample_rate();
	}
	if (m_trace) {
		console::printf("autoproxy_output_v3::get_forced_sample_rate() => %d", forced_sample_rate);
	}
	return forced_sample_rate;
}

void autoproxy_output_v3::get_injected_dsps(dsp_chain_config& p_dsps) {
	if (m_output_v3.is_valid()) {
		m_output_v3->get_injected_dsps(p_dsps);
	}
	if (m_trace) {
		console::printf("autoproxy_output_v3::get_injected_dsps() => %d", p_dsps.get_count());
	}
}

FOOGUIDDECL const GUID autoproxy_output_v4::class_guid = output_v4::class_guid;

autoproxy_output_v4::autoproxy_output_v4(service_ptr_t<output>& p_output, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth) : autoproxy_output_v3(p_output, p_buffer_length, p_dither, p_bitdepth) {
	p_output->cast(m_output_v4);
}

eventHandle_t autoproxy_output_v4::get_trigger_event() {
	eventHandle_t evt{ pfc::eventInvalid };
	if (m_output_v4.is_valid()) {
		evt = m_output_v4->get_trigger_event();
	}
	/*
	if (m_trace) {
		console::printf("autoproxy_output_v4::get_trigger_event() => %d", evt);
	}
	*/
	return evt;
}

bool autoproxy_output_v4::is_progressing() {
	bool progressing{ true };
	if (m_output_v4.is_valid()) {
		progressing = m_output_v4->is_progressing();
	}
	/*
	if (m_trace) {
		console::printf("autoproxy_output_v4::is_progressing() => %s", progressing ? "true" : "false");
	}
	*/
	return progressing;
}

t_size autoproxy_output_v4::update_v2() {
	size_t samples{ 0 };
	if (m_output_v4.is_valid()) {
		samples = m_output_v4->update_v2();
	}
	else {
		bool ok{ false };
		update(ok);
		samples = ok ? SIZE_MAX : 0;
	}
	/*
	if (m_trace) {
		console::printf("autoproxy_output_v4::update_v2() => %d", samples);
	}
	*/
	return samples;
}

FOOGUIDDECL const GUID autoproxy_output_v5::class_guid = output_v5::class_guid;

autoproxy_output_v5::autoproxy_output_v5(service_ptr_t<output>& p_output, double p_buffer_length, bool p_dither, t_uint32 p_bitdepth) : autoproxy_output_v4(p_output, p_buffer_length, p_dither, p_bitdepth) {
	p_output->cast(m_output_v5);
}

unsigned autoproxy_output_v5::get_forced_channel_mask() {
	unsigned mask{ 0 };
	if (m_output_v5.is_valid()) {
		mask = m_output_v5->get_forced_channel_mask();
	}
	if (m_trace) {
		console::printf("autoproxy_output_v5::get_forced_channel_mask() => 0x%08x", mask);
	}
	return mask;
}
