/** 
 * @file llaisapi.cpp
 * @brief classes and functions for interfacing with the v3+ ais inventory service. 
 *
 * $LicenseInfo:firstyear=2013&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2013, Linden Research, Inc.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 *
 */

#include "llviewerprecompiledheaders.h"
#include "llaisapi.h"

#include "llagent.h"
#include "llappviewer.h"
#include "llcallbacklist.h"
#include "llinventorymodel.h"
#include "llinventoryobserver.h"
#include "llnotificationsutil.h"
#include "llsdutil.h"
#include "llviewerregion.h"
#include "llvoavatar.h"
#include "llvoavatarself.h"
#include "llviewercontrol.h"

///----------------------------------------------------------------------------
/// Classes for AISv3 support.
///----------------------------------------------------------------------------

//=========================================================================
const std::string AISAPI::INVENTORY_CAP_NAME("InventoryAPIv3");
const std::string AISAPI::LIBRARY_CAP_NAME("LibraryAPIv3");
const S32 AISAPI::HTTP_TIMEOUT = 180;

std::list<AISAPI::ais_query_item_t> AISAPI::sPostponedQuery;

const S32 MAX_SIMULTANEOUS_COROUTINES = 2048;

// AIS3 allows '*' requests, but in reality those will be cut at some point
// Specify own depth to be able to anticipate it and mark folders as incomplete
const S32 MAX_FOLDER_DEPTH_REQUEST = 50;

//-------------------------------------------------------------------------
/*static*/
bool AISAPI::isAvailable()
{
    if (gAgent.getRegion())
    {
        return gAgent.getRegion()->isCapabilityAvailable(INVENTORY_CAP_NAME);
    }
    return false;
}

/*static*/
void AISAPI::getCapNames(LLSD& capNames)
{
    capNames.append(INVENTORY_CAP_NAME);
    capNames.append(LIBRARY_CAP_NAME);
}

/*static*/
std::string AISAPI::getInvCap()
{
    if (gAgent.getRegion())
    {
        return gAgent.getRegion()->getCapability(INVENTORY_CAP_NAME);
    }
    return std::string();
}

/*static*/
std::string AISAPI::getLibCap()
{
    if (gAgent.getRegion())
    {
        return gAgent.getRegion()->getCapability(LIBRARY_CAP_NAME);
    }
    return std::string();
}

/*static*/ 
void AISAPI::CreateInventory(const LLUUID& parentId, const LLSD& newInventory, completion_t callback)
{
    std::string cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }

    LLUUID tid;
    tid.generate();

    std::string url = cap + std::string("/category/") + parentId.asString() + "?tid=" + tid.asString();
    LL_DEBUGS("Inventory") << "url: " << url << " parentID " << parentId << " newInventory " << newInventory << LL_ENDL;

    // I may be suffering from golden hammer here, but the first part of this bind 
    // is actually a static cast for &HttpCoroutineAdapter::postAndSuspend so that 
    // the compiler can identify the correct signature to select.
    // 
    // Reads as follows:
    // LLSD     - method returning LLSD
    // (LLCoreHttpUtil::HttpCoroutineAdapter::*) - pointer to member function of HttpCoroutineAdapter
    // (LLCore::HttpRequest::ptr_t, const std::string &, const LLSD &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t) - signature of method
    //  
    invokationFn_t postFn = boost::bind(
        // Humans ignore next line.  It is just a cast.
        static_cast<LLSD (LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, const LLSD &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::postAndSuspend), _1, _2, _3, _4, _5, _6);

    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, postFn, url, parentId, newInventory, callback, CREATEINVENTORY));
    EnqueueAISCommand("CreateInventory", proc);
}

/*static*/ 
void AISAPI::SlamFolder(const LLUUID& folderId, const LLSD& newInventory, completion_t callback)
{
    std::string cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }

    LLUUID tid;
    tid.generate();

    std::string url = cap + std::string("/category/") + folderId.asString() + "/links?tid=" + tid.asString();

    // see comment above in CreateInventoryCommand
    invokationFn_t putFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, const LLSD &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::putAndSuspend), _1, _2, _3, _4, _5, _6);

    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, putFn, url, folderId, newInventory, callback, SLAMFOLDER));

    EnqueueAISCommand("SlamFolder", proc);
}

void AISAPI::RemoveCategory(const LLUUID &categoryId, completion_t callback)
{
    std::string cap;

    cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }

    std::string url = cap + std::string("/category/") + categoryId.asString();
    LL_DEBUGS("Inventory") << "url: " << url << LL_ENDL;

    invokationFn_t delFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::deleteAndSuspend), _1, _2, _3, _5, _6);

    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, delFn, url, categoryId, LLSD(), callback, REMOVECATEGORY));

    EnqueueAISCommand("RemoveCategory", proc);
}

/*static*/ 
void AISAPI::RemoveItem(const LLUUID &itemId, completion_t callback)
{
    std::string cap;

    cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }

    std::string url = cap + std::string("/item/") + itemId.asString();
    LL_DEBUGS("Inventory") << "url: " << url << LL_ENDL;

    invokationFn_t delFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::deleteAndSuspend), _1, _2, _3, _5, _6);

    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, delFn, url, itemId, LLSD(), callback, REMOVEITEM));

    EnqueueAISCommand("RemoveItem", proc);
}

void AISAPI::CopyLibraryCategory(const LLUUID& sourceId, const LLUUID& destId, bool copySubfolders, completion_t callback)
{
    std::string cap;

    cap = getLibCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Library cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }

    LL_DEBUGS("Inventory") << "Copying library category: " << sourceId << " => " << destId << LL_ENDL;

    LLUUID tid;
    tid.generate();

    std::string url = cap + std::string("/category/") + sourceId.asString() + "?tid=" + tid.asString();
    if (!copySubfolders)
    {
        url += ",depth=0";
    }
    LL_INFOS() << url << LL_ENDL;

    std::string destination = destId.asString();

    invokationFn_t copyFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, const std::string, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::copyAndSuspend), _1, _2, _3, destination, _5, _6);
         
    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, copyFn, url, destId, LLSD(), callback, COPYLIBRARYCATEGORY));

    EnqueueAISCommand("CopyLibraryCategory", proc);
}

