/*
* Copyright 2006 Sony Computer Entertainment Inc.
*
* Licensed under the MIT Open Source License, for details please see license.txt or the website
* http://www.opensource.org/licenses/mit-license.php
*
*/ 
#include <dae.h>
#include <list>
#include <vector>
#include <sstream>
#include <algorithm>
#include <dae/daeSIDResolver.h>
#include <dae/daeIDRef.h>
#include <dae/daeAtomicType.h>
#include <dae/daeMetaAttribute.h>
#include <dae/daeMetaElement.h>
#include <dae/daeURI.h>
#include <dom/domTypes.h>
#include <dom/domConstants.h>
#include <dae/daeDocument.h>
#include <dae/daeDatabase.h>
#include <dae/daeUtils.h>
#include <dom/domSource.h>

using namespace std;


namespace {
	template<typename T>
	T nextIter(const T& iter) {
		T next = iter;
		return ++next;
	}

	template<typename T>
	T moveIter(const T& iter, int n) {
		T result = iter;
		advance(result, n);
		return result;
	}
	
	// Implements a breadth-first sid search by starting at the container element and
	// traversing downward through the element tree.
	daeElement* findSidTopDown(daeElement* container, const string& sid, const string& profile) {
		if (!container)
			return NULL;

		vector<daeElement*> elts, matchingElts;
		elts.push_back(container);

		for (size_t i = 0; i < elts.size(); i++) {
			daeElement* elt = elts[i];
			
			// Bail if we're looking for an element in a different profile
			if (!profile.empty()) {
				if (strcmp(elt->getElementName(), COLLADA_ELEMENT_TECHNIQUE_COMMON) == 0)
					continue;
				if (strcmp(elt->getElementName(), COLLADA_ELEMENT_TECHNIQUE) == 0  &&
				    profile != elt->getAttribute("profile"))
					continue;
			}
		
			// See if this is a matching element
			if (elt->getAttribute("sid") == sid)
				return elt;
			else {
				// Add the children to the list of elements to check
				daeElementRefArray children;
				elt->getChildren(children);
				for (size_t j = 0; j < children.getCount(); j++)
					elts.push_back(children[j]);
			}
		}

		return NULL;
	}

	// Returns the distance between an element and an ancestor of the element. If 'container
	// isn't an ancestor of 'elt', or if 'elt' is in a profile that doesn't match 'profile'
	// UINT_MAX is returned.
	unsigned int computeDistance(daeElement* container, daeElement* elt, const string& profile) {
		if (!container || !elt)
			return UINT_MAX;

		unsigned int distance = 0;
		do {
			// Bail if we're looking for an element in a different profile
			if (!profile.empty()) {
				if (strcmp(elt->getElementName(), COLLADA_ELEMENT_TECHNIQUE_COMMON) == 0)
					return UINT_MAX;
				if (strcmp(elt->getElementName(), COLLADA_ELEMENT_TECHNIQUE) == 0  &&
				    profile != elt->getAttribute("profile"))
					return UINT_MAX;
			}

			if (elt == container)
				return distance;
			distance++;
		} while ((elt = elt->getParentElement()) != NULL);

		return UINT_MAX;
	}

	// Implements a breadth-first sid search by using the database to find all elements
	// matching 'sid', then finding the element closest to 'container'.
	daeElement* findSidBottomUp(daeElement* container, const string& sid, const string& profile) {
		if (!container || !container->getDocument())
			return NULL;

		// Get the elements with a matching sid
		vector<daeElement*> elts;
		container->getDocument()->getDAE()->getDatabase()->sidLookup(sid, elts, container->getDocument());

		// Compute the distance from each matching element to the container element
		unsigned int minDistance = UINT_MAX;
		daeElement* closestElt = NULL;
		for (size_t i = 0; i < elts.size(); i++) {
			unsigned int distance = computeDistance(container, elts[i], profile);
			if (distance < minDistance) {
				minDistance = distance;
				closestElt = elts[i];
			}
		}

		return closestElt;
	}

