/**
 * @file llfloaterlinkreplace.cpp
 * @brief Allows replacing link targets in inventory links
 * @author Ansariel Hiller
 *
 * $LicenseInfo:firstyear=2017&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2017, 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 "llfloaterlinkreplace.h"

#include "llagent.h"
#include "llappearancemgr.h"
#include "lllineeditor.h"
#include "llnotificationsutil.h"
#include "llnotifications.h"
#include "lltextbox.h"
#include "llviewercontrol.h"

LLFloaterLinkReplace::LLFloaterLinkReplace(const LLSD& key)
    : LLFloater(key),
    LLEventTimer(gSavedSettings.getF32("LinkReplaceBatchPauseTime")),
    mRemainingItems(0),
    mSourceUUID(LLUUID::null),
    mTargetUUID(LLUUID::null),
    mBatchSize(gSavedSettings.getU32("LinkReplaceBatchSize"))
{
    mEventTimer.stop();
}

LLFloaterLinkReplace::~LLFloaterLinkReplace()
{
}

bool LLFloaterLinkReplace::postBuild()
{
    mStartBtn = getChild<LLButton>("btn_start");
    mStartBtn->setCommitCallback(boost::bind(&LLFloaterLinkReplace::onStartClicked, this));

    mRefreshBtn = getChild<LLButton>("btn_refresh");
    mRefreshBtn->setCommitCallback(boost::bind(&LLFloaterLinkReplace::checkEnableStart, this));

    mSourceEditor = getChild<LLInventoryLinkReplaceDropTarget>("source_uuid_editor");
    mTargetEditor = getChild<LLInventoryLinkReplaceDropTarget>("target_uuid_editor");

    mSourceEditor->setDADCallback(boost::bind(&LLFloaterLinkReplace::onSourceItemDrop, this, _1));
    mTargetEditor->setDADCallback(boost::bind(&LLFloaterLinkReplace::onTargetItemDrop, this, _1));

    mStatusText = getChild<LLTextBox>("status_text");

    return true;
}

void LLFloaterLinkReplace::onOpen(const LLSD& key)
{
    if (key.asUUID().notNull())
    {
        LLUUID item_id = key.asUUID();
        LLViewerInventoryItem* item = gInventory.getItem(item_id);
        mSourceEditor->setItem(item);
        onSourceItemDrop(item->getLinkedUUID());
    }
    else
    {
        checkEnableStart();
    }
}

void LLFloaterLinkReplace::onSourceItemDrop(const LLUUID& source_item_id)
{
    mSourceUUID = source_item_id;
    checkEnableStart();
}

void LLFloaterLinkReplace::onTargetItemDrop(const LLUUID& target_item_id)
{
    mTargetUUID = target_item_id;
    checkEnableStart();
}

void LLFloaterLinkReplace::updateFoundLinks()
{
    LLInventoryModel::item_array_t items;
    LLInventoryModel::cat_array_t cat_array;
    LLLinkedItemIDMatches is_linked_item_match(mSourceUUID);
    gInventory.collectDescendentsIf(gInventory.getRootFolderID(),
                                    cat_array,
                                    items,
                                    LLInventoryModel::INCLUDE_TRASH,
                                    is_linked_item_match);
    mRemainingItems = (U32)items.size();

    LLStringUtil::format_map_t args;
    args["NUM"] = llformat("%d", mRemainingItems);
    mStatusText->setText(getString("ItemsFound", args));
}

void LLFloaterLinkReplace::checkEnableStart()
{
    if (mSourceUUID.notNull() && mTargetUUID.notNull() && mSourceUUID == mTargetUUID)
    {
        mStatusText->setText(getString("ItemsIdentical"));
    }
    else if (mSourceUUID.notNull())
    {
        updateFoundLinks();
    }

    mStartBtn->setEnabled(mRemainingItems > 0 && mSourceUUID.notNull() && mTargetUUID.notNull() && mSourceUUID != mTargetUUID);
}

void LLFloaterLinkReplace::onStartClicked()
{
    LL_INFOS() << "Starting inventory link replace" << LL_ENDL;

    if (mSourceUUID.isNull() || mTargetUUID.isNull())
    {
        LL_WARNS() << "Cannot replace. Either source or target UUID is null." << LL_ENDL;
        return;
    }

    if (mSourceUUID == mTargetUUID)
    {
        LL_WARNS() << "Cannot replace. Source and target are identical." << LL_ENDL;
        return;
    }

    const LLUUID& source_item_id = gInventory.getLinkedItemID(mSourceUUID);
    LLViewerInventoryItem *source_item = gInventory.getItem(source_item_id);
    const LLUUID& target_item_id = gInventory.getLinkedItemID(mTargetUUID);
    LLViewerInventoryItem *target_item = gInventory.getItem(target_item_id);


    LLNotification::Params params("ConfirmReplaceLink");
    params.functor.function(boost::bind(&LLFloaterLinkReplace::onStartClickedResponse, this, _1, _2));
    if (source_item && source_item->isWearableType() && source_item->getWearableType() <= LLWearableType::WT_EYES)
    {
        if(target_item && target_item->isWearableType() && source_item->getWearableType() == target_item->getWearableType())
        {
            LLNotifications::instance().forceResponse(params, 0);
        }
        else
        {
            LLSD args;
            args["TYPE"] = LLWearableType::getInstance()->getTypeName(source_item->getWearableType());
            params.substitutions(args);
            LLNotifications::instance().add(params);
        }
    }
    else
    {
        LLNotifications::instance().forceResponse(params, 0);
    }
}

void LLFloaterLinkReplace::onStartClickedResponse(const LLSD& notification, const LLSD& response)
{

    if (LLNotificationsUtil::getSelectedOption(notification, response) == 0)
    {

        LLInventoryModel::cat_array_t cat_array;
        LLLinkedItemIDMatches is_linked_item_match(mSourceUUID);
        gInventory.collectDescendentsIf(gInventory.getRootFolderID(),
            cat_array,
            mRemainingInventoryItems,
            LLInventoryModel::INCLUDE_TRASH,
            is_linked_item_match);
        LL_INFOS() << "Found " << mRemainingInventoryItems.size() << " inventory links that need to be replaced." << LL_ENDL;

        if (mRemainingInventoryItems.size() > 0)
        {
            LLViewerInventoryItem* target_item = gInventory.getItem(mTargetUUID);
            if (target_item)
            {
                mRemainingItems = (U32)mRemainingInventoryItems.size();

                LLStringUtil::format_map_t args;
                args["NUM"] = llformat("%d", mRemainingItems);
                mStatusText->setText(getString("ItemsRemaining", args));

                mStartBtn->setEnabled(false);
                mRefreshBtn->setEnabled(false);

                mEventTimer.start();
                tick();
            }
            else
            {
                mStatusText->setText(getString("TargetNotFound"));
                LL_WARNS() << "Link replace target not found." << LL_ENDL;
            }
        }
    }
}

// static
void LLFloaterLinkReplace::linkCreatedCallback(LLHandle<LLFloaterLinkReplace> floater_handle, const LLUUID& old_item_id, const LLUUID& target_item_id,
                                                bool needs_wearable_ordering_update, bool needs_description_update, const LLUUID& outfit_folder_id)
{
    LL_DEBUGS() << "Inventory link replace:" << LL_NEWLINE
        << " - old_item_id = " << old_item_id.asString() << LL_NEWLINE
        << " - target_item_id = " << target_item_id.asString() << LL_NEWLINE
        << " - order update = " << (needs_wearable_ordering_update ? "true" : "false") << LL_NEWLINE
        << " - description update = " << (needs_description_update ? "true" : "false") << LL_NEWLINE
        << " - outfit_folder_id = " << outfit_folder_id.asString() << LL_ENDL;

    // If we are replacing an object, bodypart or gesture link within an outfit folder,
    // we need to change the actual description of the link itself. LLAppearanceMgr *should*
    // have created COF links that will be used to save the outfit with an empty description.
    // Since link_inventory_array() will set the description of the linked item for the link
    // itself, this will lead to a dirty outfit state when the outfit with the replaced
    // link is worn. So we have to correct this.
    if (needs_description_update && outfit_folder_id.notNull())
    {
        LLInventoryModel::item_array_t items;
        LLInventoryModel::cat_array_t cats;
        LLLinkedItemIDMatches is_target_link(target_item_id);
        gInventory.collectDescendentsIf(outfit_folder_id,
                                        cats,
                                        items,
                                        LLInventoryModel::EXCLUDE_TRASH,
                                        is_target_link);

        for (LLInventoryModel::item_array_t::iterator it = items.begin(); it != items.end(); ++it)
        {
            LLPointer<LLViewerInventoryItem> item = *it;

            if ((item->getType() == LLAssetType::AT_BODYPART ||
                item->getType() == LLAssetType::AT_OBJECT ||
                item->getType() == LLAssetType::AT_GESTURE)
                && !item->getActualDescription().empty())
            {
                LL_DEBUGS() << "Updating description for " << item->getName() << LL_ENDL;

                LLSD updates;
                updates["desc"] = "";
                update_inventory_item(item->getUUID(), updates, LLPointer<LLInventoryCallback>(NULL));
            }
        }
    }

    LLUUID outfit_update_folder = LLUUID::null;
    if (needs_wearable_ordering_update && outfit_folder_id.notNull())
    {
        // If a wearable item was involved in the link replace operation and replaced
        // a link in an outfit folder, we need to update the clothing ordering information
        // *after* the original link has been removed. LLAppearanceMgr abuses the actual link
        // description to store the clothing ordering information it. We will have to update
        // the clothing ordering information or the outfit will be in dirty state when worn.
        outfit_update_folder = outfit_folder_id;
    }

    LLPointer<LLInventoryCallback> cb = new LLBoostFuncInventoryCallback(boost::bind(&LLFloaterLinkReplace::itemRemovedCallback, floater_handle, outfit_update_folder));
    remove_inventory_object(old_item_id, cb);
}

// static
void LLFloaterLinkReplace::itemRemovedCallback(LLHandle<LLFloaterLinkReplace> floater_handle, const LLUUID& outfit_folder_id)
{
    if (outfit_folder_id.notNull())
    {
        LLAppearanceMgr::getInstance()->updateClothingOrderingInfo(outfit_folder_id);
    }

    if (!floater_handle.isDead())
    {
        floater_handle.get()->decreaseOpenItemCount();
    }
}

void LLFloaterLinkReplace::decreaseOpenItemCount()
{
    mRemainingItems--;

    if (mRemainingItems == 0)
    {
        mStatusText->setText(getString("ReplaceFinished"));
        mStartBtn->setEnabled(true);
        mRefreshBtn->setEnabled(true);
        mEventTimer.stop();
        LL_INFOS() << "Inventory link replace finished." << LL_ENDL;
    }
    else
    {
        LLStringUtil::format_map_t args;
        args["NUM"] = llformat("%d", mRemainingItems);
        mStatusText->setText(getString("ItemsRemaining", args));
        LL_DEBUGS() << "Inventory link replace: " << mRemainingItems << " links remaining..." << LL_ENDL;
    }
}

bool LLFloaterLinkReplace::tick()
{
    LL_DEBUGS() << "Calling tick - remaining items = " << mRemainingInventoryItems.size() << LL_ENDL;

    LLInventoryModel::item_array_t current_batch;

    for (U32 i = 0; i < mBatchSize; ++i)
    {
        if (!mRemainingInventoryItems.size())
        {
            mEventTimer.stop();
            break;
        }

        current_batch.push_back(mRemainingInventoryItems.back());
        mRemainingInventoryItems.pop_back();
    }
    processBatch(current_batch);

    return false;
}

void LLFloaterLinkReplace::processBatch(LLInventoryModel::item_array_t items)
{
    const LLViewerInventoryItem* target_item = gInventory.getItem(mTargetUUID);
    const LLUUID cof_folder_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_CURRENT_OUTFIT);
    const LLUUID outfit_folder_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_MY_OUTFITS);

    for (LLInventoryModel::item_array_t::iterator it = items.begin(); it != items.end(); ++it)
    {
        LLPointer<LLInventoryItem> source_item = *it;

        if (source_item->getParentUUID() != cof_folder_id)
        {
            bool is_outfit_folder = gInventory.isObjectDescendentOf(source_item->getParentUUID(), outfit_folder_id);
            // If either the new or old item in the COF is a wearable, we need to update wearable ordering after the link has been replaced
            bool needs_wearable_ordering_update = (is_outfit_folder && source_item->getType() == LLAssetType::AT_CLOTHING) || target_item->getType() == LLAssetType::AT_CLOTHING;
            // Other items in the COF need a description update (description of the actual link item must be empty)
            bool needs_description_update = is_outfit_folder && target_item->getType() != LLAssetType::AT_CLOTHING;

            LL_DEBUGS() << "is_outfit_folder = " << (is_outfit_folder ? "true" : "false") << LL_NEWLINE
                << "needs_wearable_ordering_update = " << (needs_wearable_ordering_update ? "true" : "false") << LL_NEWLINE
                << "needs_description_update = " << (needs_description_update ? "true" : "false") << LL_ENDL;

            LLInventoryObject::const_object_list_t obj_array;
            obj_array.push_back(LLConstPointer<LLInventoryObject>(target_item));
            LLPointer<LLInventoryCallback> cb = new LLBoostFuncInventoryCallback(boost::bind(&LLFloaterLinkReplace::linkCreatedCallback,
                                                                                                            getDerivedHandle<LLFloaterLinkReplace>(),
                                                                                                            source_item->getUUID(),
                                                                                                            target_item->getUUID(),
                                                                                                            needs_wearable_ordering_update,
                                                                                                            needs_description_update,
                                                                                                            (is_outfit_folder ? source_item->getParentUUID() : LLUUID::null) ));
            link_inventory_array(source_item->getParentUUID(), obj_array, cb);
        }
        else
        {
            decreaseOpenItemCount();
        }
    }
}


//////////////////////////////////////////////////////////////////////////////
// LLInventoryLinkReplaceDropTarget

static LLDefaultChildRegistry::Register<LLInventoryLinkReplaceDropTarget> r("inventory_link_replace_drop_target");

bool LLInventoryLinkReplaceDropTarget::handleDragAndDrop(S32 x, S32 y, MASK mask, bool drop,
                                                           EDragAndDropType cargo_type,
                                                           void* cargo_data,
                                                           EAcceptance* accept,
                                                           std::string& tooltip_msg)
{
    LLInventoryItem* item = (LLInventoryItem*)cargo_data;

    if (cargo_type >= DAD_TEXTURE && cargo_type <= DAD_LINK &&
        item && item->getActualType() != LLAssetType::AT_LINK_FOLDER && item->getType() != LLAssetType::AT_CATEGORY &&
        (
            LLAssetType::lookupCanLink(item->getType()) ||
            (item->getType() == LLAssetType::AT_LINK && !gInventory.getObject(item->getLinkedUUID())) // Broken Link!
        ))
    {
        if (drop)
        {
            setItem(item);
            if (!mDADSignal.empty())
            {
                mDADSignal(mItemID);
            }
        }
        else
        {
            *accept = ACCEPT_YES_SINGLE;
        }
    }
    else
    {
        *accept = ACCEPT_NO;
    }

    return true;
}

void LLInventoryLinkReplaceDropTarget::setItem(LLInventoryItem* item)
{
    if (item)
    {
        mItemID = item->getLinkedUUID();
        setText(item->getName());
    }
    else
    {
        mItemID.setNull();
        setText(LLStringExplicit(""));
    }
}