/* This file is part of Patchage.  Copyright (C) 2005 Dave Robillard.
 * 
 * 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 2 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 this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <string>
#include <iostream>
#include <cassert>
#include <sys/poll.h>
#include <errno.h>
#include "PatchageFlowCanvas.h"
#include "AlsaDriver.h"
#include "Patchage.h"
#include "PatchageModule.h"
#include "PatchagePort.h"

using std::cerr;
using std::string;


using namespace LibFlowCanvas;

AlsaDriver::AlsaDriver(Patchage* app)
: m_app(app),
  m_seq(NULL)
{
}


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


/** Attach to ALSA.
 * @a launch_daemon is ignored, as ALSA has no daemon to launch/connect to.
 */
void
AlsaDriver::attach(bool /*launch_daemon*/)
{
	int ret = snd_seq_open(&m_seq, "default",
	                       SND_SEQ_OPEN_DUPLEX,
	                       SND_SEQ_NONBLOCK);
	if (ret) {
		m_app->status_message("[ALSA] Unable to attach");
		m_seq = NULL;
	} else {
		m_app->status_message("[ALSA] Attached");

		snd_seq_set_client_name(m_seq, "Patchage");
	
		ret = pthread_create(&m_refresh_thread, NULL, &AlsaDriver::refresh_main, this);
		if (ret)
			cerr << "Couldn't start refresh thread" << endl;
		
		signal_attached.emit();
	}
}


void
AlsaDriver::detach() 
{
	if (m_seq) {
		pthread_cancel(m_refresh_thread);
		pthread_join(m_refresh_thread, NULL);
		snd_seq_close(m_seq);
		m_seq = NULL;
		signal_detached.emit();
		m_app->status_message("[ALSA] Detached");
	}
}


/** Refresh all Alsa Midi ports and connections.
 */
void
AlsaDriver::refresh()
{
	if (!is_attached())
		return;

	assert(m_seq);
	
	refresh_ports();
	refresh_connections();

	undirty();
}


boost::shared_ptr<PatchagePort>
AlsaDriver::create_port(boost::shared_ptr<PatchageModule> parent,
		const string& name, bool is_input, snd_seq_addr_t addr)
{
	boost::shared_ptr<PatchagePort> ret(
		new PatchagePort(parent, ALSA_MIDI, name, is_input,
			m_app->state_manager()->get_port_color(ALSA_MIDI)));
	ret->alsa_addr(addr);
	return ret;
}


/** Refresh all Alsa Midi ports. 
 */
