/*
 * Copyright (C) 2022 Robin Gareus <robin@gareus.org>
 *
 * 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.
 */
#include <cassert>

#include "pbd/types_convert.h"
#include "pbd/unwind.h"
#include "pbd/xml++.h"

#include "temporal/tempo.h"

#include "ardour/audioengine.h"
#include "ardour/audio_buffer.h"
#include "ardour/audio_port.h"
#include "ardour/event_type_map.h"
#include "ardour/graph.h"
#include "ardour/io.h"
#include "ardour/io_plug.h"
#include "ardour/lv2_plugin.h"
#include "ardour/readonly_control.h"
#include "ardour/session.h"
#include "ardour/utils.h"

#include "pbd/i18n.h"

using namespace ARDOUR;
using namespace PBD;
using namespace std;

IOPlug::IOPlug (Session& s, std::shared_ptr<Plugin> p, bool pre)
	: SessionObject (s, "")
	, GraphNode (s._process_graph)
	, _plugin (p)
	, _pre (pre)
	, _plugin_signal_latency (0)
	, _configuring_io (false)
	, _window_proxy (0)
{
	_stat_reset.store (0);
	_reset_meters.store (0);

	if (_plugin) {
		setup ();
		set_name (p->get_info()->name);
		_plugin->activate ();
	}
	_input.reset (new IO (_session, io_name (), IO::Input));
	_output.reset (new IO (_session, io_name (), IO::Output));

	/* do not allow to add/remove ports (for now).
	 * when adding ports _buf will needs to be resized.
	 */
	_input->PortCountChanging.connect_same_thread (*this, [this](ChanCount) { return _configuring_io ? 0 : 1; });
	_output->PortCountChanging.connect_same_thread (*this, [this](ChanCount) { return _configuring_io ? 0 : 1; });
}

IOPlug::~IOPlug ()
{
	for (CtrlOutMap::const_iterator i = _control_outputs.begin(); i != _control_outputs.end(); ++i) {
		std::dynamic_pointer_cast<ReadOnlyControl>(i->second)->drop_references ();
	}

	PBD::Mutex::Lock lm (_control_lock);
	for (Controls::const_iterator li = _controls.begin(); li != _controls.end(); ++li) {
		std::dynamic_pointer_cast<AutomationControl>(li->second)->drop_references ();
	}
	_controls.clear ();
}

std::string
IOPlug::io_name (std::string const& n) const
{
	return (string_compose ("%1/%2/%3", _("IO"), _pre ? S_("IO|Pre"): S_("IO|Post"), n.empty () ? name () : n));
}

std::string
IOPlug::ensure_io_name (std::string newname) const
{
	while (!_session.io_name_is_legal (io_name (newname))) {
		newname = bump_name_once (newname, ' ');
		if (newname == name()) {
			break;
		}
	}
	return newname;
}

XMLNode&
IOPlug::get_state() const
{
	XMLNode* node = new XMLNode (/*state_node_name*/ "IOPlug");

	Latent::add_state (node);

	node->set_property("type", _plugin->state_node_name ());
	node->set_property("unique-id", _plugin->unique_id ());

	node->set_property("id", id());
	node->set_property("name", name());
	node->set_property("pre", _pre);

	_plugin->set_insert_id(this->id());
	node->add_child_nocopy (_plugin->get_state());

	for (auto const& c : controls()) {
		std::shared_ptr<AutomationControl> ac = std::dynamic_pointer_cast<AutomationControl> (c.second);
		if (ac) {
			node->add_child_nocopy (ac->get_state());
		}
	}

	if (_input) {
		XMLNode& i (_input->get_state ());
		node->add_child_nocopy (i);
	}
	if (_output) {
		XMLNode& o (_output->get_state ());
		node->add_child_nocopy (o);
	}
	return *node;
}