	daeElement* findID(daeElement* elt, const string& id, const string& profile) {
		return elt ? elt->getDAE()->getDatabase()->idLookup(id, elt->getDocument()) : NULL;
	}
	
	void buildString(const list<string>::iterator& begin,
	                 const list<string>::iterator& end,
	                 string& result) {
		ostringstream stream;
		for (list<string>::iterator iter = begin; iter != end; iter++)
			stream << *iter;
		result = stream.str();
	}

	// Finds an element with a matching ID or sid (depending on the 'finder' function)
	// passed in. First it tries to resolve the whole ID/sid, then it tries to resolve
	// successively smaller parts. For example, consider this sid ref: "my.sid.ref".
	// First this function will try to resolve "my.sid.ref" entirely, then if that
	// fails it'll try to resolve "my.sid.", "my.sid", "my.", and "my", in that order.
	// The part that wasn't matched is returned in the 'remainingPart' parameter.
	daeElement* findWithDots(daeElement* container,
	                         const string& s,
	                         const string& profile,
	                         daeElement* (*finder)(daeElement*, const string&, const string&),
	                         list<string>& remainingPart) {
		remainingPart.clear();

		// First see if the whole thing resolves correctly
		if (daeElement* result = finder(container, s, profile))
			return result;

		// It didn't resolve. Let's tokenize it by '.'s and see if we can resolve a
		// portion of it.
		cdom::tokenize(s, ".", remainingPart, true);
		if (remainingPart.size() == 1)
			return NULL; // There were no '.'s, so the result won't be any different

		list<string>::iterator iter = moveIter(remainingPart.end(), -1);
		for (int i = int(remainingPart.size())-1; i >= 1; i--, iter--) {
			string substr;
			buildString(remainingPart.begin(), iter, substr);
			if (daeElement* result = finder(container, substr, profile)) {
				// Remove the part we matched against from the list
				remainingPart.erase(remainingPart.begin(), iter);
				return result;
			}
		}

		remainingPart.clear();
		return NULL;
	}