void
AlsaDriver::refresh_ports()
{
	assert(is_attached());
	assert(m_seq);
	
	snd_seq_client_info_t* cinfo;
	snd_seq_client_info_alloca(&cinfo);
	snd_seq_client_info_set_client(cinfo, -1);

	snd_seq_port_info_t * pinfo;
	snd_seq_port_info_alloca(&pinfo);
	
	string client_name;
	string port_name;
	bool is_input       = false;
	bool is_duplex      = false;
	bool is_application = true;
	
	while (snd_seq_query_next_client (m_seq, cinfo) >= 0) {
		snd_seq_port_info_set_client(pinfo, snd_seq_client_info_get_client(cinfo));
		snd_seq_port_info_set_port(pinfo, -1);

		client_name = snd_seq_client_info_get_name(cinfo);

		while (snd_seq_query_next_port(m_seq, pinfo) >= 0) {
			int caps = snd_seq_port_info_get_capability(pinfo);
			int type = snd_seq_port_info_get_type(pinfo);

			// Skip ports we shouldn't show
			if (caps & SND_SEQ_PORT_CAP_NO_EXPORT)
				continue;
			else if ( !( (caps & SND_SEQ_PORT_CAP_READ)
						|| (caps & SND_SEQ_PORT_CAP_WRITE)
						|| (caps & SND_SEQ_PORT_CAP_DUPLEX)))
				continue;
			else if ((snd_seq_client_info_get_type(cinfo) != SND_SEQ_USER_CLIENT)
					&& ((type == SND_SEQ_PORT_SYSTEM_TIMER
						|| type == SND_SEQ_PORT_SYSTEM_ANNOUNCE)))
				continue;
			
			const snd_seq_addr_t addr = *snd_seq_port_info_get_addr(pinfo);
			
			is_duplex = false;

			// FIXME: Should be CAP_SUBS_READ etc?
			if ((caps & SND_SEQ_PORT_CAP_READ) && (caps & SND_SEQ_PORT_CAP_WRITE))
				is_duplex = true;
			else if (caps & SND_SEQ_PORT_CAP_READ)
				is_input = false;
			else if (caps & SND_SEQ_PORT_CAP_WRITE)
				is_input = true;

			is_application = (type & SND_SEQ_PORT_TYPE_APPLICATION);
			port_name = snd_seq_port_info_get_name(pinfo);
			boost::shared_ptr<PatchageModule> m;
			
			//cout << client_name << " : " << port_name << " is_application = " << is_application
			//	<< " is_duplex = " << is_duplex << endl;

			bool split =  m_app->state_manager()->get_module_split(client_name, !is_application);
			
			// Application input/output ports go on the same module
			if (!split) {
				m = m_app->canvas()->find_module(client_name, InputOutput);
				if (!m) {
					m = boost::shared_ptr<PatchageModule>(new PatchageModule(m_app, client_name, InputOutput));
					m->load_location();
					m->store_location();
				}
				
				if (!m->get_port(port_name)) {
					if (!is_duplex) {
						m->add_port(create_port(m, port_name, is_input, addr));
					} else {
						m->add_port(create_port(m, port_name, true, addr));
						m->add_port(create_port(m, port_name, false, addr));
					}
				}
			} else { // non-application input/output ports (hw interface, etc) go on separate modules
				ModuleType type = InputOutput;
				
				// The 'application' hint isn't always set by clients, so this bit
				// is pretty nasty...

				if (!is_duplex) {  // just one port to add
					if (is_input) type = Input;
					else type = Output;
					
					// See if an InputOutput module exists (maybe with Jack ports on it)
					m = m_app->canvas()->find_module(client_name, InputOutput);
				
					if (!m)
						m = m_app->canvas()->find_module(client_name, type);

					if (!m) {
						m = boost::shared_ptr<PatchageModule>(
							new PatchageModule(m_app, client_name, type));
						m->load_location();
						m->store_location();
					}
					if (!m->get_port(port_name))
						m->add_port(create_port(m, port_name, is_input, addr));
				} else {  // two ports to add
					type = Input;
					
					// See if an InputOutput module exists (maybe with Jack ports on it)
					m = m_app->canvas()->find_module(client_name, InputOutput);
					
					if (!m)
						m = m_app->canvas()->find_module(client_name, type);

					if (!m) {
						m = boost::shared_ptr<PatchageModule>(
							new PatchageModule(m_app, client_name, type));
						m->load_location();
						m->store_location();
					}

					assert(m);

					if (!m->get_port(port_name))
						m->add_port(create_port(m, port_name, true, addr));

					type = Output;
					
					// See if an InputOutput module exists (maybe with Jack ports on it)
					m = m_app->canvas()->find_module(client_name, InputOutput);
					
					if (!m)
						m = m_app->canvas()->find_module(client_name, type);

					if (!m) {
						m = boost::shared_ptr<PatchageModule>(
							new PatchageModule(m_app, client_name, type));
						m->load_location();
						m->store_location();
					}
					if (!m->get_port(port_name))
						m->add_port(create_port(m, port_name, false, addr));
				}
			}
			m->resize();
			m_app->canvas()->add_module(m);
		}
	}
}


/** Refresh all Alsa Midi connections.
 */
