/*
 * 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 <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 <dae/daeDocument.h>
#include <dae/daeDatabase.h>
#include <dae/daeUtils.h>
#include <dae/daeDom.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(*container->getDAE())) == 0)
                continue;
            if (strcmp(elt->getElementName(), COLLADA_ELEMENT_TECHNIQUE(*container->getDAE())) == 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(*container->getDAE())) == 0)
                return UINT_MAX;
            if (strcmp(elt->getElementName(), COLLADA_ELEMENT_TECHNIQUE(*container->getDAE())) == 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();

    // need to follow instance urls since they are just copies of code in the url.
    if ( strncmp( container->getElementName(), "instance_", 9 ) == 0 ) {
        daeURI *uri = (daeURI*)container->getAttributeValue("url");
        if ( !!uri && !!uri->getElement() ) {
            // found the url, now resolve the left over part
            daeElement *e = findWithDots( uri->getElement(), s, profile, finder, remainingPart );
            if ( !!e ) {
                return e;
            }
        }
    }

    // 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() == getDomSourceID(*result.elt->getDAE())) {
        result.array = getDomSourceFloatArray(result.elt);
    }
    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();

    if( !!result.elt && !result.array && !result.scalar ) {
        // <newparam> can have a SIDREF specification, so if the current sid resolves to this newparam, then start a new search
        // for the <newparam>'s SIDREF
        if( strcmp(result.elt->getElementName(),"newparam") == 0) {
            daeElement* psidref = result.elt->getChild("SIDREF");
            if( !!psidref ) {
                daeSidRef::resolveData newresult;
                daeSidRef newsidref(psidref->getCharData(),result.elt->getParent(),sidRef.profile);
                newresult = result.elt->getDAE()->getSidRefCache().lookup(newsidref);
                if (!!newresult.elt) {
                    return newresult;
                }
                // try resolving as an animation-style sid ref, where the first part is an ID.
                newresult = resolveImpl(newsidref);
                if( !newresult.elt ) {
                    // this is actually not part of the standard, it is only a hack
                    newresult = resolveImpl(daeSidRef(string("./") + psidref->getCharData(),result.elt->getParent(),sidRef.profile));
                    if( !!newresult.elt ) {
                        fprintf(stderr,"SID '%s' that needs  './' prefixed to it to resolve correctly\n",psidref->getCharData().c_str());
                    }
                }
                if( !!newresult.elt ) {
                    return newresult;
                }
            }
        }

    }

    // 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)
{
    lookupTable = new std::map<daeSidRef, daeSidRef::resolveData>();
}

daeSidRefCache::~daeSidRefCache()
{
    delete lookupTable;
}

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;
}
