/*=============================================================================
	CAMIDIEndpoints.cpp
	
	$Log: CAMIDIEndpoints.cpp,v $
	Revision 1.3  2003/12/04 19:56:14  dwyatt
	fixes:
	- in EndpointInfo copy constructor, null out mDisplayName
	- don't prepend the device name to the entity name if the entity name already starts with the device name (Edirol UM-4)
	
	Revision 1.2  2003/10/04 00:17:04  dwyatt
	minor tweak to endpoint naming
	
	Revision 1.1  2003/09/24 21:31:11  dwyatt
	initial checkin
	
	created Thu Sep 18 2003, Doug Wyatt
	Copyright (c) 2003 Apple Computer, Inc.  All Rights Reserved

	$NoKeywords: $
=============================================================================*/

#include "CAMIDIEndpoints.h"
#include <algorithm>

// ____________________________________________________________________________
// Obtain the name of an endpoint without regard for whether it has connections.
// The result should be released by the caller.
static CFStringRef EndpointName(MIDIEndpointRef endpoint, bool isExternal)
{
	CFMutableStringRef result = CFStringCreateMutable(NULL, 0);
	CFStringRef str;
	
	// begin with the endpoint's name
	str = NULL;
	MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &str);
	if (str != NULL) {
		CFStringAppend(result, str);
		CFRelease(str);
	}

	MIDIEntityRef entity = NULL;
	MIDIEndpointGetEntity(endpoint, &entity);
	if (entity == NULL)
		// probably virtual
		return result;
		
	if (CFStringGetLength(result) == 0) {
		// endpoint name has zero length -- try the entity
		str = NULL;
		MIDIObjectGetStringProperty(entity, kMIDIPropertyName, &str);
		if (str != NULL) {
			CFStringAppend(result, str);
			CFRelease(str);
		}
	}
	// now consider the device's name
	MIDIDeviceRef device = NULL;
	MIDIEntityGetDevice(entity, &device);
	if (device == NULL)
		return result;
	
	str = NULL;
	MIDIObjectGetStringProperty(device, kMIDIPropertyName, &str);
	if (str != NULL) {
		// if an external device has only one entity, throw away the endpoint name and just use the device name
		if (isExternal && MIDIDeviceGetNumberOfEntities(device) < 2) {
			CFRelease(result);
			return str;
		} else {
			// does the entity name already start with the device name? (some drivers do this though they shouldn't)
			// if so, do not prepend
			if (CFStringCompareWithOptions(str /* device name */, result /* endpoint name */, CFRangeMake(0, CFStringGetLength(str)), 0) != kCFCompareEqualTo) {
				// prepend the device name to the entity name
				if (CFStringGetLength(result) > 0)
					CFStringInsert(result, 0, CFSTR(" "));
				CFStringInsert(result, 0, str);
			}
			CFRelease(str);
		}
	}
	return result;
}

#if 0
// Obtain the name of an endpoint, following connections.
// The result should be released by the caller.
static CFStringRef ConnectedEndpointName(MIDIEndpointRef endpoint)
{
	CFMutableStringRef result = CFStringCreateMutable(NULL, 0);
	CFStringRef str;
	OSStatus err;
	
	// Does the endpoint have connections?
	CFDataRef connections = NULL;
	int nConnected = 0;
	bool anyStrings = false;
	err = MIDIObjectGetDataProperty(endpoint, kMIDIPropertyConnectionUniqueID, &connections);
	if (connections != NULL) {
		// It has connections, follow them
		// Concatenate the names of all connected devices
		nConnected = CFDataGetLength(connections) / sizeof(MIDIUniqueID);
		if (nConnected) {
			const SInt32 *pid = reinterpret_cast<const SInt32 *>(CFDataGetBytePtr(connections));
			for (int i = 0; i < nConnected; ++i, ++pid) {
				MIDIUniqueID id = EndianS32_BtoN(*pid);
				MIDIObjectRef connObject;
				MIDIObjectType connObjectType;
				err = MIDIObjectFindByUniqueID(id, &connObject, &connObjectType);
				if (err == noErr) {
					if (connObjectType == kMIDIObjectType_ExternalSource 
					|| connObjectType == kMIDIObjectType_ExternalDestination) {
						// Connected to an external device's endpoint (10.3 and later).
						str = EndpointName(static_cast<MIDIEndpointRef>(connObject), true);
					} else {
						// Connected to an external device (10.2) (or something else, catch-all)
						str = NULL;
						MIDIObjectGetStringProperty(connObject, kMIDIPropertyName, &str);
					}
					if (str != NULL) {
						if (anyStrings)
							CFStringAppend(result, CFSTR(", "));
						else anyStrings = true;
						CFStringAppend(result, str);
						CFRelease(str);
					}
				}
			}
		}
		CFRelease(connections);
	}
	if (anyStrings)
		return result;
	
	// Here, either the endpoint had no connections, or we failed to obtain names for any of them.
	return EndpointName(endpoint, false);
}
#endif

