/* This file is part of Patchage.
 * Copyright 2007-2020 David Robillard <d@drobilla.net>
 *
 * Patchage 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 3 of the License, or (at your option)
 * any later version.
 *
 * Patchage 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 details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Patchage.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "AlsaDriver.hpp"

#include "ClientID.hpp"
#include "ClientInfo.hpp"
#include "Patchage.hpp"
#include "PatchageCanvas.hpp"
#include "PortInfo.hpp"
#include "PortType.hpp"
#include "SignalDirection.hpp"

PATCHAGE_DISABLE_FMT_WARNINGS
#include <fmt/core.h>
#include <fmt/ostream.h>
PATCHAGE_RESTORE_WARNINGS

#include <cassert>
#include <limits>
#include <set>
#include <string>
#include <utility>

namespace {

PortID
addr_to_id(const snd_seq_addr_t& addr, const bool is_input)
{
	return PortID::alsa(addr.client, addr.port, is_input);
}

SignalDirection
port_direction(const snd_seq_port_info_t* const pinfo)
{
	const int caps = snd_seq_port_info_get_capability(pinfo);

	if ((caps & SND_SEQ_PORT_CAP_READ) && (caps & SND_SEQ_PORT_CAP_WRITE)) {
		return SignalDirection::duplex;
	}

	if (caps & SND_SEQ_PORT_CAP_READ) {
		return SignalDirection::output;
	}

	if (caps & SND_SEQ_PORT_CAP_WRITE) {
		return SignalDirection::input;
	}

	return SignalDirection::duplex;
}

ClientInfo
client_info(snd_seq_client_info_t* const cinfo)
{
	return {snd_seq_client_info_get_name(cinfo)};
}

PortInfo
port_info(const snd_seq_port_info_t* const pinfo)
{
	const int type = snd_seq_port_info_get_type(pinfo);

	return {snd_seq_port_info_get_name(pinfo),
	        PortType::alsa_midi,
	        port_direction(pinfo),
	        snd_seq_port_info_get_port(pinfo),
	        (type & SND_SEQ_PORT_TYPE_APPLICATION) == 0};
}

} // namespace

AlsaDriver::AlsaDriver(ILog& log, EventSink emit_event)
    : Driver{std::move(emit_event)}
    , _log(log)
    , _seq(nullptr)
    , _refresh_thread{}
{}

AlsaDriver::~AlsaDriver()
{
	detach();
}

void
AlsaDriver::attach(bool /*launch_daemon*/)
{
	int ret = snd_seq_open(&_seq, "default", SND_SEQ_OPEN_DUPLEX, 0);
	if (ret) {
		_log.error("[ALSA] Unable to attach");
		_seq = nullptr;
	} else {
		_log.info("[ALSA] Attached");

		snd_seq_set_client_name(_seq, "Patchage");

		pthread_attr_t attr;
		pthread_attr_init(&attr);
		pthread_attr_setstacksize(&attr, 50000);

		ret = pthread_create(
		    &_refresh_thread, &attr, &AlsaDriver::refresh_main, this);
		if (ret) {
			_log.error("[ALSA] Failed to start refresh thread");
		}

		signal_attached.emit();
	}
}

void
AlsaDriver::detach()
{
	if (_seq) {
		pthread_cancel(_refresh_thread);
		pthread_join(_refresh_thread, nullptr);
		snd_seq_close(_seq);
		_seq = nullptr;
		signal_detached.emit();
		_log.info("[ALSA] Detached");
	}
}

