/////////////////////////////////////////////////////////////////////////////
// File:        mod_midi.cpp
// Author:      Cesar Mauri Loba (cesar at crea-si dot com)
// Copyright:   (C) 2011 Cesar Mauri Loba - CREA Software Systems
//
//  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 3 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, see <http://www.gnu.org/licenses/>.
/////////////////////////////////////////////////////////////////////////////

#include "spcore/component.h"
#include "spcore/module.h"
#include "spcore/basictypes.h"
#include "spcore/libimpexp.h"
#include "spcore/pinimpl.h"
#include "portmidi.h"
#include "midiconfiggui.h"
#include "mod_midi/midi_types.h"

#include <string>
#include <boost/static_assert.hpp>

BOOST_STATIC_ASSERT(sizeof(unsigned int) == sizeof(PmTimestamp));
BOOST_STATIC_ASSERT(sizeof(unsigned int) == sizeof(PmMessage));

using namespace spcore;
using namespace std;

namespace mod_midi {

/* ******************************************************************************
	midi_config_gui component
****************************************************************************** */
class MidiConfigGui : public spcore::CComponentAdapter {
public:
	static const char* getTypeName() { return "midi_config_gui"; };
	virtual const char* GetTypeName() const { return MidiConfigGui::getTypeName(); };

	MidiConfigGui(const char * name, int argc, const char * argv[])
	: CComponentAdapter(name, argc, argv)
	{
	}

	virtual wxWindow* GetGUI(wxWindow * parent) {
		return new MIDIConfigGui( parent );
	}
};

typedef ComponentFactory<MidiConfigGui> MidiConfigGuiFactory;


/* ******************************************************************************
	midi_config component
	This manages the midi subsystem configuration
****************************************************************************** */

class MidiConfig : public spcore::CComponentAdapter {

private:
	typedef struct {
		const PmDeviceInfo* devInfo;
		PmDeviceID id;
	} DevInfo;

public:
	static const char* getTypeName() { return "midi_config"; };
	virtual const char* GetTypeName() const { return MidiConfig::getTypeName(); };

	MidiConfig(const char * name, int argc, const char * argv[])
	: CComponentAdapter(name, argc, argv)
	, m_outDevice(0)
	{
		// Initialize portmidi subsystem
		PmError errVal= Pm_Initialize();
		if (errVal!= pmNoError)
			throw std::runtime_error("midi_config. portmidi initialization failed");

		// Counts the number of devices
		int numDevices= Pm_CountDevices( );

		// Store output devices
		for (int i= 0; i< numDevices; ++i) {
			DevInfo devInfo;

			devInfo.devInfo= Pm_GetDeviceInfo( i );
			assert (devInfo.devInfo);
			if (devInfo.devInfo->output) {
				devInfo.id= i;
				m_outDevices.push_back(devInfo);
				if (i == Pm_GetDefaultOutputDeviceID())
					// Is this the default device? Otherwise the default is 0
					m_outDevice= m_outDevices.size() - 1;
			}
		}

		// Check if is any output device available
		if (m_outDevices.size() == 0)
			getSpCoreRuntime()->LogMessage (ICoreRuntime::LOG_WARNING, "no output midi devices found", "mod_midi");

		RegisterInputPin (*SmartPtr<InputPinOutDevice>(new InputPinOutDevice(*this), false));
		RegisterInputPin (*SmartPtr<InputPinReqStatus>(new InputPinReqStatus(*this), false));

		m_oPinDevList= CTypeComposite::CreateOutputPinLock("device_list");
		if (m_oPinDevList.get()== NULL)
			// Output pin creation failed. Shouldn't happen but...
			throw std::runtime_error("midi_config. output pin creation failed.");
		RegisterOutputPin (*m_oPinDevList);
	}

