/*

kibibu PeerTune
for tuning up the Green Milk and other tunable synths

Thanks to BTD for providing the source to his peer control stuff
(which this doesn't use directly, but the code was very helpful!)

20070314 - Created

*/

#include "../mdk/mdk.h"

#include "../mdk/MachineInterface.h"

// use BTD's hack code
// for finding CMachineInterfaces from CMachine *
#include "hack.h"

#include <stdlib.h>
#include <math.h>
#include <vector>

#pragma optimize ("awy", on)

#define PARA_NONE	0xFFFF

#define ROOT_MIN	0
#define ROOT_MAX	11
#define ROOT_NO		0xFF

#define MAX_MACHINES 512

#define max(x, y) ((x)>(y)?(x):(y))
#define min(x, y) ((x)>(y)?(y):(x))
#define clip(x, mx, mn) max(min((x),(mx)),(mn))

// Threshold under which the signal must be to cut off the signal
CMachineParameter const paraRootNote =
{ pt_byte, "Root Note", "Root Note", ROOT_MIN, ROOT_MAX, ROOT_NO, MPF_STATE, ROOT_MIN };

#define PITCH_MIN	0

// 2 octaves at 1/10 cent resolution
#define PITCH_MAX	24000	

#define PITCH_ZERO	12000

#define PITCH_NO	0xFFFF

// Threshold under which the signal must be to cut off the signal
CMachineParameter const paraPitch =
{  pt_word,"Pitch","Pitch",PITCH_MIN,PITCH_MAX,PITCH_NO,MPF_STATE,PITCH_ZERO };

CMachineParameter const paraDetune = 
{ pt_word, "Detune", "Detune", 0, 2000, 0xFF, MPF_STATE, 1000};

CMachineParameter const *pParameters[] = 
{ 
	// global
	&paraRootNote,
		// 12 copies of the pitch param
	&paraPitch,	&paraPitch,	&paraPitch,	&paraPitch,
	&paraPitch,	&paraPitch,	&paraPitch,	&paraPitch,
	&paraPitch,	&paraPitch,	&paraPitch,	&paraPitch,
	&paraDetune
};

CMachineAttribute const *pAttributes[] = { NULL };

#pragma pack(1)		

class gvals
{
public:
	byte rootNote;
	word pitch[12];
	word detune;
};

#pragma pack()

CMachineInfo const MacInfo = 
{
	MT_GENERATOR,								// type
	MI_VERSION,	
	MIF_CONTROL_MACHINE | MIF_NO_OUTPUT,	// flags
	0,										// min tracks
	0,										// max tracks
	14,										// numGlobalParameters
	0,										// numTrackParameters
	pParameters,
	0,
	pAttributes,
#ifdef _DEBUG
	"kibibu PeerTune (Debug build)",		// name
#else
	"kibibu PeerTune",						// name
#endif
	"PeerTune",								// short name
	"Cameron Foale (kibibu)",				// author
	"/Apply to"
};

class mi;

const char * describeNote(unsigned int note)
{
	static const char * const noteNames[12] =
		{"C ","C#","D ","D#",
		"E ","F ","F#","G ",
		"G#","A ","A#","B "};	// these are 2 chars so we can strncmp with attr names

	return (noteNames[note % 12]);
}

class miex : public CMDKMachineInterfaceEx {
public:
	virtual char const *DescribeParam(int const param); 

	virtual void GetSubMenu(int const i, CMachineDataOutput *pout);

	mi * pMI;

};

class CControlledMachine
{
public:
	int attributeNumbers[12];
	CMachine * pMach;
};

class CMenuItem
{
public:
	std::string menuName;
	CMachine * pMach;
	bool bControlled;
};

class CMachineFilter;

class mi : public CMDKMachineInterface
{
public:
	friend miex;
	friend CMachineFilter;

	mi();
	virtual ~mi();

	virtual void Tick();

	virtual void MDKInit(CMachineDataInput * const pi);
	virtual void MDKSave(CMachineDataOutput * const po);

	virtual bool MDKWork(float *psamples, int numsamples, int const mode);
	virtual bool MDKWorkStereo(float *psamples, int numsamples, int const mode);
	virtual void OutputModeChanged(bool stereo);

	virtual char const *DescribeValue(int const param, int const value);
	virtual void Command(int const i);
	virtual CMDKMachineInterfaceEx * mi::GetEx();

	unsigned int getRootNote() { return rootNote; }

	void updateAttributes();

	bool checkHasAttributes(const CMachineInfo * pMachInfo);
	void getAttributeIndices(const CMachineInfo * pMachInfo, int * pOut);

	bool controlledMachine(CMachine * pMach);

	void checkMachines();

	void iterateNames(CMachineDataOutput * pout);

public:
	miex ex;

	typedef std::vector<CControlledMachine> MachineList;
	MachineList controlledMachines;

	typedef std::vector<std::string> StringList;
	StringList machinesToControl;

	// this is built in the GetSubMenu method
	typedef std::vector<CMenuItem> MenuItemList;
	MenuItemList menuItems;

private:
	unsigned int rootNote;
	unsigned int pitches[12];
	int detune;
	bool changed;