void
AlsaDriver::refresh(const EventSink& sink)
{
	if (!is_attached() || !_seq) {
		return;
	}

	_ignored.clear();

	snd_seq_client_info_t* cinfo = nullptr;
	snd_seq_client_info_alloca(&cinfo);
	snd_seq_client_info_set_client(cinfo, -1);

	snd_seq_port_info_t* pinfo = nullptr;
	snd_seq_port_info_alloca(&pinfo);

	// Emit all clients
	snd_seq_client_info_set_client(cinfo, -1);
	while (snd_seq_query_next_client(_seq, cinfo) >= 0) {
		const auto client_id = snd_seq_client_info_get_client(cinfo);

		assert(client_id < std::numeric_limits<uint8_t>::max());
		sink({ClientCreationEvent{
		    ClientID::alsa(static_cast<uint8_t>(client_id)),
		    client_info(cinfo)}});
	}

	// Emit all ports
	snd_seq_client_info_set_client(cinfo, -1);
	while (snd_seq_query_next_client(_seq, cinfo) >= 0) {
		const auto client_id = snd_seq_client_info_get_client(cinfo);

		snd_seq_port_info_set_client(pinfo, client_id);
		snd_seq_port_info_set_port(pinfo, -1);
		while (snd_seq_query_next_port(_seq, pinfo) >= 0) {
			const auto addr = *snd_seq_port_info_get_addr(pinfo);
			if (!ignore(addr)) {
				const auto caps = snd_seq_port_info_get_capability(pinfo);
				auto       info = port_info(pinfo);

				if (caps & SND_SEQ_PORT_CAP_READ) {
					info.direction = SignalDirection::input;
					sink({PortCreationEvent{addr_to_id(addr, true), info}});
				}

				if (caps & SND_SEQ_PORT_CAP_WRITE) {
					info.direction = SignalDirection::output;
					sink({PortCreationEvent{addr_to_id(addr, false), info}});
				}
			}
		}
	}

	// Emit all connections
	snd_seq_client_info_set_client(cinfo, -1);
	while (snd_seq_query_next_client(_seq, cinfo) >= 0) {
		const auto client_id = snd_seq_client_info_get_client(cinfo);

		snd_seq_port_info_set_client(pinfo, client_id);
		snd_seq_port_info_set_port(pinfo, -1);
		while (snd_seq_query_next_port(_seq, pinfo) >= 0) {
			const auto tail_addr = *snd_seq_port_info_get_addr(pinfo);
			const auto tail_id   = addr_to_id(tail_addr, false);
			if (ignore(tail_addr)) {
				continue;
			}

			snd_seq_query_subscribe_t* sinfo = nullptr;
			snd_seq_query_subscribe_alloca(&sinfo);
			snd_seq_query_subscribe_set_root(sinfo, &tail_addr);
			snd_seq_query_subscribe_set_index(sinfo, 0);
			while (!snd_seq_query_port_subscribers(_seq, sinfo)) {
				const auto head_addr = *snd_seq_query_subscribe_get_addr(sinfo);
				const auto head_id   = addr_to_id(head_addr, true);

				sink({ConnectionEvent{tail_id, head_id}});

				snd_seq_query_subscribe_set_index(
				    sinfo, snd_seq_query_subscribe_get_index(sinfo) + 1);
			}
		}
	}
}

bool
AlsaDriver::ignore(const snd_seq_addr_t& addr, bool add)
{
	if (_ignored.find(addr) != _ignored.end()) {
		return true;
	}

	if (!add) {
		return false;
	}

	snd_seq_client_info_t* cinfo = nullptr;
	snd_seq_client_info_alloca(&cinfo);
	snd_seq_client_info_set_client(cinfo, addr.client);
	snd_seq_get_any_client_info(_seq, addr.client, cinfo);

	snd_seq_port_info_t* pinfo = nullptr;
	snd_seq_port_info_alloca(&pinfo);
	snd_seq_port_info_set_client(pinfo, addr.client);
	snd_seq_port_info_set_port(pinfo, addr.port);
	snd_seq_get_any_port_info(_seq, addr.client, addr.port, pinfo);

	const int type = snd_seq_port_info_get_type(pinfo);
	const int caps = snd_seq_port_info_get_capability(pinfo);

	if (caps & SND_SEQ_PORT_CAP_NO_EXPORT) {
		_ignored.insert(addr);
		return true;
	}

	if (!((caps & SND_SEQ_PORT_CAP_READ) || (caps & SND_SEQ_PORT_CAP_WRITE) ||
	      (caps & SND_SEQ_PORT_CAP_DUPLEX))) {
		_ignored.insert(addr);
		return true;
	}

	if ((snd_seq_client_info_get_type(cinfo) != SND_SEQ_USER_CLIENT) &&
	    ((type == SND_SEQ_PORT_SYSTEM_TIMER ||
	      type == SND_SEQ_PORT_SYSTEM_ANNOUNCE))) {
		_ignored.insert(addr);
		return true;
	}

	return false;
}