/*static*/ 
void AISAPI::PurgeDescendents(const LLUUID &categoryId, completion_t callback)
{
    std::string cap;

    cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }

    std::string url = cap + std::string("/category/") + categoryId.asString() + "/children";
    LL_DEBUGS("Inventory") << "url: " << url << LL_ENDL;

    invokationFn_t delFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::deleteAndSuspend), _1, _2, _3, _5, _6);

    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, delFn, url, categoryId, LLSD(), callback, PURGEDESCENDENTS));

    EnqueueAISCommand("PurgeDescendents", proc);
}


/*static*/
void AISAPI::UpdateCategory(const LLUUID &categoryId, const LLSD &updates, completion_t callback)
{
    std::string cap;

    cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }
    std::string url = cap + std::string("/category/") + categoryId.asString();

    invokationFn_t patchFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, const LLSD &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::patchAndSuspend), _1, _2, _3, _4, _5, _6);

    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, patchFn, url, categoryId, updates, callback, UPDATECATEGORY));

    EnqueueAISCommand("UpdateCategory", proc);
}

/*static*/
void AISAPI::UpdateItem(const LLUUID &itemId, const LLSD &updates, completion_t callback)
{

    std::string cap;

    cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }
    std::string url = cap + std::string("/item/") + itemId.asString();

    invokationFn_t patchFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, const LLSD &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::patchAndSuspend), _1, _2, _3, _4, _5, _6);

    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, patchFn, url, itemId, updates, callback, UPDATEITEM));

    EnqueueAISCommand("UpdateItem", proc);
}

/*static*/
void AISAPI::FetchItem(const LLUUID &itemId, ITEM_TYPE type, completion_t callback)
{
	std::string cap;

	cap = (type == INVENTORY) ? getInvCap() : getLibCap();
	if (cap.empty())
	{
		LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
		return;
	}
	std::string url = cap + std::string("/item/") + itemId.asString();

	invokationFn_t getFn = boost::bind(
		// Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
		static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
		//----
		// _1 -> httpAdapter
		// _2 -> httpRequest
		// _3 -> url
		// _4 -> body 
		// _5 -> httpOptions
		// _6 -> httpHeaders
		(&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6);

	LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
		_1, getFn, url, itemId, LLSD(), callback, FETCHITEM));

	EnqueueAISCommand("FetchItem", proc);
}

/*static*/
void AISAPI::FetchCategoryChildren(const LLUUID &catId, ITEM_TYPE type, bool recursive, completion_t callback, S32 depth)
{
    std::string cap;

    cap = (type == INVENTORY) ? getInvCap() : getLibCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }
    std::string url = cap + std::string("/category/") + catId.asString() + "/children";

    if (recursive)
    {
        // can specify depth=*, but server side is going to cap requests
        // and reject everything 'over the top',.
        depth = MAX_FOLDER_DEPTH_REQUEST;
    }
    else
    {
        depth = llmin(depth, MAX_FOLDER_DEPTH_REQUEST);
    }

    url += "?depth=" + std::to_string(depth);

    invokationFn_t getFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6);

    // get doesn't use body, can pass additional data
    LLSD body;
    body["depth"] = depth;
    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, getFn, url, catId, body, callback, FETCHCATEGORYCHILDREN));

    EnqueueAISCommand("FetchCategoryChildren", proc);
}

// some folders can be requested by name, like
// animatn | bodypart | clothing | current | favorite | gesture | inbox | landmark | lsltext
// lstndfnd | my_otfts | notecard | object | outbox | root | snapshot | sound | texture | trash
void AISAPI::FetchCategoryChildren(const std::string &identifier, bool recursive, completion_t callback, S32 depth)
{
    std::string cap;

    cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }
    std::string url = cap + std::string("/category/") + identifier + "/children";

    if (recursive)
    {
        // can specify depth=*, but server side is going to cap requests
        // and reject everything 'over the top',.
        depth = MAX_FOLDER_DEPTH_REQUEST;
    }
    else
    {
        depth = llmin(depth, MAX_FOLDER_DEPTH_REQUEST);
    }

    url += "?depth=" + std::to_string(depth);

    invokationFn_t getFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6);

    // get doesn't use body, can pass additional data
    LLSD body;
    body["depth"] = depth;
    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, getFn, url, LLUUID::null, body, callback, FETCHCATEGORYCHILDREN));

    EnqueueAISCommand("FetchCategoryChildren", proc);
}

/*static*/
void AISAPI::FetchCategoryCategories(const LLUUID &catId, ITEM_TYPE type, bool recursive, completion_t callback, S32 depth)
{
    std::string cap;

    cap = (type == INVENTORY) ? getInvCap() : getLibCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }
    std::string url = cap + std::string("/category/") + catId.asString() + "/categories";

    if (recursive)
    {
        // can specify depth=*, but server side is going to cap requests
        // and reject everything 'over the top',.
        depth = MAX_FOLDER_DEPTH_REQUEST;
    }
    else
    {
        depth = llmin(depth, MAX_FOLDER_DEPTH_REQUEST);
    }

    url += "?depth=" + std::to_string(depth);

    invokationFn_t getFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6);

    // get doesn't use body, can pass additional data
    LLSD body;
    body["depth"] = depth;
    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
        _1, getFn, url, catId, body, callback, FETCHCATEGORYCATEGORIES));

    EnqueueAISCommand("FetchCategoryCategories", proc);
}

void AISAPI::FetchCategorySubset(const LLUUID& catId,
                                   const uuid_vec_t specificChildren,
                                   ITEM_TYPE type,
                                   bool recursive,
                                   completion_t callback,
                                   S32 depth)
{
    std::string cap = (type == INVENTORY) ? getInvCap() : getLibCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }
    if (specificChildren.empty())
    {
        LL_WARNS("Inventory") << "Empty request!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }
    // category/any_folder_id/children?depth=*&children=child_id1,child_id2,child_id3
    std::string url = cap + std::string("/category/") + catId.asString() + "/children";

    if (recursive)
    {
        depth = MAX_FOLDER_DEPTH_REQUEST;
    }
    else
    {
        depth = llmin(depth, MAX_FOLDER_DEPTH_REQUEST);
    }

    uuid_vec_t::const_iterator iter = specificChildren.begin();
    uuid_vec_t::const_iterator end = specificChildren.end();

    url += "?depth=" + std::to_string(depth) + "&children=" + iter->asString();
    iter++;

    while (iter != end)
    {
        url += "," + iter->asString();
        iter++;
    }

    const S32 MAX_URL_LENGH = 2000; // RFC documentation specifies a maximum length of 2048
    if (url.length() > MAX_URL_LENGH)
    {
        LL_WARNS("Inventory") << "Request url is too long, url: " << url << LL_ENDL;
    }

    invokationFn_t getFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string&, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6);

    // get doesn't use body, can pass additional data
    LLSD body;
    body["depth"] = depth;
    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
                                                         _1, getFn, url, catId, body, callback, FETCHCATEGORYSUBSET));

    EnqueueAISCommand("FetchCategorySubset", proc);
}