	bool initialising;


	gvals gval;

};

char const * miex::DescribeParam(int const param) { 
	static char desc[256];

	if(param == 0) return NULL;
	if(param == 13) return NULL;

	sprintf(desc,"Note %d", param -1);
	return desc;
	
}

void mi::iterateNames(CMachineDataOutput * pout)
{
	for(mi::MachineList::iterator iter = controlledMachines.begin();
		iter != controlledMachines.end();
		++iter)
	{
		pout->Write(pCB->GetMachineName(iter->pMach));
	}
}



bool mi::controlledMachine(CMachine * pMach)
{
	for(mi::MachineList::iterator iter = controlledMachines.begin();
		iter != controlledMachines.end();
		++iter)
	{
		if(iter->pMach == pMach) return true;
	}

	// not found
	return false;

}

// pOut must be an array of size 12
void mi::getAttributeIndices(const CMachineInfo * pMachInfo, int * pOut)
{
	memset(pOut, 0, 12 * sizeof(int));

	// for each note
	for(int i = 0; i < 12; ++i)
	{
		// for each attribute
		// check
		for(int j = 0; j < pMachInfo->numAttributes; ++j)
		{
			// if we can't find one, return false
			if(strncmp(pMachInfo->Attributes[j]->Name, describeNote(i), 2) == 0)
			{
				pOut[i] = j;
			}

		}
	}
}

bool mi::checkHasAttributes(const CMachineInfo * pMachInfo)
{
	bool found;

	// for each note
	for(int i = 0; i < 12; ++i)
	{
		// for each attribute
		// check
		found = false;
		for(int j = 0; j < pMachInfo->numAttributes; ++j)
		{
			// if we can't find one, return false
			if(strncmp(pMachInfo->Attributes[j]->Name, describeNote(i), 2) == 0)
			{
				found = true;
				break;
			}

		}

		if(!found) return false;
	}
	return true;
}

class CMachineCleaner : public CMachineDataOutput
{
public:
	mi * pMI;
	int index;
	static CMachine * allMachines[MAX_MACHINES];

	CMachineCleaner() { index = 0; }
	virtual void Write(void *pbuf, const int numbytes);

	void clean();
};

CMachine * CMachineCleaner::allMachines[MAX_MACHINES];


void CMachineCleaner::Write(void * pbuf, const int numbytes)
{
	if(index < MAX_MACHINES)
	{
		allMachines[index++] = pMI->pCB->GetMachine((char *)pbuf);
	}
}

void mi::checkMachines()
{
	CMachineCleaner theCleaner;
	theCleaner.pMI = this;
	theCleaner.clean();
}

void CMachineCleaner::clean()
{
	index = 0;

	pMI->pCB->GetMachineNames(this);
	// then go through all the machines in our list and remove em if they're not there
	for(mi::MachineList::iterator iter = pMI->controlledMachines.begin();
		iter != pMI->controlledMachines.end(); 
		++iter)
	{
		// loop till we find it
		bool found = false;
		for(int j = 0; j < MAX_MACHINES && !found; ++j)
		{
			if(allMachines[j] == iter->pMach)
			{
				found = true;
			}
		}

		if(!found)
		{
			// wasn't found remove it
			pMI->controlledMachines.erase(iter);
		}
	}
}

class CMachineFilter : public CMachineDataOutput
{
public:	
	virtual void Write(void *pbuf, const int numbytes);

	CMachineDataOutput * pout;
	mi * pMI;

	CMachineFilter() {};
	
};


void CMachineFilter::Write(void *pbuf, const int numbytes)
{
	static char buff[256];

	char * pcpy = buff;
	
	CMenuItem item;
		
	// grab the machine first
	CMachine * pMach = pMI->pCB->GetMachine((const char *)pbuf);
	const CMachineInfo * pMachInfo = pMI->pCB->GetMachineInfo(pMach);

	// if there's not enough attributes to have all the offsets
	// cheap check
	if(pMachInfo->numAttributes < 12) return;

	// check whether we already control it
	if(pMI->controlledMachine(pMach))
	{
		*pcpy++ = '*';	// add a tick box
		*pcpy = 0;
			
		item.bControlled = true;
	} else {
		// not controlled, do the expensive check
		if(!pMI->checkHasAttributes(pMachInfo)) return;
		item.bControlled = false;
	}

	strncpy(pcpy, (char *)pbuf, 255);

	// ok, got one
	item.menuName = buff;
	item.pMach = pMach;

	pMI->menuItems.push_back(item);

	// otherwise, its ok
	pout->Write(buff);

}

void miex::GetSubMenu(int const i, CMachineDataOutput *pout) {
	CMachineFilter mf;
	mf.pout = pout;
	mf.pMI = pMI;

	pMI->menuItems.clear();
	
	pMI->pCB->GetMachineNames(&mf);
}

mi::mi() {  GlobalVals = &gval; ex.pMI = this; rootNote = 0; }
mi::~mi() { }

void mi::OutputModeChanged(bool stereo)
{
}

CMDKMachineInterfaceEx * mi::GetEx()
{
	return &ex;
}