	// returns pmNoDevice if no device available
	PmDeviceID GetOutDevice() const {
		if (m_outDevices.size() == 0) {
			getSpCoreRuntime()->LogMessage (ICoreRuntime::LOG_WARNING, "no output midi devices found", "mod_midi");
			return pmNoDevice;
		}
		else if ((unsigned int) m_outDevice>= m_outDevices.size()) {
			getSpCoreRuntime()->LogMessage (ICoreRuntime::LOG_WARNING, "wrong output MIDI device", "mod_midi");
			return pmNoDevice;
		}

		return m_outDevices[m_outDevice].id;
	}

	#define OUT_DEVICE_SETTING_NAME "out_device"

	virtual void SaveSettings(IConfiguration& cfg) {
		cfg.WriteInt(OUT_DEVICE_SETTING_NAME, m_outDevice);
	}

	virtual void LoadSettings(IConfiguration& cfg) {
		int outDev= 0;
		if (cfg.ReadInt(OUT_DEVICE_SETTING_NAME, &outDev)) {
			if ((unsigned) outDev< m_outDevices.size()) m_outDevice= outDev;
		}
	}

private:
	virtual ~MidiConfig()
	{
		// portmidi cleanup
		Pm_Terminate();
	}

	void SendStatus()
	{
		SmartPtr<CTypeComposite> result= CTypeComposite::CreateInstance();

		for (unsigned int i= 0; i< m_outDevices.size(); ++i) {
			SmartPtr<CTypeString> name= CTypeString::CreateInstance();
			name->set (m_outDevices[i].devInfo->name);
			result->AddChild(name);
		}

		m_oPinDevList->Send(result);
	}

	//
	// Attributes
	//
	int m_outDevice;
	vector<DevInfo> m_outDevices;
	SmartPtr<spcore::IOutputPin> m_oPinDevList;

	// Read/Write pin to send/read selected MIDI output device
	class InputPinOutDevice : public CInputPinReadWrite<CTypeInt, MidiConfig> {
	public:
		InputPinOutDevice (MidiConfig & component)
		: CInputPinReadWrite<CTypeInt, MidiConfig>("out_device", component) {}

		virtual SmartPtr<CTypeInt> DoRead() const {
			SmartPtr<CTypeInt> result= CTypeInt::CreateInstance();
			result->setValue(m_component->m_outDevice);
			return result;
		}
		virtual int DoSend(const CTypeInt &src) {
			// Sanity check
			if ((unsigned) src.getValue()>= m_component->m_outDevices.size()) return -1;

			// Simply change the selected device
			m_component->m_outDevice= src.getValue();

			return 0;
		}
	};

	// Write pin to request send/read selected MIDI output device
	class InputPinReqStatus : public CInputPinWriteOnly<CTypeAny, MidiConfig> {
	public:
		InputPinReqStatus (MidiConfig & component)
		: CInputPinWriteOnly<CTypeAny, MidiConfig>("req_status", component) {}

		virtual int DoSend(const CTypeAny &) {
			m_component->SendStatus();
			return 0;
		}
	};
};

// MidiConfig component factory
typedef SingletonComponentFactory<MidiConfig> MidiConfigFactory;

/** *****************************************************************************
	midi_out component

	Input pins:
		message (midi_message): input message to be executed

		all_off (any): stop all notes
****************************************************************************** */
class MidiOut : public spcore::CComponentAdapter {
public:
	static const char* getTypeName() { return "midi_out"; };
	virtual const char* GetTypeName() const { return MidiOut::getTypeName(); };

	MidiOut(const char * name, int argc, const char * argv[])
	: CComponentAdapter(name, argc, argv)
	, m_stream(NULL)
	{
		RegisterInputPin (*SmartPtr<InputPinMessage>(new InputPinMessage(*this), false));
		RegisterInputPin (*SmartPtr<InputPinAllOff>(new InputPinAllOff(*this), false));
	}