/*static*/
// Will get COF folder, links in it and items those links point to
void AISAPI::FetchCOF(completion_t callback)
{
    std::string cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }
    std::string url = cap + std::string("/category/current/links");

    invokationFn_t getFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string&, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6);

    LLSD body;
    // Only cof folder will be full, but cof can contain an outfit
    // link with embedded outfit folder for request to parse
    body["depth"] = 0;
    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
                                                         _1, getFn, url, LLUUID::null, body, callback, FETCHCOF));

    EnqueueAISCommand("FetchCOF", proc);
}

void AISAPI::FetchCategoryLinks(const LLUUID &catId, completion_t callback)
{
    std::string cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }
    std::string url = cap + std::string("/category/") + catId.asString() + "/links";

    invokationFn_t getFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD (LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &,
                                                                   LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend),
        _1, _2, _3, _5, _6);

    LLSD body;
    body["depth"] = 0;
    LLCoprocedureManager::CoProcedure_t proc(
        boost::bind(&AISAPI::InvokeAISCommandCoro, _1, getFn, url, LLUUID::null, body, callback, FETCHCATEGORYLINKS));

    EnqueueAISCommand("FetchCategoryLinks", proc);
}

/*static*/
void AISAPI::FetchOrphans(completion_t callback)
{
    std::string cap = getInvCap();
    if (cap.empty())
    {
        LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }
    std::string url = cap + std::string("/orphans");

    invokationFn_t getFn = boost::bind(
        // Humans ignore next line.  It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
        static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t , const std::string& , LLCore::HttpOptions::ptr_t , LLCore::HttpHeaders::ptr_t)>
        //----
        // _1 -> httpAdapter
        // _2 -> httpRequest
        // _3 -> url
        // _4 -> body 
        // _5 -> httpOptions
        // _6 -> httpHeaders
        (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend) , _1 , _2 , _3 , _5 , _6);

    LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro ,
                                                         _1 , getFn , url , LLUUID::null , LLSD() , callback , FETCHORPHANS));

    EnqueueAISCommand("FetchOrphans" , proc);
}

/*static*/
void AISAPI::EnqueueAISCommand(const std::string &procName, LLCoprocedureManager::CoProcedure_t proc)
{
    LLCoprocedureManager &inst = LLCoprocedureManager::instance();
    S32 pending_in_pool = inst.countPending("AIS");
    std::string procFullName = "AIS(" + procName + ")";
    if (pending_in_pool < MAX_SIMULTANEOUS_COROUTINES)
    {
        inst.enqueueCoprocedure("AIS", procFullName, proc);
    }
    else
    {
        // As I understand it, coroutines have built-in 'pending' pool
        // but unfortunately it has limited size which inventory often goes over
        // so this is a workaround to not overfill it.
        if (sPostponedQuery.empty())
        {
            sPostponedQuery.push_back(ais_query_item_t(procFullName, proc));
            gIdleCallbacks.addFunction(onIdle, NULL);
        }
        else
        {
            sPostponedQuery.push_back(ais_query_item_t(procFullName, proc));
        }
    }
}

/*static*/
void AISAPI::onIdle(void *userdata)
{
    if (!sPostponedQuery.empty())
    {
        LLCoprocedureManager &inst = LLCoprocedureManager::instance();
        S32 pending_in_pool = inst.countPending("AIS");
        while (pending_in_pool < MAX_SIMULTANEOUS_COROUTINES && !sPostponedQuery.empty())
        {
            ais_query_item_t &item = sPostponedQuery.front();
            inst.enqueueCoprocedure("AIS", item.first, item.second);
            sPostponedQuery.pop_front();
            pending_in_pool++;
        }
    }
    
    if (sPostponedQuery.empty())
    {
        // Nothing to do anymore
        gIdleCallbacks.deleteFunction(onIdle, NULL);
    }
}

/*static*/
void AISAPI::onUpdateReceived(const LLSD& update, COMMAND_TYPE type, const LLSD& request_body)
{
    LLTimer timer;
    if ( (type == UPDATECATEGORY || type == UPDATEITEM)
        && gSavedSettings.getBOOL("DebugAvatarAppearanceMessage"))
    {
        dump_sequential_xml(gAgentAvatarp->getFullname() + "_ais_update", update);
    }

    AISUpdate ais_update(update, type, request_body);
    ais_update.doUpdate(); // execute the updates in the appropriate order.
    LL_DEBUGS("Inventory", "AIS3") << "Elapsed processing: " << timer.getElapsedTimeF32() << LL_ENDL;
}