	daeSidRef::resolveData resolveImpl(const daeSidRef& sidRef) {
		if (sidRef.sidRef.empty() || !sidRef.refElt)
			return daeSidRef::resolveData();

		daeSidRef::resolveData result;
		string separators = "/()";
		list<string> tokens;
		cdom::tokenize(sidRef.sidRef, separators, /* out */ tokens, true);

		list<string>::iterator tok = tokens.begin();

		// The first token should be either an ID or a '.' to indicate
		// that we should start the search from the container element.
		if (tok == tokens.end())
			return daeSidRef::resolveData();

		list<string> remainingPart;
		if (*tok == ".") {
			result.elt = sidRef.refElt;
			tok++;
		}	else {
			// Try to resolve it as an ID
			result.elt = findWithDots(sidRef.refElt, *tok, sidRef.profile, findID, remainingPart);
			if (result.elt) {
				if (!remainingPart.empty()) {
					// Insert the "remaining part" from the ID resolve into our list of tokens
					tokens.erase(tokens.begin());
					tokens.splice(tokens.begin(), remainingPart);
					tok = tokens.begin();
				} else
					tok++;
			}
		}

		if (!result.elt)
			return daeSidRef::resolveData();

		// Next we have an optional list of SIDs, each one separated by "/". Once we hit one of "()",
		// we know we're done with the SID section.
		for (; tok != tokens.end() && *tok == "/"; tok++) {
			tok++; // Read the '/'
			if (tok == tokens.end())
				return daeSidRef::resolveData();

			// Find the element matching the SID
			result.elt = findWithDots(result.elt, *tok, sidRef.profile, findSidTopDown, remainingPart);
			if (!result.elt)
				return daeSidRef::resolveData();

			if (!remainingPart.empty()) {
				list<string>::iterator tmp = tok;
				tok--;
				tokens.splice(tmp, remainingPart);
				tokens.erase(tmp);
			}
		}

		// Now we want to parse the member selection tokens. It can either be
		//   (a) '.' followed by a string representing the member to access
		//   (b) '(x)' where x is a number, optionally followed by another '(x)'
		// Anything else is an error.
		string member;
		bool haveArrayIndex1 = false, haveArrayIndex2 = false;
		int arrayIndex1 = -1, arrayIndex2 = -1;
		if (tok != tokens.end()) {
			if (*tok == ".") {
				tok++;
				if (tok == tokens.end())
					return daeSidRef::resolveData();
				member = *tok;
				tok++;
			}
			else if (*tok == "(") {
				tok++;
				if (tok == tokens.end())
					return daeSidRef::resolveData();

				istringstream stream(*tok);
				stream >> arrayIndex1;
				haveArrayIndex1 = true;
				if (!stream.good() && !stream.eof())
					return daeSidRef::resolveData();
				tok++;
				if (tok == tokens.end()  ||  *tok != ")")
					return daeSidRef::resolveData();
				tok++;
			
				if (tok != tokens.end()  &&  *tok == "(") {
					tok++;
					if (tok == tokens.end())
						return daeSidRef::resolveData();

					stream.clear();
					stream.str(*tok);
					stream >> arrayIndex2;
					haveArrayIndex2 = true;
					if (!stream.good() && !stream.eof())
						return daeSidRef::resolveData();
					tok++;
					if (tok == tokens.end()  ||  *tok != ")")
						return daeSidRef::resolveData();
					tok++;
				}
			}
		}

		// We shouldn't have any tokens left. If we do it's an error.
		if (tok != tokens.end())
			return daeSidRef::resolveData();

		// At this point we've parsed a correctly formatted SID reference. The only thing left is to resolve
		// the member selection portion of the SID ref. First, see if the resolved element has a float array we
		// can use.
		if (result.elt->typeID() == domSource::ID()) {
			if (domFloat_array* floatArray = ((domSource*)result.elt)->getFloat_array())
				result.array = (daeDoubleArray*)floatArray->getCharDataObject()->get(floatArray);
		}
		else 
		{
			daeMetaAttribute *ma = result.elt->getCharDataObject();
			if ( ma != NULL ) {
				if ( ma->isArrayAttribute() && ma->getType()->getTypeEnum() == daeAtomicType::DoubleType ) {
					result.array = (daeDoubleArray*)ma->get( result.elt );
				}
			}
		}

		if( result.array ) {
			// We have an array to use for indexing. Let's see if the SID ref uses member selection.
			if (!member.empty()) {
				// Do member lookup based on the constants defined in the COMMON profile
				if (member == "ANGLE") {
					result.scalar = &(result.array->get(3));
				}	else if (member.length() == 1) {
					switch(member[0]) {
					case 'X':
					case 'R':
					case 'U':
					case 'S':
						result.scalar = &(result.array->get(0));
						break;
					case 'Y':
					case 'G':
					case 'V':
					case 'T':
						result.scalar = &(result.array->get(1));
						break;
					case 'Z':
					case 'B':
					case 'P':
						result.scalar = &(result.array->get(2));
						break;
					case 'W':
					case 'A':
					case 'Q':
						result.scalar = &(result.array->get(3));
						break;
					};
				}
			} else if (haveArrayIndex1) {
				// Use the indices to lookup a value in the array
				if (haveArrayIndex2  &&  result.array->getCount() == 16) {
					// We're doing a matrix lookup. Make sure the index is valid.
					int i = arrayIndex1*4 + arrayIndex2;
					if (i >= 0  &&  i < int(result.array->getCount()))
						result.scalar = &(result.array->get(i));
				} else {
					// Vector lookup. Make sure the index is valid.
					if (arrayIndex1 >= 0  &&  arrayIndex1 < int(result.array->getCount()))
						result.scalar = &(result.array->get(arrayIndex1));
				}
			}
		}

		// If we tried to do member selection but we couldn't resolve it to a doublePtr, fail.
		if ((!member.empty() || haveArrayIndex1)  &&  result.scalar == NULL)
			return daeSidRef::resolveData();

		// SID resolution was successful.
		return result;
	}
} // namespace {