void mi::MDKInit(CMachineDataInput * const pi) {

	initialising = true;
	changed = false;

	// set up the hack lib
	FindMIOffset(pCB->GetThisMachine(), this);

	rootNote = 0;
	
	char macName[256];	// for reading in names

	// empty the controlled machines vector
	controlledMachines.clear();
	machinesToControl.clear();

	if(pi)
	{
		// read a version
		byte version;
		pi->Read(version);

		// get a count of the controlled machines
		int mac_count;
		pi->Read(mac_count);

		// 
		if(mac_count > 0)
		{
			for(int i = 0; i < mac_count; ++i)
			{
				int str_len;
				pi->Read(str_len);

				memset(macName,0,256);
				pi->Read((void *)macName, min(str_len+1, 255));
				machinesToControl.push_back(macName);
			}
		}
	}
}

void mi::MDKSave(CMachineDataOutput * const po)
{
	// write version number
	po->Write((byte)1);

	// write number of machines
	po->Write((int)controlledMachines.size());

	// then write, for each one, just the machine name
	// length of the name first
	for(MachineList::const_iterator i = controlledMachines.begin();
		i!=controlledMachines.end(); ++i)
	{
		const char * pName = pCB->GetMachineName(i->pMach);
		int len = (int)strlen(pName);
		po->Write((int)len);
		po->Write(pName);
	}
}

void mi::updateAttributes()
{
	if(!initialising)
	{
		checkMachines();
		// go through all the machines and set their tuning
		for(mi::MachineList::iterator iter = controlledMachines.begin();
			iter != controlledMachines.end();
			iter++)
		{
			CMachineInterface * pControlledMI = GetMI(iter->pMach);
			if(!pControlledMI) continue;

			// ok, got an MI, set the attributes!
			// for each attr
			for(int i = 0; i < 12; i++)
			{
				pControlledMI->AttrVals[iter->attributeNumbers[i]] =
					pitches[(12 + i - this->rootNote) % 12] + detune;
			}
			pControlledMI->AttributesChanged();
		}
		changed = false;
	}
}

#define TICK_UPDATE(x,y) {if(x!=y){x=y;changed=true;}}

void mi::Tick()
{
	// grab the params
	if(gval.rootNote != paraRootNote.NoValue)
	{
		TICK_UPDATE(rootNote,gval.rootNote);
	}

	// check pitches
	for(int i = 0; i < 12; ++i)
	{
		if(gval.pitch[i] != paraPitch.NoValue)
		{
			TICK_UPDATE(pitches[i], gval.pitch[i]);
		}
	}

	if(gval.detune != paraRootNote.NoValue)
	{
		TICK_UPDATE(detune, gval.detune - 1000);
	}

	if(changed)
	{
		updateAttributes();
	}

}

void mi::Command(int const i) {
	// low byte is the index
	int index = (i & 0xFF);
	int whichMenu = i >> 8;

	// ok, here's where we handle the add/remove
	CMenuItem mi = menuItems[index];

	if(mi.bControlled)
	{
		// stop tuning
		// find the pMach in our controlled machines
		for(mi::MachineList::iterator iter = controlledMachines.begin();
			iter != controlledMachines.end(); ++iter)
		{
			if(iter->pMach == mi.pMach)
			{
				controlledMachines.erase(iter);
				break;
			}
		}
	} else {
		// start tuning
		CControlledMachine mach;
		mach.pMach = mi.pMach;

		// get the attribute numbers
		this->getAttributeIndices(pCB->GetMachineInfo(mi.pMach), mach.attributeNumbers);

		this->controlledMachines.push_back(mach);

		// and apply to all the machines
		updateAttributes();
	}

}

bool mi::MDKWork(float * psamples, int numsamples, const int mode)
{
	// check if not initialised
	if(initialising)
	{
		// check if there are any machines to load up
		if(!this->machinesToControl.empty())
		{
			for(mi::StringList::iterator iter = machinesToControl.begin();
				iter != machinesToControl.end();
				++iter )
			{

				// got some machines to hook up to
				CControlledMachine mach;

				mach.pMach = pCB->GetMachine(iter->c_str());
				if(!mach.pMach) continue;

				// hook up the attributes
				this->getAttributeIndices(pCB->GetMachineInfo(mach.pMach), mach.attributeNumbers);

				this->controlledMachines.push_back(mach);
			}

		}

		initialising = false;
	}
	
	return false;
}

bool mi::MDKWorkStereo(float * psamples, int numsamples, const int mode)
{
	initialising = false;
	return false;
}

#define MAX_DESCRIPTION 64

// describe a value
const char * mi::DescribeValue(const int param, const int value)
{	
	static char description[MAX_DESCRIPTION];

	if(param == 0) return describeNote(value);
	if(param == 13)
	{
		sprintf(description, "%.1f", float(value -999.999999) * 0.1);	// just to avoid -0
		return description;
	}
	
	// else
	int val = value - PITCH_ZERO;
	if(val < 0)
	{
		val = -val;
		sprintf(description, "-%d.%d",val / 10, val % 10);
	} else {
		sprintf(description, "%d.%d",val / 10, val % 10);
	}
	return description;

}

DLL_EXPORTS;