/*static*/
void AISAPI::InvokeAISCommandCoro(LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t httpAdapter, 
        invokationFn_t invoke, std::string url, 
        LLUUID targetId, LLSD body, completion_t callback, COMMAND_TYPE type)
{
    if (gDisconnected)
    {
        if (callback)
        {
            callback(LLUUID::null);
        }
        return;
    }

    LLCore::HttpOptions::ptr_t httpOptions(std::make_shared<LLCore::HttpOptions>());
    LLCore::HttpRequest::ptr_t httpRequest(std::make_shared<LLCore::HttpRequest>());
    LLCore::HttpHeaders::ptr_t httpHeaders;

    httpOptions->setTimeout(HTTP_TIMEOUT);

    LL_DEBUGS("Inventory") << "Request url: " << url << LL_ENDL;

    LLSD result;
    LLSD httpResults;
    LLCore::HttpStatus status;

    result = invoke(httpAdapter , httpRequest , url , body , httpOptions , httpHeaders);
    httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
    status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);

    if (!status || !result.isMap())
    {
        if (!result.isMap())
        {
            status = LLCore::HttpStatus(HTTP_INTERNAL_ERROR, "Malformed response contents");
        }
        else if (status.getType() == 410) //GONE
        {
            // Item does not exist or was already deleted from server.
            // parent folder is out of sync
            if (type == REMOVECATEGORY)
            {
                LLViewerInventoryCategory *cat = gInventory.getCategory(targetId);
                if (cat)
                {
                    LL_WARNS("Inventory") << "Purge failed for '" << cat->getName()
                        << "' local version:" << cat->getVersion()
                        << " since folder no longer exists at server. Descendent count: server == " << cat->getDescendentCount()
                        << ", viewer == " << cat->getViewerDescendentCount()
                        << LL_ENDL;
                    gInventory.fetchDescendentsOf(cat->getParentUUID());
                    // Note: don't delete folder here - contained items will be deparented (or deleted)
                    // and since we are clearly out of sync we can't be sure we won't get rid of something we need.
                    // For example folder could have been moved or renamed with items intact, let it fetch first.
                }
            }
            else if (type == REMOVEITEM)
            {
                LLViewerInventoryItem *item = gInventory.getItem(targetId);
                if (item)
                {
                    LL_WARNS("Inventory") << "Purge failed for '" << item->getName()
                        << "' since item no longer exists at server." << LL_ENDL;
                    gInventory.fetchDescendentsOf(item->getParentUUID());
                    // since item not on the server and exists at viewer, so it needs an update at the least,
                    // so delete it, in worst case item will be refetched with new params.
                    gInventory.onObjectDeletedFromServer(targetId);
                }
            }
        }
        else if (status == LLCore::HttpStatus(HTTP_FORBIDDEN) /*403*/)
        {
            if (type == FETCHCATEGORYCHILDREN)
            {
                if (body.has("depth") && body["depth"].asInteger() == 0)
                {
                    // Can't fetch a single folder with depth 0, folder is too big.
                    static bool first_call = true;
                    if (first_call)
                    {
                        first_call = false;
                        LLNotificationsUtil::add("InventoryLimitReachedAISAlert");
                    }
                    else
                    {
                        LLNotificationsUtil::add("InventoryLimitReachedAIS");
                    }
                    LL_WARNS("Inventory") << "Fetch failed, content is over limit, url: " << url << LL_ENDL;
                }
                else
                {
                    // Result was too big, but situation is recoverable by requesting with lower depth
                    LL_DEBUGS("Inventory") << "Fetch failed, content is over limit, url: " << url << LL_ENDL;
                }
            }
        }
        LL_WARNS("Inventory") << "Inventory error: " << status.toString() << LL_ENDL;
        LL_WARNS("Inventory") << ll_pretty_print_sd(result) << LL_ENDL;
    }

	LL_DEBUGS("Inventory", "AIS3") << "Result: " << result << LL_ENDL;
    onUpdateReceived(result, type, body);

    if (callback && !callback.empty())
    {   
// ALCHMERGE
#if 0
// [SL:KB] - Patch: Appearance-SyncAttach | Checked: Catznip-3.7
		uuid_list_t ids;
		switch (type)
		{
			case COPYLIBRARYCATEGORY:
				if (result.has("category_id"))
				{
					ids.insert(result["category_id"]);
				}
				break;
			case COPYINVENTORY:
				{
					AISUpdate::parseUUIDArray(result, "_created_items", ids);
					AISUpdate::parseUUIDArray(result, "_created_categories", ids);
				}
				break;
			case UPDATECATEGORY:
				{
					AISUpdate::parseUUIDArray(result, "_updated_categories", ids);
				}
				break;
			default:
				break;
		}

		// If we were feeling daring we'd call LLInventoryCallback::fire for every item but it would take additional work to investigate whether all LLInventoryCallback derived classes
		// were designed to handle multiple fire calls (with legacy link creation only one would ever fire per link creation) so we'll be cautious and only call for the first one for now
		// (note that the LL code as written below will always call fire once with the NULL UUID for anything but CopyLibraryCategoryCommand so even the above is an improvement)
		callback( (!ids.empty()) ? *ids.begin() : LLUUID::null);
// [/SL:KB]
//        LLUUID id(LLUUID::null);
//
//        if (result.has("category_id") && (type == COPYLIBRARYCATEGORY))
//	    {
//		    id = result["category_id"];
//	    }
//
//        callback(id);
#else
        bool needs_callback = true;
        LLUUID id(LLUUID::null);

        switch (type)
        {
        case COPYLIBRARYCATEGORY:
        case FETCHCATEGORYCATEGORIES:
        case FETCHCATEGORYCHILDREN:
        case FETCHCATEGORYSUBSET:
        case FETCHCATEGORYLINKS:
        case FETCHCOF:
            if (result.has("category_id"))
            {
                id = result["category_id"];
            }
            break;
        case FETCHITEM:
            if (result.has("item_id"))
            {
                // Error message might contain an item_id!!!
                id = result["item_id"];
            }
            if (result.has("linked_id"))
            {
                id = result["linked_id"];
            }
            break;
        case CREATEINVENTORY:
            // CREATEINVENTORY can have multiple callbacks
            if (result.has("_created_categories"))
            {
                LLSD& cats = result["_created_categories"];
                LLSD::array_const_iterator cat_iter;
                for (cat_iter = cats.beginArray(); cat_iter != cats.endArray(); ++cat_iter)
                {
                    LLUUID cat_id = *cat_iter;
                    callback(cat_id);
                    needs_callback = false;
                }
            }
            if (result.has("_created_items"))
            {
                LLSD& items = result["_created_items"];
                LLSD::array_const_iterator item_iter;
                for (item_iter = items.beginArray(); item_iter != items.endArray(); ++item_iter)
                {
                    LLUUID item_id = *item_iter;
                    callback(item_id);
                    needs_callback = false;
                }
            }
            break;
        default:
            break;
        }

        if (needs_callback)
        {
            // Call callback at least once regardless of failure.
            // UPDATEITEM doesn't expect an id
            callback(id);
        }
#endif
    }

}

//-------------------------------------------------------------------------
AISUpdate::AISUpdate(const LLSD& update, AISAPI::COMMAND_TYPE type, const LLSD& request_body)
: mType(type)
{
    mFetch = (type == AISAPI::FETCHITEM)
        || (type == AISAPI::FETCHCATEGORYCHILDREN)
        || (type == AISAPI::FETCHCATEGORYCATEGORIES)
        || (type == AISAPI::FETCHCATEGORYSUBSET)
        || (type == AISAPI::FETCHCOF)
        || (type == AISAPI::FETCHCATEGORYLINKS)
        || (type == AISAPI::FETCHORPHANS);
    // parse update llsd into stuff to do or parse received items.
    mFetchDepth = MAX_FOLDER_DEPTH_REQUEST;
    if (mFetch && request_body.has("depth"))
    {
        mFetchDepth = request_body["depth"].asInteger();
    }

    mTimer.setTimerExpirySec(AIS_EXPIRY_SECONDS);
    mTimer.start();
	parseUpdate(update);
}