daeSidRef::resolveData::resolveData() : elt(NULL), array(NULL), scalar(NULL) { }

daeSidRef::resolveData::resolveData(daeElement* elt, daeDoubleArray* array, daeDouble* scalar)
  : elt(elt),
    array(array),
    scalar(scalar) { }


daeSidRef::daeSidRef() : refElt(NULL) { }

daeSidRef::daeSidRef(const string& sidRef, daeElement* referenceElt, const string& profile)
  : sidRef(sidRef),
    refElt(referenceElt),
    profile(profile) { }

bool daeSidRef::operator<(const daeSidRef& other) const {
	if (refElt != other.refElt)
		return refElt < other.refElt;
	if (sidRef != other.sidRef)
		return sidRef < other.sidRef;
	return profile < other.profile;
}

daeSidRef::resolveData daeSidRef::resolve() {
	if (!refElt)
		return daeSidRef::resolveData();
	
	// First check the cache
	daeSidRef::resolveData result = refElt->getDAE()->getSidRefCache().lookup(*this);
	if (result.elt)
		return result;

	// Try to resolve as an effect-style sid ref by prepending "./" to the sid ref.
	// If that fails, try resolving as an animation-style sid ref, where the first part is an ID.
	result = resolveImpl(daeSidRef(string("./") + sidRef, refElt, profile));
	if (!result.elt)
		result = resolveImpl(*this);

	if (result.elt) // Add the result to the cache
		refElt->getDAE()->getSidRefCache().add(*this, result);

	return result;
}


daeSIDResolver::daeSIDResolver( daeElement *container, daeString target, daeString profile )
	: container(NULL)
{
	setContainer(container);
	setTarget(target);
	setProfile(profile);
}

daeString daeSIDResolver::getTarget() const {
	return target.empty() ? NULL : target.c_str();
}

void daeSIDResolver::setTarget( daeString t )
{
	target = t ? t : "";
}

daeString daeSIDResolver::getProfile() const {
	return profile.empty() ? NULL : profile.c_str();
}

void daeSIDResolver::setProfile( daeString p )
{
	profile = p ? p : "";
}

daeElement* daeSIDResolver::getContainer() const {
	return container;
}

void daeSIDResolver::setContainer(daeElement* element)
{
	container = element;
}

daeSIDResolver::ResolveState daeSIDResolver::getState() const {
	if (target.empty())
		return target_empty;

	daeSidRef::resolveData result = daeSidRef(target, container, profile).resolve();
	if (!result.elt)
		return sid_failed_not_found;
	if (result.scalar)
		return sid_success_double;
	if (result.array)
		return sid_success_array;

	return sid_success_element;
}

daeElement* daeSIDResolver::getElement()
{
	return daeSidRef(target, container, profile).resolve().elt;
}

daeDoubleArray *daeSIDResolver::getDoubleArray()
{
	return daeSidRef(target, container, profile).resolve().array;
}

daeDouble *daeSIDResolver::getDouble()
{
	return daeSidRef(target, container, profile).resolve().scalar;
}


daeSidRefCache::daeSidRefCache() : hitCount(0), missCount(0) { }

daeSidRef::resolveData daeSidRefCache::lookup(const daeSidRef& sidRef) {
	map<daeSidRef, daeSidRef::resolveData>::iterator iter = lookupTable.find(sidRef);
	if (iter != lookupTable.end()) {
		hitCount++;
		return iter->second;
	}
	missCount++;
	return daeSidRef::resolveData();
}

void daeSidRefCache::add(const daeSidRef& sidRef, const daeSidRef::resolveData& result) {
	lookupTable[sidRef] = result;
}

void daeSidRefCache::clear() {
	lookupTable.clear();
	hitCount = missCount = 0;
}

bool daeSidRefCache::empty() {
	return lookupTable.empty();
}

int daeSidRefCache::misses() {
	return missCount;
}

int daeSidRefCache::hits() {
	return hitCount;
}