int
IOPlug::set_state (const XMLNode& node, int version)
{
	set_id (node);
	assert (!regenerate_xml_or_string_ids ());

	ARDOUR::PluginType type;
	std::string unique_id;
	if (! parse_plugin_type (node, type, unique_id)) {
		return -1;
	}

	bool any_vst = false;
	_plugin = find_and_load_plugin (_session, node, type, unique_id, any_vst);

	if (!_plugin) {
		return -1;
	}

	node.get_property ("pre", _pre);

	string name;
	if (node.get_property ("name", name)) {
		set_name (name);
	} else {
		set_name (_plugin->get_info()->name);
	}

	setup ();
	set_control_ids (node, version);

	_plugin->set_insert_id (this->id());

	XMLNodeList nlist = node.children();
	XMLNodeIterator niter;

	for (niter = nlist.begin(); niter != nlist.end(); ++niter) {
		if ((*niter)->name() == _plugin->state_node_name ()
		   || (any_vst && ((*niter)->name() == "lxvst" || (*niter)->name() == "windows-vst" || (*niter)->name() == "mac-vst"))
			 ) {
			_plugin->set_state (**niter, version);
			break;
		}
	}

	if (_input) {
		std::string str;
		const string instr = enum_2_string (IO::Input);
		for (niter = nlist.begin(); niter != nlist.end(); ++niter) {
			if ((*niter)->get_property ("direction", str) && str == instr) {
				_input->set_state(**niter, version);
				break;
			}
		}
	}
	if (_output) {
		std::string str;
		const string outstr = enum_2_string (IO::Output);
		for (niter = nlist.begin(); niter != nlist.end(); ++niter) {
			if ((*niter)->get_property ("direction", str) && str == outstr) {
				_output->set_state(**niter, version);
			}
		}
	}

	Latent::set_state (node, version);

	_plugin->activate ();

	return 0;
}

bool
IOPlug::set_name (std::string const& str)
{
	bool ret = true;

	if (name () == str) {
		return ret;
	}

	std::string new_name = ensure_io_name (str);

	if (ret && _input) {
		ret = _input->set_name (io_name (new_name));
	}

	if (ret && _output) {
		ret = _output->set_name (io_name (new_name));
	}

	if (ret) {
		ret = SessionObject::set_name (new_name); /* never fails */
		assert (ret);
	}

	return ret;
}

void
IOPlug::setup ()
{
	 create_parameters ();

	 PluginInfoPtr pip = _plugin->get_info ();
	 ChanCount aux_in;
	 if (pip->reconfigurable_io()) {
		 _n_in  = _plugin->input_streams ();
		 _n_out = _plugin->output_streams ();
		 if (_n_in.n_total () == 0 && _n_out.n_total () == 0) {
			 if (pip->is_instrument ()) {
				 _n_in.set_midi (1);
			 } else {
				 _n_in.set_audio (2);
			 }
			 _n_out.set_audio (2);
		 }
		 _plugin->match_variable_io (_n_in, aux_in, _n_out);
	 } else {
		 _n_in  = pip->n_inputs;
		 _n_out = pip->n_outputs;
	 }

	 _plugin->reconfigure_io (_n_in, aux_in, _n_out);
	 _plugin->ParameterChangedExternally.connect_same_thread (*this, std::bind (&IOPlug::parameter_changed_externally, this, _1, _2));
	 _plugin->PropertyChanged.connect_same_thread (*this, std::bind (&IOPlug::property_changed_externally, this, _1, _2));
	 _plugin->ProcessorChange.connect_same_thread (*this, std::bind (&IOPlug::processor_change, this, _1));
	 _plugin->set_insert (this, 0);
}

samplecnt_t
IOPlug::signal_latency () const
{
	return _plugin->signal_latency ();
}

void
IOPlug::set_public_latency (bool playback)
{
	/* Step1: set private port latency
	 * compare to Route::set_private_port_latencies, Route::update_port_latencies
	 */
	std::shared_ptr<PortSet> from = playback ? _output->ports () : _input->ports ();
	std::shared_ptr<PortSet> to   = playback ? _input->ports () : _output->ports ();

	LatencyRange all_connections;
	all_connections.min = ~((pframes_t) 0);
	all_connections.max = 0;

	for (auto const& p : *from) {
		if (!p->connected ()) {
			continue;
		}

		LatencyRange range;
		p->get_connected_latency_range (range, playback);

		all_connections.min = min (all_connections.min, range.min);
		all_connections.max = max (all_connections.max, range.max);
	}
	if (all_connections.min == ~((pframes_t) 0)) {
		all_connections.min = 0;
	}

	/* set the "from" port latencies to the max/min range of all their connections */
	for (auto const& p : *from) {
		p->set_private_latency_range (all_connections, playback);
	}

	all_connections.min += _plugin_signal_latency;
	all_connections.max += _plugin_signal_latency;

	for (auto const& p : *to) {
		p->set_private_latency_range (all_connections, playback);
	}

	/* Step2: publish private latencies.
	 * compare to Route::set_public_port_latencies
	 */

	if (playback) {
		_output->set_public_port_latency_from_connections ();
		_input->set_public_port_latencies (all_connections.max, true);
	} else {
		_input->set_public_port_latency_from_connections ();
		_output->set_public_port_latencies (all_connections.max, false);
	}
}