void AISUpdate::clearParseResults()
{
	mCatDescendentDeltas.clear();
	mCatDescendentsKnown.clear();
	mCatVersionsUpdated.clear();
	mItemsCreated.clear();
    mItemsLost.clear();
	mItemsUpdated.clear();
	mCategoriesCreated.clear();
	mCategoriesUpdated.clear();
	mObjectsDeletedIds.clear();
	mItemIds.clear();
	mCategoryIds.clear();
}

void AISUpdate::checkTimeout()
{
    if (mTimer.hasExpired())
    {
        llcoro::suspend();
        LLCoros::checkStop();
        mTimer.setTimerExpirySec(AIS_EXPIRY_SECONDS);
    }
}

void AISUpdate::parseUpdate(const LLSD& update)
{
	clearParseResults();
	parseMeta(update);
	parseContent(update);
}

void AISUpdate::parseMeta(const LLSD& update)
{
	// parse _categories_removed -> mObjectsDeletedIds
	uuid_list_t cat_ids;
	parseUUIDArray(update,"_categories_removed",cat_ids);
	for (uuid_list_t::const_iterator it = cat_ids.begin();
		 it != cat_ids.end(); ++it)
	{
		LLViewerInventoryCategory *cat = gInventory.getCategory(*it);
		if(cat)
		{
			mCatDescendentDeltas[cat->getParentUUID()]--;
			mObjectsDeletedIds.insert(*it);
		}
		else
		{
			LL_WARNS("Inventory") << "removed category not found " << *it << LL_ENDL;
		}
	}

	// parse _categories_items_removed -> mObjectsDeletedIds
	uuid_list_t item_ids;
	parseUUIDArray(update,"_category_items_removed",item_ids);
	parseUUIDArray(update,"_removed_items",item_ids);
	for (uuid_list_t::const_iterator it = item_ids.begin();
		 it != item_ids.end(); ++it)
	{
		LLViewerInventoryItem *item = gInventory.getItem(*it);
		if(item)
		{
			mCatDescendentDeltas[item->getParentUUID()]--;
			mObjectsDeletedIds.insert(*it);
		}
		else
		{
			LL_WARNS("Inventory") << "removed item not found " << *it << LL_ENDL;
		}
	}

	// parse _broken_links_removed -> mObjectsDeletedIds
	uuid_list_t broken_link_ids;
	parseUUIDArray(update,"_broken_links_removed",broken_link_ids);
	for (uuid_list_t::const_iterator it = broken_link_ids.begin();
		 it != broken_link_ids.end(); ++it)
	{
		LLViewerInventoryItem *item = gInventory.getItem(*it);
		if(item)
		{
			mCatDescendentDeltas[item->getParentUUID()]--;
			mObjectsDeletedIds.insert(*it);
		}
		else
		{
			LL_WARNS("Inventory") << "broken link not found " << *it << LL_ENDL;
		}
	}

	// parse _created_items
	parseUUIDArray(update,"_created_items",mItemIds);

	// parse _created_categories
	parseUUIDArray(update,"_created_categories",mCategoryIds);

	// Parse updated category versions.
	const std::string& ucv = "_updated_category_versions";
	if (update.has(ucv))
	{
		for(LLSD::map_const_iterator it = update[ucv].beginMap(),
				end = update[ucv].endMap();
			it != end; ++it)
		{
			const LLUUID id((*it).first);
			S32 version = (*it).second.asInteger();
			mCatVersionsUpdated[id] = version;
		}
	}
}

void AISUpdate::parseContent(const LLSD& update)
{
    // Errors from a fetch request might contain id without
    // full item or folder.
    // Todo: Depending on error we might want to do something,
    // like removing a 404 item or refetching parent folder
	if (update.has("linked_id") && update.has("parent_id"))
	{
		parseLink(update, mFetchDepth);
	}
	else if (update.has("item_id") && update.has("parent_id"))
	{
		parseItem(update);
	}

    if (mType == AISAPI::FETCHCATEGORYSUBSET)
    {
        // initial category is incomplete, don't process it,
        // go for content instead
        if (update.has("_embedded"))
        {
            parseEmbedded(update["_embedded"], mFetchDepth - 1);
        }
    }
    else if (update.has("category_id") && update.has("parent_id"))
    {
        parseCategory(update, mFetchDepth);
    }
	else
	{
		if (update.has("_embedded"))
		{
			parseEmbedded(update["_embedded"], mFetchDepth);
		}
	}
}

void AISUpdate::parseItem(const LLSD& item_map)
{
	LLUUID item_id = item_map["item_id"].asUUID();
	LLPointer<LLViewerInventoryItem> new_item(new LLViewerInventoryItem);
	LLViewerInventoryItem *curr_item = gInventory.getItem(item_id);
	if (curr_item)
	{
		// Default to current values where not provided.
		new_item->copyViewerItem(curr_item);
	}
	BOOL rv = new_item->unpackMessage(item_map);
	if (rv)
	{
        if (mFetch)
        {
            mItemsCreated[item_id] = new_item;
            new_item->setComplete(true);

            if (new_item->getParentUUID().isNull())
            {
                mItemsLost[item_id] = new_item;
            }
        }
        else if (curr_item)
		{
			mItemsUpdated[item_id] = new_item;
			// This statement is here to cause a new entry with 0
			// delta to be created if it does not already exist;
			// otherwise has no effect.
			mCatDescendentDeltas[new_item->getParentUUID()];
		}
		else
		{
			mItemsCreated[item_id] = new_item;
			mCatDescendentDeltas[new_item->getParentUUID()]++;
            new_item->setComplete(true);
		}
	}
	else
	{
		// *TODO: Wow, harsh.  Should we just complain and get out?
		LL_ERRS() << "unpack failed" << LL_ENDL;
	}
}