void
AlsaDriver::refresh_connections()
{
	assert(is_attached());
	assert(m_seq);
	
	boost::shared_ptr<PatchageModule> m;
	boost::shared_ptr<PatchagePort>   p;
	
	for (ModuleMap::iterator i = m_app->canvas()->modules().begin();
			i != m_app->canvas()->modules().end(); ++i) {
		m = boost::dynamic_pointer_cast<PatchageModule>((*i).second);
		if (m) {
			for (PortVector::const_iterator j = m->ports().begin(); j != m->ports().end(); ++j) {
				p = boost::dynamic_pointer_cast<PatchagePort>(*j);
				if (p->type() == ALSA_MIDI)
					add_connections(p);
			}
		}
	}
}


/** Add all connections for the given port.
 */
void
AlsaDriver::add_connections(boost::shared_ptr<PatchagePort> port)
{
	assert(is_attached());
	assert(m_seq);
	
	const snd_seq_addr_t* addr = port->alsa_addr();
	boost::shared_ptr<PatchagePort> connected_port;
	
	// Fix a problem with duplex->duplex connections (would show up twice)
	// No sense doing them all twice anyway..
	if (port->is_input())
		return;
	
	snd_seq_query_subscribe_t* subsinfo;
	snd_seq_query_subscribe_alloca(&subsinfo);
	snd_seq_query_subscribe_set_root(subsinfo, addr);
	snd_seq_query_subscribe_set_index(subsinfo, 0);
	
	while(!snd_seq_query_port_subscribers(m_seq, subsinfo)) {
		const snd_seq_addr_t* connected_addr = snd_seq_query_subscribe_get_addr(subsinfo);
		
		connected_port = m_app->canvas()->find_port(connected_addr);

		if (connected_port) {
			boost::shared_ptr<Connection> existing = m_app->canvas()->get_connection(port, connected_port);
			if (existing) {
				existing->set_flagged(false);
			} else {
				m_app->canvas()->add_connection(port, connected_port);
			}
		}

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

}


/** Connects two Alsa Midi ports.
 * 
 * \return Whether connection succeeded.
 */
bool
AlsaDriver::connect(boost::shared_ptr<PatchagePort> src_port, boost::shared_ptr<PatchagePort> dst_port) 
{
	const snd_seq_addr_t* src = src_port->alsa_addr();
	const snd_seq_addr_t* dst = dst_port->alsa_addr();

	bool result = true;
	
	if (src && dst) {
		snd_seq_port_subscribe_t* subs;
		snd_seq_port_subscribe_malloc(&subs);
		snd_seq_port_subscribe_set_sender(subs, src);
		snd_seq_port_subscribe_set_dest(subs, dst);
		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(m_seq, subs)) {
			cerr << "Error: Attempt to subscribe Alsa ports that are already subscribed." << endl;
			result = false;
		}

		int ret = snd_seq_subscribe_port(m_seq, subs);
		if (ret < 0) {
			cerr << "Alsa subscription failed: " << snd_strerror(ret) << endl;
			result = false;
		}
	}
	
	if (result)
		m_app->status_message(string("[ALSA] Connected ")
			+ src_port->full_name() + " -> " + dst_port->full_name());
	else
		m_app->status_message(string("[ALSA] Unable to connect ")
			+ src_port->full_name() + " -> " + dst_port->full_name());

	return (!result);
}


/** Disconnects two Alsa Midi ports.
 * 
 * \return Whether disconnection succeeded.
 */