void
IOPlug::create_parameters ()
{
	assert (_plugin);

	for (uint32_t i = 0; i < _plugin->parameter_count(); ++i) {
		if (!_plugin->parameter_is_control (i)) {
			continue;
		}

		ParameterDescriptor desc;
		_plugin->get_parameter_descriptor (i, desc);

		if (!_plugin->parameter_is_input (i)) {
			_control_outputs[i] = std::shared_ptr<ReadOnlyControl> (new ReadOnlyControl (_plugin, desc, i));
			continue;
		}

		Evoral::Parameter param (PluginAutomation, 0, i);

		std::shared_ptr<AutomationControl> c (new PluginControl (_session, this, param, desc));
		c->set_flag (Controllable::NotAutomatable);
		add_control (c);

		_plugin->set_automation_control (i, c);
	}

	Plugin::PropertyDescriptors const& pdl (_plugin->get_supported_properties ());

	for (Plugin::PropertyDescriptors::const_iterator p = pdl.begin(); p != pdl.end(); ++p) {
		Evoral::Parameter param (PluginPropertyAutomation, 0, p->first);
		ParameterDescriptor const& desc = _plugin->get_property_descriptor (param.id());
		if (desc.datatype == Variant::NOTHING) {
			continue;
		}
		std::shared_ptr<AutomationControl> c (new PluginPropertyControl (_session, this, param, desc));
		c->set_flag (Controllable::NotAutomatable);
		add_control (c);
	}

	_plugin->PresetPortSetValue.connect_same_thread (*this, std::bind (&IOPlug::preset_load_set_value, this, _1, _2));
}

void
IOPlug::parameter_changed_externally (uint32_t which, float val)
{
	std::shared_ptr<Evoral::Control> c = control (Evoral::Parameter (PluginAutomation, 0, which));
	std::shared_ptr<PluginControl> pc = std::dynamic_pointer_cast<PluginControl> (c);
	if (pc) {
		pc->catch_up_with_external_value (val);
	}
}

void
IOPlug::property_changed_externally (uint32_t which, Variant val)
{
	std::shared_ptr<Evoral::Control>        c  = control (Evoral::Parameter (PluginPropertyAutomation, 0, which));
	std::shared_ptr<PluginPropertyControl>  pc = std::dynamic_pointer_cast<PluginPropertyControl> (c);

	if (pc) {
		pc->catch_up_with_external_value (val.to_double ());
	}
}

void
IOPlug::processor_change (RouteProcessorChange const& rpc)
{
	if (rpc.type & RouteProcessorChange::PortNameChange) {
		set_pretty_port_names ();
	}
}

int
IOPlug::set_block_size (pframes_t n_samples)
{
	return _plugin->set_block_size (n_samples);
}

PlugInsertBase::UIElements
IOPlug::ui_elements () const
{
	UIElements rv = PluginPreset;
	if (_plugin->get_info ()->is_instrument ()) {
		rv = static_cast<PlugInsertBase::UIElements> (static_cast <std::uint8_t>(rv) | static_cast<std::uint8_t> (PlugInsertBase::MIDIKeyboard));
	}
	return rv;
}

void
IOPlug::set_pretty_port_names ()
{
	for (uint32_t i = 0; i < _n_in.n_audio (); ++i) {
		const auto& pd (_plugin->describe_io_port (DataType::AUDIO, true, i));
		std::string const pn = string_compose ("%1 %2 - %3", _("IO"), name (), pd.name);
		_input->audio (i)->set_pretty_name (pn);
	}
	for (uint32_t i = 0; i < _n_in.n_midi (); ++i) {
		const auto& pd (_plugin->describe_io_port (DataType::MIDI, true, i));
		std::string const pn = string_compose ("%1 %2 - %3", _("IO"), name (), pd.name);
		_input->midi (i)->set_pretty_name (pn);
	}
	for (uint32_t i = 0; i < _n_out.n_audio (); ++i) {
		const auto& pd (_plugin->describe_io_port (DataType::AUDIO, false, i));
		std::string const pn = string_compose ("%1 %2 - %3", _("IO"), name (), pd.name);
		_output->audio (i)->set_pretty_name (pn);
	}
	for (uint32_t i = 0; i < _n_out.n_midi (); ++i) {
		const auto& pd (_plugin->describe_io_port (DataType::MIDI, false, i));
		std::string const pn = string_compose ("%1 %2 - %3", _("IO"), name (), pd.name);
		_output->midi (i)->set_pretty_name (pn);
	}
}