	virtual int DoInitialize() {
		if (m_stream)
			// Already initialized
			return 0;

		// Obtain midi device to use
		SmartPtr<IComponent> midiConfig= getSpCoreRuntime()->CreateComponent("midi_config", "mc", 0, NULL);

		PmDeviceID outDevID= (static_cast<MidiConfig *>(midiConfig.get()))->GetOutDevice();

		// Try to open device
		PmError retcode= Pm_OpenOutput( &m_stream, outDevID,  NULL, 0, NULL, NULL, 0);

		if (retcode!= pmNoError) {
			// An error occurred
			assert (!m_stream);
			getSpCoreRuntime()->LogMessage (ICoreRuntime::LOG_ERROR, Pm_GetErrorText( retcode ), "mod_midi");
			return -1;
		}

		// Device successfully opened
		return 0;
	}

	virtual void DoFinish() {
		if (m_stream) {
			// Stop sound
			AllNotesOff ();
			// Close device
			Pm_Close (m_stream);
			m_stream= NULL;
		}
	}

	virtual void DoStop() {
		if (m_stream) {
			// Stop sound
			AllNotesOff ();			
		}
	}
private:
	//
	// Attributes
	//
	PortMidiStream*	m_stream;

	virtual ~MidiOut() {
		Finish();
	}

	void ProcessMIDIMessage (const CTypeMIDIMessage & msg) {
		assert (m_stream);
		if (m_stream) {
			PmEvent evt;

			evt.message= msg.GetBuffer();
			evt.timestamp= 0;

			Pm_Write(m_stream, &evt, 1);
		}
	}

	void AllNotesOff () {
		assert (m_stream);
		if (m_stream) {
			PmEvent buffer[32];
			int i= 0, channel= 0;
			for (; channel < 16; ++channel) {
				buffer[i].message = Pm_Message(0xB0+channel, 0x78, 0);
				buffer[i++].timestamp = 0;
				buffer[i].message = Pm_Message(0xB0+channel, 0x7B, 0);
				buffer[i++].timestamp = 0;
			}
			Pm_Write(m_stream, buffer, 32);
		}
	}

	// Write pin which receives and executes MIDI messages
	class InputPinMessage : public CInputPinWriteOnly<CTypeMIDIMessage, MidiOut> {
	public:
		InputPinMessage (MidiOut & component)
		: CInputPinWriteOnly<CTypeMIDIMessage, MidiOut>("message", component) {}

		virtual int DoSend(const CTypeMIDIMessage & msg) {
			m_component->ProcessMIDIMessage (msg);
			return 0;
		}
	};

	// Write pin which turns all notes off
	class InputPinAllOff : public CInputPinWriteOnly<CTypeAny, MidiOut> {
	public:
		InputPinAllOff (MidiOut & component)
		: CInputPinWriteOnly<CTypeAny, MidiOut>("all_off", component) {}

		virtual int DoSend(const CTypeAny &) {
			m_component->AllNotesOff ();
			return 0;
		}
	};
};

typedef ComponentFactory<MidiOut> MidiOutFactory;

/* ******************************************************************************
	midi types factories
****************************************************************************** */
typedef spcore::SimpleTypeFactory<CTypeMIDIMessage> CTypeMIDIMessageFactory;

/* ******************************************************************************
	midi  module
****************************************************************************** */
class MidiModule : public CModuleAdapter {
public:
	MidiModule() {
		//
		// types
		//
		RegisterTypeFactory(SmartPtr<spcore::ITypeFactory>(new CTypeMIDIMessageFactory(), false));

		//
		// components
		//
		RegisterComponentFactory(SmartPtr<spcore::IComponentFactory>(new MidiConfigFactory(), false));
		RegisterComponentFactory(SmartPtr<spcore::IComponentFactory>(new MidiConfigGuiFactory(), false));
		RegisterComponentFactory(SmartPtr<spcore::IComponentFactory>(new MidiOutFactory(), false));
	}
	virtual const char * GetName() const { return "mod_midi"; }
};

static MidiModule* g_module= NULL;
SPEXPORT_FUNCTION spcore::IModule* module_create_instance()
{
	if (g_module== NULL) g_module= new MidiModule();
	return g_module;
}

};