// ____________________________________________________________________________

CAMIDIEndpoints::Endpoint::Endpoint(MIDIEndpointRef endpoint, CFStringRef name, MIDIObjectRef connectedObj) :
	mUniqueID(kMIDIInvalidUniqueID), 
	mIOEndpoint(endpoint), 
	mName(name), 
	mEntity(NULL),
	mEmbeddedOrVirtual(false),
	mConnectedObj(connectedObj),
	mNext(NULL),
	mPairMate(NULL)
{
	MIDIObjectGetIntegerProperty(connectedObj ? connectedObj : endpoint, kMIDIPropertyUniqueID, &mUniqueID);

	// Is the endpoint that of an embedded entity? or virtual?
	MIDIEndpointGetEntity(endpoint, &mEntity);
	if (mEntity == NULL) {
		mEmbeddedOrVirtual = true;	// presumably virtual
	} else {
		SInt32 embedded = 0;
		MIDIObjectGetIntegerProperty(mEntity, kMIDIPropertyIsEmbeddedEntity, &embedded);
		if (embedded) {
			mEmbeddedOrVirtual = true;
		}
	}
}

CAMIDIEndpoints::Endpoint::~Endpoint()
{
	if (mName)
		CFRelease(mName);
}

bool	CAMIDIEndpoints::Endpoint::GetEndpointInfo(EMode mode, EndpointInfo &info)
{
	Endpoint *ept = this, *ept2;
	if (mode == kPairs) {
		if ((ept2 = ept->PairMate()) == NULL)
			return false;
		info.mSourceEndpoint = ept2->IOEndpoint();
		info.mDestinationEndpoint = ept->IOEndpoint();
	} else if (mode == kSources) {
		info.mSourceEndpoint = ept->IOEndpoint();
		info.mDestinationEndpoint = NULL;
	} else {
		info.mSourceEndpoint = NULL;
		info.mDestinationEndpoint = ept->IOEndpoint();
	}
	info.mUniqueID = ept->UniqueID();

	if (ept->DriverOwned() && ept->Next() != NULL) {
		// add one item for all connected items
		CFMutableStringRef names = CFStringCreateMutable(NULL, 0);
		bool first = true;
		while (true) {
			ept = ept->Next();
			if (ept == NULL)
				break;
			if (!first) {
				CFStringAppend(names, CFSTR(", "));
			} else first = false;
			CFStringAppend(names, ept->Name());
		}
		info.mDisplayName = names;
	} else {
		// a driver-owned endpoint with nothing connected externally,
		// or an external endpoint
		CFRetain(info.mDisplayName = ept->Name());
	}
	return true;
}

// ____________________________________________________________________________

CAMIDIEndpoints::CAMIDIEndpoints()
{
	UpdateFromCurrentState();
}

CAMIDIEndpoints::~CAMIDIEndpoints()
{
	Clear();
}

void	CAMIDIEndpoints::Clear()
{
	EndpointList::iterator epit;
	for (epit = mSources.begin(); epit != mSources.end(); ++epit) {
		delete *epit;
	}
	mSources.clear();
	for (epit = mDestinations.begin(); epit != mDestinations.end(); ++epit) {
		delete *epit;
	}
	mDestinations.clear();
}

void	CAMIDIEndpoints::UpdateFromCurrentState()
{
	Clear();

	UInt32 i, n;
	MIDIEndpointRef epRef;
	
	n = MIDIGetNumberOfSources();
	mSources.reserve(n);
	for (i = 0; i < n; ++i) {
		epRef = MIDIGetSource(i);
		if (epRef)
			AddEndpoints(epRef, mSources);
	}

	n = MIDIGetNumberOfDestinations();
	mDestinations.reserve(n);
	for (i = 0; i < n; ++i) {
		epRef = MIDIGetDestination(i);
		if (epRef)
			AddEndpoints(epRef, mDestinations);
	}
	
	// pairing
	for (EndpointList::iterator dit = mDestinations.begin(); dit != mDestinations.end(); ++dit) {
		Endpoint *ep = *dit;
		MIDIEntityRef destEntity = ep->Entity();
		if (destEntity != NULL) {
			for (EndpointList::iterator eit = mSources.begin(); eit != mSources.end(); ++eit) {
				Endpoint *ep2 = *eit;
				MIDIEntityRef srcEntity = ep2->Entity();
				if (srcEntity == destEntity && ep2->DriverOwned() == ep->DriverOwned()) {
					ep2->SetPairMate(ep);
					ep->SetPairMate(ep2);
				}
			}
		}
	}
}