bool
IOPlug::ensure_io ()
{
	PBD::Unwinder<bool> uw (_configuring_io, true);

	/* must be called with process-lock held */
	if (_input->ensure_io (_n_in, false) != 0) {
		return false;
	}
	if (_output->ensure_io (_n_out, false) != 0) {
		return false;
	}

	_bufs.ensure_buffers (ChanCount::max (_n_in, _n_out), _session.get_block_size ());

	set_pretty_port_names ();

	if (_pre) {
		for (uint32_t i = 0; i < _n_out.n_audio (); ++i) {
			std::string const& n = AudioEngine::instance ()->make_port_name_non_relative (_output->audio (i)->name ());
			_audio_input_ports.insert (make_pair (n, PortManager::AudioInputPort (24288))); // 2^19 ~ 1MB / port
		}
		for (uint32_t i = 0; i < _n_out.n_midi (); ++i) {
			std::string const& n = AudioEngine::instance ()->make_port_name_non_relative (_output->midi (i)->name ());
			_midi_input_ports.insert (make_pair (n, PortManager::MIDIInputPort (32)));
		}
	}
	return true;
}

ChanMapping
IOPlug::input_map (uint32_t num) const
{
	if (num == 1) {
		return ChanMapping (_n_in);
	} else {
		return ChanMapping ();
	}
}

ChanMapping
IOPlug::output_map (uint32_t num) const
{
	if (num == 1) {
		return ChanMapping (_n_out);
	} else {
		return ChanMapping ();
	}
}

void
IOPlug::process ()
{
	_graph->process_one_ioplug (this);
}

void
IOPlug::connect_and_run (samplepos_t start, pframes_t n_samples)
{
	Temporal::TempoMap::update_thread_tempo_map ();
	assert (n_samples > 0);

	int canderef (1);
	if (_stat_reset.compare_exchange_strong (canderef, 0)) {
		_timing_stats.reset ();
	}

	if (!_plugin) {
		_output->silence (n_samples);
		return;
	}

	_timing_stats.start ();

	ARDOUR::ChanMapping in_map (_n_in);
	ARDOUR::ChanMapping out_map (_n_out);

	double speed = 0;
	samplepos_t end = start + n_samples * speed;

	_input->collect_input (_bufs, n_samples, ChanCount::ZERO); /* calls _bufs.set_count() */

	if (_plugin->connect_and_run (_bufs, start, end, speed, in_map, out_map, n_samples, 0)) {
		// TODO report error, deactivate plugin
		_output->silence (n_samples);
		_timing_stats.update ();
		return;
	}

	_bufs.set_count (_n_out);

	for (DataType::iterator t = DataType::begin(); t != DataType::end(); ++t) {
		if (_bufs.count().get(*t) > 0) {
			_output->copy_to_outputs (_bufs, *t, n_samples, 0);
		}
	}

	if (_pre) {
		canderef = 1;
		const bool reset       = _reset_meters.compare_exchange_strong (canderef, 0);
		samplecnt_t const rate = _session.nominal_sample_rate ();

		auto a = _audio_input_ports.begin ();
		auto m = _midi_input_ports.begin ();
		for (auto p = _bufs.audio_begin (); p != _bufs.audio_end (); ++p, ++a) {
			AudioBuffer const& ab (*p);
			PortManager::AudioInputPort& ai (a->second);
			ai.apply_falloff (n_samples, rate, reset);
			ai.process (ab.data (), n_samples, reset);
		}
		for (auto p = _bufs.midi_begin (); p != _bufs.midi_end (); ++p, ++m) {
			PortManager::MIDIInputPort& mi (m->second);
			MidiBuffer const& mb (*p);
			mi.apply_falloff (n_samples, rate, reset);
			for (MidiBuffer::const_iterator i = mb.begin (); i != mb.end (); ++i) {
				Evoral::Event<samplepos_t> ev (*i, false);
				mi.process_event (ev.buffer (), ev.size ());
			}
		}
	}
	_output->flush_buffers (n_samples);;

	const samplecnt_t l = effective_latency ();
	if (_plugin_signal_latency != l) {
		_plugin_signal_latency = l;
		LatencyChanged (); /* EMIT SIGNAL */
	}

	_timing_stats.update ();
}