bool
AlsaDriver::disconnect(boost::shared_ptr<PatchagePort> src_port, boost::shared_ptr<PatchagePort> dst_port) 
{
	const snd_seq_addr_t* src = src_port->alsa_addr();
	const snd_seq_addr_t* dst = dst_port->alsa_addr();
	
	bool result = true;

	snd_seq_port_subscribe_t* subs;
	snd_seq_port_subscribe_malloc(&subs);
	snd_seq_port_subscribe_set_sender(subs, src);
	snd_seq_port_subscribe_set_dest(subs, dst);
	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(m_seq, subs) != 0) {
		cerr << "Error: Attempt to unsubscribe Alsa ports that are not subscribed." << endl;
		result = false;
	}
	
	int ret = snd_seq_unsubscribe_port(m_seq, subs);
	if (ret < 0) {
		cerr << "Alsa unsubscription failed: " << snd_strerror(ret) << endl;
		result = false;
	}
	
	if (result)
		m_app->status_message(string("[ALSA] Disconnected ")
			+ src_port->full_name() + " -> " + dst_port->full_name());
	else
		m_app->status_message(string("[ALSA] Unable to disconnect ")
			+ src_port->full_name() + " -> " + dst_port->full_name());

	return (!result);
}


bool
AlsaDriver::create_refresh_port()
{
	// Mostly lifted from alsa-patch-bay, (C) 2002 Robert Ham, released under GPL

	int ret;
	snd_seq_port_info_t* port_info;

	snd_seq_port_info_alloca(&port_info);
	snd_seq_port_info_set_name(port_info, "System Announcement Reciever");
	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);
	
	snd_seq_port_info_set_type(port_info, SND_SEQ_PORT_TYPE_APPLICATION);

	ret = snd_seq_create_port(m_seq, port_info);
	if (ret) {
		cerr << "Error creating alsa port: " << snd_strerror(ret) << endl;
		return false;
	}

	// Subscribe the port to the system announcer
	ret = snd_seq_connect_from(m_seq,
		snd_seq_port_info_get_port(port_info),
		SND_SEQ_CLIENT_SYSTEM,
		SND_SEQ_PORT_SYSTEM_ANNOUNCE);
	
	if (ret) {
		cerr << "Could not connect to system announcer port: " << snd_strerror(ret) << endl;
		return false;
	}

	return true;
}


void*
AlsaDriver::refresh_main(void* me)
{
	AlsaDriver* ad = (AlsaDriver*)me;
	ad->m_refresh_main();
	return NULL;
}


void
AlsaDriver::m_refresh_main()
{
	// "Heavily influenced" from alsa-patch-bay
	// (C) 2002 Robert Ham, released under GPL

	int             ret;
	int             nfds    = snd_seq_poll_descriptors_count(m_seq, POLLIN);
	struct pollfd*  pfds    = new struct pollfd[nfds];
	unsigned short* revents = new unsigned short[nfds];

	if (!create_refresh_port()) {
		cerr << "Could not create Alsa listen port.  Auto refreshing will not work." << endl;
		return;
	}

	snd_seq_poll_descriptors(m_seq, pfds, nfds, POLLIN);

	while (true) {
		ret = poll(pfds, nfds, -1);
		if (ret == -1) {
			if (errno == EINTR)
				continue;

			cerr << "Error polling Alsa sequencer: " << strerror(errno) << endl;
			continue;
		}

		ret = snd_seq_poll_descriptors_revents(m_seq, pfds, nfds, revents);
		if (ret) {
			cerr << "Error getting Alsa sequencer poll events: "
				<< snd_strerror(ret) << endl;
			continue;
		}

		for (int i = 0; i < nfds; ++i) {
			if (revents[i] > 0) {
				snd_seq_event_t* ev;
				snd_seq_event_input(m_seq, &ev);

				if (ev == NULL)
					continue;

				switch (ev->type) {
				case SND_SEQ_EVENT_RESET:
				case SND_SEQ_EVENT_CLIENT_START:
				case SND_SEQ_EVENT_CLIENT_EXIT:
				case SND_SEQ_EVENT_CLIENT_CHANGE:
				case SND_SEQ_EVENT_PORT_START:
				case SND_SEQ_EVENT_PORT_EXIT:
				case SND_SEQ_EVENT_PORT_CHANGE:
				case SND_SEQ_EVENT_PORT_SUBSCRIBED:
				case SND_SEQ_EVENT_PORT_UNSUBSCRIBED:
					m_is_dirty = true;
					break;
				default:
					break;
				}
			}
		}
	}
}