void	CAMIDIEndpoints::AddEndpoints(MIDIEndpointRef endpoint, EndpointList &eplist)
{
	Endpoint *ep, *prev;
	OSStatus err;
	CFStringRef str;
	
	// Add the driver-owned endpoint
	ep = new Endpoint(endpoint, EndpointName(endpoint, false), NULL);
	eplist.push_back(ep);
	prev = ep;

	// Does the endpoint have connections?
	CFDataRef connections = NULL;
	int nConnected = 0;
	MIDIObjectGetDataProperty(endpoint, kMIDIPropertyConnectionUniqueID, &connections);
	if (connections != NULL) {
		// It has connections, follow them
		nConnected = CFDataGetLength(connections) / sizeof(MIDIUniqueID);
		if (nConnected) {
			const SInt32 *pid = reinterpret_cast<const SInt32 *>(CFDataGetBytePtr(connections));
			for (int i = 0; i < nConnected; ++i, ++pid) {
				MIDIUniqueID id = EndianS32_BtoN(*pid);
				MIDIObjectRef connObject;
				MIDIObjectType connObjectType;
				err = MIDIObjectFindByUniqueID(id, &connObject, &connObjectType);
				if (err == noErr) {
					if (connObjectType == kMIDIObjectType_ExternalSource 
					|| connObjectType == kMIDIObjectType_ExternalDestination) {
						// Connected to an external device's endpoint (10.3 and later).
						str = EndpointName(static_cast<MIDIEndpointRef>(connObject), true);
					} else {
						// Connected to an external device (10.2) (or something else, catch-all)
						str = NULL;
						MIDIObjectGetStringProperty(connObject, kMIDIPropertyName, &str);
					}
					if (str != NULL) {
						ep = new Endpoint(endpoint, str, connObject);
						eplist.push_back(ep);
						prev->SetNext(ep);
						prev = ep;
					}
				}
			}
		}
	}
}


class CompareEndpointsByName {
public:
	bool operator () (CAMIDIEndpoints::EndpointInfo *a, CAMIDIEndpoints::EndpointInfo *b)
	{
		CFStringRef namea = a->mDisplayName;
		return CFStringCompareWithOptions(namea, b->mDisplayName, CFRangeMake(0, CFStringGetLength(namea)), 0) < 0;
	}
};

CAMIDIEndpoints::EndpointInfoList *	CAMIDIEndpoints::GetEndpoints(EMode mode, UInt32 opts)
{
	EndpointList &srcList = (mode == kSources) ? mSources : mDestinations;
	EndpointInfoList *list = new EndpointInfoList;
	EndpointInfo info;
	
	for (EndpointList::iterator it = srcList.begin(); it != srcList.end(); ++it) {
		Endpoint *ept = *it;
		
		if (ept->DriverOwned()) {
			// driver-owned endpoint
			if (ept->Next() == NULL) {
				// nothing connected externally
				if ((opts & kOptIncludeUnconnectedExternalPorts) || ept->EmbeddedOrVirtual()) {
					if (ept->GetEndpointInfo(mode, info))
						list->push_back(new EndpointInfo(info));
				}
			} else if (opts & kOptCombineByPort) {
				// add one item for all connected items
				if (ept->GetEndpointInfo(mode, info))
					list->push_back(new EndpointInfo(info));
			}
			// else it has external connections, which we'll pick up separately
		} else {
			// external endpoint
			if (!(opts & kOptCombineByPort)) {
				if (ept->GetEndpointInfo(mode, info))
					list->push_back(new EndpointInfo(info));
			}
		}
	}
	
	if (opts & kOptSortByName) {
		std::sort(list->begin(), list->end(), CompareEndpointsByName() );
	}
	return list;
}

bool	CAMIDIEndpoints::FindEndpoint(EMode mode, EndpointInfo &info, UInt32 opts)
{
	EndpointList &srcList = (mode == kSources) ? mSources : mDestinations;
	
	for (EndpointList::iterator it = srcList.begin(); it != srcList.end(); ++it) {
		Endpoint *ept = *it;
		
		if (ept->UniqueID() == info.mUniqueID) {
			if (ept->GetEndpointInfo(mode, info))
				return true;
			break;
		}
	}
	info.mSourceEndpoint = NULL;
	info.mDestinationEndpoint = NULL;
	return false;
}


/*
	Resolving of persistent endpoint references
	
	Cases to handle:
	- Was referring to an external endpoint
		- missing
	- Was referring to a driver endpoint
		- missing
		- now has external endpoint(s) connected
*/