void
IOPlug::reset_input_meters ()
{
	_reset_meters.store (1);
}

bool
IOPlug::get_stats (PBD::microseconds_t& min, PBD::microseconds_t& max, double& avg, double& dev) const
{
	return _timing_stats.get_stats (min, max, avg, dev);
}

void
IOPlug::clear_stats ()
{
	_stat_reset.store (1);
}

std::shared_ptr<ReadOnlyControl>
IOPlug::control_output (uint32_t num) const
{
	CtrlOutMap::const_iterator i = _control_outputs.find (num);
	if (i == _control_outputs.end ()) {
		return std::shared_ptr<ReadOnlyControl> ();
	} else {
		return (*i).second;
	}
}

bool
IOPlug::load_preset (Plugin::PresetRecord pr)
{
	return _plugin->load_preset (pr);
}

bool
IOPlug::write_immediate_event (Evoral::EventType event_type, size_t size, const uint8_t* buf)
{
	return _plugin->write_immediate_event (event_type, size, buf);
}

std::shared_ptr<Evoral::Control>
IOPlug::control_factory(const Evoral::Parameter& param)
{
	Evoral::Control*                  control   = NULL;
	ParameterDescriptor               desc(param);
	std::shared_ptr<AutomationList> list;

#if 0
	if (param.type() == PluginAutomation) {
		_plugin->get_parameter_descriptor(param.id(), desc);
		control = new IOPlug::PluginControl (pi, param, desc);
	} else if (param.type() == PluginPropertyAutomation) {
		desc = _plugin->get_property_descriptor (param.id());
		if (desc.datatype != Variant::NOTHING) {
			control = new IOPlug::PluginPropertyControl(pi, param, desc, list);
		}
	}
#endif

	if (!control) {
		std::shared_ptr<AutomationList> list;
		control = new AutomationControl (_session, param, desc, list);
	}

	return std::shared_ptr<Evoral::Control>(control);
}

std::string
IOPlug::describe_parameter (Evoral::Parameter param)
{
	if (param.type() == PluginAutomation) {
		return _plugin->describe_parameter (param);
	} else if (param.type() == PluginPropertyAutomation) {
		return string_compose ("Property %1", URIMap::instance ().id_to_uri (param.id()));
	}
	return EventTypeMap::instance ().to_symbol (param);
}

bool
IOPlug::direct_feeds_according_to_reality (std::shared_ptr<GraphNode> node, bool* via_send_only)
{
	std::shared_ptr<IOPlug> other (std::dynamic_pointer_cast<IOPlug> (node));
	assert (other && other->_pre == _pre);
	if (via_send_only) {
		*via_send_only = false;
	}
	return other->input()->connected_to (_output);
}

/* ****************************************************************************/

bool
IOPlug::can_reset_all_parameters ()
{
	bool all = true;
	uint32_t params = 0;
	for (uint32_t par = 0; par < _plugin->parameter_count(); ++par) {
		bool ok=false;
		const uint32_t cid = _plugin->nth_parameter (par, ok);

		if (!ok || !_plugin->parameter_is_input(cid)) {
			continue;
		}

		++params;
	}
	return all && (params > 0);
}

bool
IOPlug::reset_parameters_to_default ()
{
	bool all = true;

	for (uint32_t par = 0; par < _plugin->parameter_count(); ++par) {
		bool ok=false;
		const uint32_t cid = _plugin->nth_parameter (par, ok);

		if (!ok || !_plugin->parameter_is_input(cid)) {
			continue;
		}

		const float dflt = _plugin->default_value (cid);
		const float curr = _plugin->get_parameter (cid);

		if (dflt == curr) {
			continue;
		}

		std::shared_ptr<AutomationControl> ac = std::dynamic_pointer_cast<AutomationControl>(control (Evoral::Parameter(PluginAutomation, 0, cid)));
		if (!ac) {
			continue;
		}

		ac->set_value (dflt, Controllable::NoGroup);
	}
	return all;
}