bool
AlsaDriver::connect(const PortID tail_id, const PortID head_id)
{
	if (tail_id.type() != PortID::Type::alsa ||
	    head_id.type() != PortID::Type::alsa) {
		_log.error("[ALSA] Attempt to connect non-ALSA ports");
		return false;
	}

	const snd_seq_addr_t tail_addr = {tail_id.alsa_client(),
	                                  tail_id.alsa_port()};

	const snd_seq_addr_t head_addr = {head_id.alsa_client(),
	                                  head_id.alsa_port()};

	if (tail_addr.client == head_addr.client &&
	    tail_addr.port == head_addr.port) {
		_log.warning("[ALSA] Refusing to connect port to itself");
		return false;
	}

	bool result = true;

	snd_seq_port_subscribe_t* subs = nullptr;
	snd_seq_port_subscribe_malloc(&subs);
	snd_seq_port_subscribe_set_sender(subs, &tail_addr);
	snd_seq_port_subscribe_set_dest(subs, &head_addr);
	snd_seq_port_subscribe_set_exclusive(subs, 0);
	snd_seq_port_subscribe_set_time_update(subs, 0);
	snd_seq_port_subscribe_set_time_real(subs, 0);

	// Already connected (shouldn't happen)
	if (!snd_seq_get_port_subscription(_seq, subs)) {
		_log.error("[ALSA] Attempt to double subscribe ports");
		result = false;
	}

	int ret = snd_seq_subscribe_port(_seq, subs);
	if (ret < 0) {
		_log.error(
		    fmt::format("[ALSA] Subscription failed ({})", snd_strerror(ret)));
		result = false;
	}

	if (result) {
		_log.info(fmt::format("[ALSA] Connected {} => {}", tail_id, head_id));
	} else {
		_log.error(
		    fmt::format("[ALSA] Failed to connect {} => {}", tail_id, head_id));
	}

	return (!result);
}

bool
AlsaDriver::disconnect(const PortID tail_id, const PortID head_id)
{
	if (tail_id.type() != PortID::Type::alsa ||
	    head_id.type() != PortID::Type::alsa) {
		_log.error("[ALSA] Attempt to disconnect non-ALSA ports");
		return false;
	}

	const snd_seq_addr_t tail_addr = {tail_id.alsa_client(),
	                                  tail_id.alsa_port()};

	const snd_seq_addr_t head_addr = {head_id.alsa_client(),
	                                  head_id.alsa_port()};

	snd_seq_port_subscribe_t* subs = nullptr;
	snd_seq_port_subscribe_malloc(&subs);
	snd_seq_port_subscribe_set_sender(subs, &tail_addr);
	snd_seq_port_subscribe_set_dest(subs, &head_addr);
	snd_seq_port_subscribe_set_exclusive(subs, 0);
	snd_seq_port_subscribe_set_time_update(subs, 0);
	snd_seq_port_subscribe_set_time_real(subs, 0);

	// Not connected (shouldn't happen)
	if (snd_seq_get_port_subscription(_seq, subs) != 0) {
		_log.error(
		    "[ALSA] Attempt to unsubscribe ports that are not subscribed");
		return false;
	}

	int ret = snd_seq_unsubscribe_port(_seq, subs);
	if (ret < 0) {
		_log.error(fmt::format("[ALSA] Failed to disconnect {} => {} ({})",
		                       tail_id,
		                       head_id,
		                       snd_strerror(ret)));
		return false;
	}

	_log.info(fmt::format("[ALSA] Disconnected {} => {}", tail_id, head_id));

	return true;
}