void AISUpdate::parseLink(const LLSD& link_map, S32 depth)
{
	LLUUID item_id = link_map["item_id"].asUUID();
	LLPointer<LLViewerInventoryItem> new_link(new LLViewerInventoryItem);
	LLViewerInventoryItem *curr_link = gInventory.getItem(item_id);
	if (curr_link)
	{
		// Default to current values where not provided.
		new_link->copyViewerItem(curr_link);
	}
	BOOL rv = new_link->unpackMessage(link_map);
	if (rv)
	{
		const LLUUID& parent_id = new_link->getParentUUID();
        if (mFetch)
        {
            LLPermissions default_perms;
            default_perms.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
            default_perms.initMasks(PERM_NONE, PERM_NONE, PERM_NONE, PERM_NONE, PERM_NONE);
            new_link->setPermissions(default_perms);
            LLSaleInfo default_sale_info;
            new_link->setSaleInfo(default_sale_info);
            //LL_DEBUGS("Inventory") << "creating link from llsd: " << ll_pretty_print_sd(link_map) << LL_ENDL;
            mItemsCreated[item_id] = new_link;
            new_link->setComplete(true);

            if (new_link->getParentUUID().isNull())
            {
                mItemsLost[item_id] = new_link;
            }
        }
		else if (curr_link)
		{
			mItemsUpdated[item_id] = new_link;
			// This statement is here to cause a new entry with 0
			// delta to be created if it does not already exist;
			// otherwise has no effect.
			mCatDescendentDeltas[parent_id];
		}
		else
		{
			LLPermissions default_perms;
			default_perms.init(gAgent.getID(),gAgent.getID(),LLUUID::null,LLUUID::null);
			default_perms.initMasks(PERM_NONE,PERM_NONE,PERM_NONE,PERM_NONE,PERM_NONE);
			new_link->setPermissions(default_perms);
			LLSaleInfo default_sale_info;
			new_link->setSaleInfo(default_sale_info);
			//LL_DEBUGS("Inventory") << "creating link from llsd: " << ll_pretty_print_sd(link_map) << LL_ENDL;
			mItemsCreated[item_id] = new_link;
			mCatDescendentDeltas[parent_id]++;
            new_link->setComplete(true);
		}

        if (link_map.has("_embedded"))
        {
            parseEmbedded(link_map["_embedded"], depth);
        }
	}
	else
	{
		// *TODO: Wow, harsh.  Should we just complain and get out?
		LL_ERRS() << "unpack failed" << LL_ENDL;
	}
}


void AISUpdate::parseCategory(const LLSD& category_map, S32 depth)
{
    LLUUID category_id = category_map["category_id"].asUUID();
    S32 version = LLViewerInventoryCategory::VERSION_UNKNOWN;

    if (category_map.has("version"))
    {
        version = category_map["version"].asInteger();
    }

    LLViewerInventoryCategory *curr_cat = gInventory.getCategory(category_id);

    if (curr_cat
        && curr_cat->getVersion() > LLViewerInventoryCategory::VERSION_UNKNOWN
        && curr_cat->getDescendentCount() != LLViewerInventoryCategory::DESCENDENT_COUNT_UNKNOWN
        && version > LLViewerInventoryCategory::VERSION_UNKNOWN
        && version < curr_cat->getVersion())
    {
        LL_WARNS() << "Got stale folder, known: " << curr_cat->getVersion()
            << ", received: " << version << LL_ENDL;
        return;
    }

	LLPointer<LLViewerInventoryCategory> new_cat;
	if (curr_cat)
	{
		// Default to current values where not provided.
        new_cat = new LLViewerInventoryCategory(curr_cat);
    }
    else
    {
        if (category_map.has("agent_id"))
        {
            new_cat = new LLViewerInventoryCategory(category_map["agent_id"].asUUID());
        }
        else
        {
            LL_DEBUGS() << "No owner provided, folder might be assigned wrong owner" << LL_ENDL;
            new_cat = new LLViewerInventoryCategory(LLUUID::null);
        }
    }
	BOOL rv = new_cat->unpackMessage(category_map);
	// *NOTE: unpackMessage does not unpack version or descendent count.
	if (rv)
	{
        // Check descendent count first, as it may be needed
        // to populate newly created categories
        if (category_map.has("_embedded"))
        {
            parseDescendentCount(category_id, new_cat->getPreferredType(), category_map["_embedded"]);
        }

        if (mFetch)
        {
            uuid_int_map_t::const_iterator lookup_it = mCatDescendentsKnown.find(category_id);
            if (mCatDescendentsKnown.end() != lookup_it)
            {
                S32 descendent_count = lookup_it->second;
                LL_DEBUGS("Inventory") << "Setting descendents count to " << descendent_count
                    << " for category " << category_id << LL_ENDL;
                new_cat->setDescendentCount(descendent_count);

                // set version only if we are sure this update has full data and embeded items
                // since viewer uses version to decide if folder and content still need fetching
                if (version > LLViewerInventoryCategory::VERSION_UNKNOWN
                    && depth >= 0)
                {
                    if (curr_cat && curr_cat->getVersion() > version)
                    {
                        LL_WARNS("Inventory") << "Version was " << curr_cat->getVersion()
                            << ", but fetch returned version " << version
                            << " for category " << category_id << LL_ENDL;
                    }
                    else
                    {
                        LL_DEBUGS("Inventory") << "Setting version to " << version
                            << " for category " << category_id << LL_ENDL;
                    }

                    new_cat->setVersion(version);
                }
            }
            else if (curr_cat
                     && curr_cat->getVersion() > LLViewerInventoryCategory::VERSION_UNKNOWN
                     && version > curr_cat->getVersion())
            {
                // Potentially should new_cat->setVersion(unknown) here,
                // but might be waiting for a callback that would increment
                LL_DEBUGS("Inventory") << "Category " << category_id
                    << " is stale. Known version: " << curr_cat->getVersion()
                    << " server version: " << version << LL_ENDL;
            }
            mCategoriesCreated[category_id] = new_cat;
        }
		else if (curr_cat)
		{
			mCategoriesUpdated[category_id] = new_cat;
			// This statement is here to cause a new entry with 0
			// delta to be created if it does not already exist;
			// otherwise has no effect.
			mCatDescendentDeltas[new_cat->getParentUUID()];
			// Capture update for the category itself as well.
			mCatDescendentDeltas[category_id];
		}
		else
		{
			// Set version/descendents for newly created categories.
            uuid_int_map_t::const_iterator lookup_it = mCatDescendentsKnown.find(category_id);
            if (mCatDescendentsKnown.end() != lookup_it)
            {
                S32 descendent_count = lookup_it->second;
                LL_DEBUGS("Inventory") << "Setting descendents count to " << descendent_count
                    << " for new category " << category_id << LL_ENDL;
                new_cat->setDescendentCount(descendent_count);

                // Don't set version unles correct children count is present
                if (category_map.has("version"))
                {
                    S32 version = category_map["version"].asInteger();
                    LL_DEBUGS("Inventory") << "Setting version to " << version
                        << " for new category " << category_id << LL_ENDL;
                    new_cat->setVersion(version);
                }
			}
			mCategoriesCreated[category_id] = new_cat;
			mCatDescendentDeltas[new_cat->getParentUUID()]++;
		}
	}
	else
	{
		// *TODO: Wow, harsh.  Should we just complain and get out?
		LL_ERRS() << "unpack failed" << LL_ENDL;
	}

	// Check for more embedded content.
	if (category_map.has("_embedded"))
	{
		parseEmbedded(category_map["_embedded"], depth - 1);
	}
}

void AISUpdate::parseDescendentCount(const LLUUID& category_id, LLFolderType::EType type, const LLSD& embedded)
{
    // We can only determine true descendent count if this contains all descendent types.
    if (embedded.has("categories") &&
        embedded.has("links") &&
        embedded.has("items"))
    {
        mCatDescendentsKnown[category_id] = embedded["categories"].size();
        mCatDescendentsKnown[category_id] += embedded["links"].size();
        mCatDescendentsKnown[category_id] += embedded["items"].size();
    }
    else if (mFetch && embedded.has("links") && (type == LLFolderType::FT_CURRENT_OUTFIT || type == LLFolderType::FT_OUTFIT))
    {
        // COF and outfits contain links only
        mCatDescendentsKnown[category_id] = embedded["links"].size();
    }
}

void AISUpdate::parseEmbedded(const LLSD& embedded, S32 depth)
{
    checkTimeout();

	if (embedded.has("links")) // _embedded in a category
	{
		parseEmbeddedLinks(embedded["links"], depth);
	}
	if (embedded.has("items")) // _embedded in a category
	{
		parseEmbeddedItems(embedded["items"]);
	}
	if (embedded.has("item")) // _embedded in a link
	{
		parseEmbeddedItem(embedded["item"]);
	}
	if (embedded.has("categories")) // _embedded in a category
	{
		parseEmbeddedCategories(embedded["categories"], depth);
	}
	if (embedded.has("category")) // _embedded in a link
	{
		parseEmbeddedCategory(embedded["category"], depth);
	}
}

void AISUpdate::parseUUIDArray(const LLSD& content, const std::string& name, uuid_list_t& ids)
{
	if (content.has(name))
	{
		for(LLSD::array_const_iterator it = content[name].beginArray(),
				end = content[name].endArray();
				it != end; ++it)
		{
			ids.insert((*it).asUUID());
		}
	}
}

void AISUpdate::parseEmbeddedLinks(const LLSD& links, S32 depth)
{
	for(LLSD::map_const_iterator linkit = links.beginMap(),
			linkend = links.endMap();
		linkit != linkend; ++linkit)
	{
		const LLUUID link_id((*linkit).first);
		const LLSD& link_map = (*linkit).second;
		if (!mFetch && mItemIds.end() == mItemIds.find(link_id))
		{
			LL_DEBUGS("Inventory") << "Ignoring link not in items list " << link_id << LL_ENDL;
		}
		else
		{
			parseLink(link_map, depth);
		}
	}
}

void AISUpdate::parseEmbeddedItem(const LLSD& item)
{
	// a single item (_embedded in a link)
	if (item.has("item_id"))
	{
		if (mFetch || mItemIds.end() != mItemIds.find(item["item_id"].asUUID()))
		{
			parseItem(item);
		}
	}
}

void AISUpdate::parseEmbeddedItems(const LLSD& items)
{
	// a map of items (_embedded in a category)
	for(LLSD::map_const_iterator itemit = items.beginMap(),
			itemend = items.endMap();
		itemit != itemend; ++itemit)
	{
		const LLUUID item_id((*itemit).first);
		const LLSD& item_map = (*itemit).second;
		if (!mFetch && mItemIds.end() == mItemIds.find(item_id))
		{
			LL_DEBUGS("Inventory") << "Ignoring item not in items list " << item_id << LL_ENDL;
		}
		else
		{
			parseItem(item_map);
		}
	}
}

void AISUpdate::parseEmbeddedCategory(const LLSD& category, S32 depth)
{
	// a single category (_embedded in a link)
	if (category.has("category_id"))
	{
		if (mFetch || mCategoryIds.end() != mCategoryIds.find(category["category_id"].asUUID()))
		{
			parseCategory(category, depth);
		}
	}
}

void AISUpdate::parseEmbeddedCategories(const LLSD& categories, S32 depth)
{
	// a map of categories (_embedded in a category)
	for(LLSD::map_const_iterator categoryit = categories.beginMap(),
			categoryend = categories.endMap();
		categoryit != categoryend; ++categoryit)
	{
		const LLUUID category_id((*categoryit).first);
		const LLSD& category_map = (*categoryit).second;
		if (!mFetch && mCategoryIds.end() == mCategoryIds.find(category_id))
		{
			LL_DEBUGS("Inventory") << "Ignoring category not in categories list " << category_id << LL_ENDL;
		}
		else
		{
			parseCategory(category_map, depth);
		}
	}
}