bool
AlsaDriver::create_refresh_port()
{
	snd_seq_port_info_t* port_info = nullptr;
	snd_seq_port_info_alloca(&port_info);
	snd_seq_port_info_set_name(port_info, "System Announcement Receiver");
	snd_seq_port_info_set_type(port_info, SND_SEQ_PORT_TYPE_APPLICATION);
	snd_seq_port_info_set_capability(port_info,
	                                 SND_SEQ_PORT_CAP_WRITE |
	                                     SND_SEQ_PORT_CAP_SUBS_WRITE |
	                                     SND_SEQ_PORT_CAP_NO_EXPORT);

	int ret = snd_seq_create_port(_seq, port_info);
	if (ret) {
		_log.error(
		    fmt::format("[ALSA] Error creating port ({})", snd_strerror(ret)));
		return false;
	}

	// Subscribe the port to the system announcer
	ret = snd_seq_connect_from(_seq,
	                           snd_seq_port_info_get_port(port_info),
	                           SND_SEQ_CLIENT_SYSTEM,
	                           SND_SEQ_PORT_SYSTEM_ANNOUNCE);
	if (ret) {
		_log.error(
		    fmt::format("[ALSA] Failed to connect to system announce port ({})",
		                snd_strerror(ret)));
		return false;
	}

	return true;
}

void*
AlsaDriver::refresh_main(void* me)
{
	auto* ad = static_cast<AlsaDriver*>(me);
	ad->_refresh_main();
	return nullptr;
}

void
AlsaDriver::_refresh_main()
{
	if (!create_refresh_port()) {
		_log.error(
		    "[ALSA] Could not create listen port, auto-refresh disabled");
		return;
	}

	int caps = 0;

	snd_seq_client_info_t* cinfo = nullptr;
	snd_seq_client_info_alloca(&cinfo);

	snd_seq_port_info_t* pinfo = nullptr;
	snd_seq_port_info_alloca(&pinfo);

	snd_seq_event_t* ev = nullptr;
	while (snd_seq_event_input(_seq, &ev) > 0) {
		assert(ev);

		switch (ev->type) {
		case SND_SEQ_EVENT_CLIENT_START:
			snd_seq_get_any_client_info(_seq, ev->data.addr.client, cinfo);
			_emit_event(ClientCreationEvent{
			    ClientID::alsa(ev->data.addr.client),
			    client_info(cinfo),
			});
			break;

		case SND_SEQ_EVENT_CLIENT_EXIT:
			_emit_event(ClientDestructionEvent{
			    ClientID::alsa(ev->data.addr.client),
			});
			break;

		case SND_SEQ_EVENT_CLIENT_CHANGE:
			break;

		case SND_SEQ_EVENT_PORT_START:
			snd_seq_get_any_client_info(_seq, ev->data.addr.client, cinfo);
			snd_seq_get_any_port_info(
			    _seq, ev->data.addr.client, ev->data.addr.port, pinfo);
			caps = snd_seq_port_info_get_capability(pinfo);

			if (!ignore(ev->data.addr)) {
				_emit_event(PortCreationEvent{
				    addr_to_id(ev->data.addr, (caps & SND_SEQ_PORT_CAP_READ)),
				    port_info(pinfo),
				});
			}
			break;

		case SND_SEQ_EVENT_PORT_EXIT:
			if (!ignore(ev->data.addr, false)) {
				// Note: getting caps at this point does not work
				// Delete both inputs and outputs (to handle duplex ports)
				_emit_event(
				    PortDestructionEvent{addr_to_id(ev->data.addr, true)});
				_emit_event(
				    PortDestructionEvent{addr_to_id(ev->data.addr, false)});
			}
			break;

		case SND_SEQ_EVENT_PORT_CHANGE:
			break;

		case SND_SEQ_EVENT_PORT_SUBSCRIBED:
			if (!ignore(ev->data.connect.sender) &&
			    !ignore(ev->data.connect.dest)) {
				_emit_event(
				    ConnectionEvent{addr_to_id(ev->data.connect.sender, false),
				                    addr_to_id(ev->data.connect.dest, true)});
			}
			break;

		case SND_SEQ_EVENT_PORT_UNSUBSCRIBED:
			if (!ignore(ev->data.connect.sender) &&
			    !ignore(ev->data.connect.dest)) {
				_emit_event(DisconnectionEvent{
				    addr_to_id(ev->data.connect.sender, false),
				    addr_to_id(ev->data.connect.dest, true)});
			}
			break;

		case SND_SEQ_EVENT_RESET:
		default:
			break;
		}
	}
}