void AISUpdate::doUpdate()
{
    checkTimeout();

	// Do version/descendant accounting.
	for (auto catit = mCatDescendentDeltas.begin();
		 catit != mCatDescendentDeltas.end(); ++catit)
	{
#ifdef SHOW_DEBUG
		LL_DEBUGS("Inventory") << "descendant accounting for " << catit->first << LL_ENDL;
#endif

		const LLUUID cat_id(catit->first);
		// Don't account for update if we just created this category.
		if (mCategoriesCreated.find(cat_id) != mCategoriesCreated.end())
		{
			LL_DEBUGS("Inventory") << "Skipping version increment for new category " << cat_id << LL_ENDL;
			continue;
		}

		// Don't account for update unless AIS told us it updated that category.
		if (mCatVersionsUpdated.find(cat_id) == mCatVersionsUpdated.end())
		{
			LL_DEBUGS("Inventory") << "Skipping version increment for non-updated category " << cat_id << LL_ENDL;
			continue;
		}

		// If we have a known descendant count, set that now.
		LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id);
		if (cat)
		{
			S32 descendent_delta = catit->second;
			S32 old_count = cat->getDescendentCount();
			LL_DEBUGS("Inventory") << "Updating descendant count for "
								   << cat->getName() << " " << cat_id
								   << " with delta " << descendent_delta << " from "
								   << old_count << " to " << (old_count+descendent_delta) << LL_ENDL;
			LLInventoryModel::LLCategoryUpdate up(cat_id, descendent_delta);
			gInventory.accountForUpdate(up);
		}
		else
		{
			LL_DEBUGS("Inventory") << "Skipping version accounting for unknown category " << cat_id << LL_ENDL;
		}
	}

	// CREATE CATEGORIES
    const S32 MAX_UPDATE_BACKLOG = 50; // stall prevention
	for (deferred_category_map_t::const_iterator create_it = mCategoriesCreated.begin();
		 create_it != mCategoriesCreated.end(); ++create_it)
	{
		LLUUID category_id(create_it->first);
		LLPointer<LLViewerInventoryCategory> new_category = create_it->second;

		gInventory.updateCategory(new_category, LLInventoryObserver::CREATE);
		LL_DEBUGS("Inventory") << "created category " << category_id << LL_ENDL;

        // fetching can receive massive amount of items and folders
        if (gInventory.getChangedIDs().size() > MAX_UPDATE_BACKLOG)
        {
            gInventory.notifyObservers();
            checkTimeout();
        }
	}

	// UPDATE CATEGORIES
	for (deferred_category_map_t::const_iterator update_it = mCategoriesUpdated.begin();
		 update_it != mCategoriesUpdated.end(); ++update_it)
	{
		LLUUID category_id(update_it->first);
		LLPointer<LLViewerInventoryCategory> new_category = update_it->second;
		// Since this is a copy of the category *before* the accounting update, above,
		// we need to transfer back the updated version/descendant count.
		LLViewerInventoryCategory* curr_cat = gInventory.getCategory(new_category->getUUID());
		if (!curr_cat)
		{
			LL_WARNS("Inventory") << "Failed to update unknown category " << new_category->getUUID() << LL_ENDL;
		}
		else
		{
			new_category->setVersion(curr_cat->getVersion());
			new_category->setDescendentCount(curr_cat->getDescendentCount());
			gInventory.updateCategory(new_category);
			LL_DEBUGS("Inventory") << "updated category " << new_category->getName() << " " << category_id << LL_ENDL;
		}
	}

    // LOST ITEMS
    if (!mItemsLost.empty())
    {
        LL_INFOS("Inventory") << "Received " << (S32)mItemsLost.size() << " items without a parent" << LL_ENDL;
        const LLUUID lost_uuid(gInventory.findCategoryUUIDForType(LLFolderType::FT_LOST_AND_FOUND));
        if (lost_uuid.notNull())
        {
            for (deferred_item_map_t::const_iterator lost_it = mItemsLost.begin();
                 lost_it != mItemsLost.end(); ++lost_it)
            {
                LLPointer<LLViewerInventoryItem> new_item = lost_it->second;

                new_item->setParent(lost_uuid);
                new_item->updateParentOnServer(FALSE);
            }
        }
    }

	// CREATE ITEMS
	for (deferred_item_map_t::const_iterator create_it = mItemsCreated.begin();
		 create_it != mItemsCreated.end(); ++create_it)
	{
		LLUUID item_id(create_it->first);
		LLPointer<LLViewerInventoryItem> new_item = create_it->second;

		// FIXME risky function since it calls updateServer() in some
		// cases.  Maybe break out the update/create cases, in which
		// case this is create.
		LL_DEBUGS("Inventory") << "created item " << item_id << LL_ENDL;
		gInventory.updateItem(new_item, LLInventoryObserver::CREATE);

        // fetching can receive massive amount of items and folders
        if (gInventory.getChangedIDs().size() > MAX_UPDATE_BACKLOG)
        {
            gInventory.notifyObservers();
            checkTimeout();
        }
	}
	
	// UPDATE ITEMS
	for (deferred_item_map_t::const_iterator update_it = mItemsUpdated.begin();
		 update_it != mItemsUpdated.end(); ++update_it)
	{
		LLUUID item_id(update_it->first);
		LLPointer<LLViewerInventoryItem> new_item = update_it->second;
		// FIXME risky function since it calls updateServer() in some
		// cases.  Maybe break out the update/create cases, in which
		// case this is update.
		LL_DEBUGS("Inventory") << "updated item " << item_id << LL_ENDL;
		//LL_DEBUGS("Inventory") << ll_pretty_print_sd(new_item->asLLSD()) << LL_ENDL;
		gInventory.updateItem(new_item);
	}

	// DELETE OBJECTS
	for (uuid_list_t::const_iterator del_it = mObjectsDeletedIds.begin();
		 del_it != mObjectsDeletedIds.end(); ++del_it)
	{
		LL_DEBUGS("Inventory") << "deleted item " << *del_it << LL_ENDL;
		gInventory.onObjectDeletedFromServer(*del_it, false, false, false);
	}

	// TODO - how can we use this version info? Need to be sure all
	// changes are going through AIS first, or at least through
	// something with a reliable responder.
	for (uuid_int_map_t::iterator ucv_it = mCatVersionsUpdated.begin();
		 ucv_it != mCatVersionsUpdated.end(); ++ucv_it)
	{
		const LLUUID id = ucv_it->first;
		S32 version = ucv_it->second;
		LLViewerInventoryCategory *cat = gInventory.getCategory(id);
		LL_DEBUGS("Inventory") << "cat version update " << cat->getName() << " to version " << cat->getVersion() << LL_ENDL;
		if (cat->getVersion() != version)
		{
			LL_WARNS() << "Possible version mismatch for category " << cat->getName()
					<< ", viewer version " << cat->getVersion()
					<< " AIS version " << version << " !!!Adjusting local version!!!" <<  LL_ENDL;

            // the AIS version should be considered the true version. Adjust 
            // our local category model to reflect this version number.  Otherwise 
            // it becomes possible to get stuck with the viewer being out of 
            // sync with the inventory system.  Under normal circumstances 
            // inventory COF is maintained on the viewer through calls to 
            // LLInventoryModel::accountForUpdate when a changing operation 
            // is performed.  This occasionally gets out of sync however.
            if (version != LLViewerInventoryCategory::VERSION_UNKNOWN)
            {
                cat->setVersion(version);
            }
            else
            {
                // We do not account for update if version is UNKNOWN, so we shouldn't rise version
                // either or viewer will get stuck on descendants count -1, try to refetch folder instead
                cat->fetch();
            }
		}
	}

    checkTimeout();

	gInventory.notifyObservers();
}

