/** 
 * @file llmaterialeditor.cpp
 * @brief Implementation of the gltf material editor
 *
 * $LicenseInfo:firstyear=2022&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, 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 "llmaterialeditor.h"

#include "llagent.h"
#include "llagentbenefits.h"
#include "llappviewer.h"
#include "llcolorswatch.h"
#include "llcombobox.h"
#include "llfloaterreg.h"
#include "llfilesystem.h"
#include "llgltfmateriallist.h"
#include "llinventorymodel.h"
#include "llinventoryobserver.h"
#include "lllocalgltfmaterials.h"
#include "llnotificationsutil.h"
#include "lltexturectrl.h"
#include "lltrans.h"
#include "llviewermenufile.h"
#include "llviewertexture.h"
#include "llsdutil.h"
#include "llselectmgr.h"
#include "llstatusbar.h"	// can_afford_transaction()
#include "lltoolpie.h"
#include "llviewerinventory.h"
#include "llinventory.h"
#include "llviewerregion.h"
#include "llvovolume.h"
#include "roles_constants.h"
#include "llviewerobjectlist.h"
#include "llsdserialize.h"
#include "llimagej2c.h"
#include "llviewertexturelist.h"
#include "llfloaterperms.h"

#include "tinygltf/tiny_gltf.h"
#include "lltinygltfhelper.h"
#include <strstream>


const std::string MATERIAL_BASE_COLOR_DEFAULT_NAME = "Base Color";
const std::string MATERIAL_NORMAL_DEFAULT_NAME = "Normal";
const std::string MATERIAL_METALLIC_DEFAULT_NAME = "Metallic Roughness";
const std::string MATERIAL_EMISSIVE_DEFAULT_NAME = "Emissive";

// Dirty flags
static const U32 MATERIAL_BASE_COLOR_DIRTY = 0x1 << 0;
static const U32 MATERIAL_BASE_COLOR_TEX_DIRTY = 0x1 << 1;

static const U32 MATERIAL_NORMAL_TEX_DIRTY = 0x1 << 2;

static const U32 MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY = 0x1 << 3;
static const U32 MATERIAL_METALLIC_ROUGHTNESS_METALNESS_DIRTY = 0x1 << 4;
static const U32 MATERIAL_METALLIC_ROUGHTNESS_ROUGHNESS_DIRTY = 0x1 << 5;

static const U32 MATERIAL_EMISIVE_COLOR_DIRTY = 0x1 << 6;
static const U32 MATERIAL_EMISIVE_TEX_DIRTY = 0x1 << 7;

static const U32 MATERIAL_DOUBLE_SIDED_DIRTY = 0x1 << 8;
static const U32 MATERIAL_ALPHA_MODE_DIRTY = 0x1 << 9;
static const U32 MATERIAL_ALPHA_CUTOFF_DIRTY = 0x1 << 10;

LLUUID LLMaterialEditor::mOverrideObjectId;
S32 LLMaterialEditor::mOverrideObjectTE = -1;
bool LLMaterialEditor::mOverrideInProgress = false;
bool LLMaterialEditor::mSelectionNeedsUpdate = true;

LLFloaterComboOptions::LLFloaterComboOptions()
    : LLFloater(LLSD())
{
    buildFromFile("floater_combobox_ok_cancel.xml");
}

LLFloaterComboOptions::~LLFloaterComboOptions()
{

}

BOOL LLFloaterComboOptions::postBuild()
{
    mConfirmButton = getChild<LLButton>("combo_ok", TRUE);
    mCancelButton = getChild<LLButton>("combo_cancel", TRUE);
    mComboOptions = getChild<LLComboBox>("combo_options", TRUE);
    mComboText = getChild<LLTextBox>("combo_text", TRUE);

    mConfirmButton->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) {onConfirm(); });
    mCancelButton->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) {onCancel(); });

    return TRUE;
}

LLFloaterComboOptions* LLFloaterComboOptions::showUI(
    combo_callback callback,
    const std::string &title,
    const std::string &description,
    const std::list<std::string> &options)
{
    LLFloaterComboOptions* combo_picker = new LLFloaterComboOptions();
    if (combo_picker)
    {
        combo_picker->mCallback = callback;
        combo_picker->setTitle(title);

        combo_picker->mComboText->setText(description);

        std::list<std::string>::const_iterator iter = options.begin();
        std::list<std::string>::const_iterator end = options.end();
        for (; iter != end; iter++)
        {
            combo_picker->mComboOptions->addSimpleElement(*iter);
        }
        combo_picker->mComboOptions->selectFirstItem();

        combo_picker->openFloater(LLSD(title));
        combo_picker->setFocus(TRUE);
        combo_picker->center();
    }
    return combo_picker;
}

LLFloaterComboOptions* LLFloaterComboOptions::showUI(
    combo_callback callback,
    const std::string &title,
    const std::string &description,
    const std::string &ok_text,
    const std::string &cancel_text,
    const std::list<std::string> &options)
{
    LLFloaterComboOptions* combo_picker = showUI(callback, title, description, options);
    if (combo_picker)
    {
        combo_picker->mConfirmButton->setLabel(ok_text);
        combo_picker->mCancelButton->setLabel(cancel_text);
    }
    return combo_picker;
}

void LLFloaterComboOptions::onConfirm()
{
    mCallback(mComboOptions->getSimple(), mComboOptions->getCurrentIndex());
    closeFloater();
}

void LLFloaterComboOptions::onCancel()
{
    mCallback(std::string(), -1);
    closeFloater();
}

class LLMaterialEditorCopiedCallback : public LLInventoryCallback
{
public:
    LLMaterialEditorCopiedCallback(
        const std::string &buffer,
        const LLSD &old_key,
        bool has_unsaved_changes)
        : mBuffer(buffer),
          mOldKey(old_key),
          mHasUnsavedChanges(has_unsaved_changes)
    {}

    LLMaterialEditorCopiedCallback(
        const LLSD &old_key,
        const std::string &new_name)
        : mOldKey(old_key),
          mNewName(new_name),
          mHasUnsavedChanges(false)
    {}

    virtual void fire(const LLUUID& inv_item_id)
    {
        if (!mNewName.empty())
        {
            // making a copy from a notecard doesn't change name, do it now
            LLViewerInventoryItem* item = gInventory.getItem(inv_item_id);
            if (item->getName() != mNewName)
            {
                LLSD updates;
                updates["name"] = mNewName;
                update_inventory_item(inv_item_id, updates, NULL);
            }
        }
        LLMaterialEditor::finishSaveAs(mOldKey, inv_item_id, mBuffer, mHasUnsavedChanges);
    }

private:
    std::string mBuffer;
    LLSD mOldKey;
    std::string mNewName;
    bool mHasUnsavedChanges;
};

///----------------------------------------------------------------------------
/// Class LLSelectedTEGetMatData
/// For finding selected applicable inworld material
///----------------------------------------------------------------------------

struct LLSelectedTEGetMatData : public LLSelectedTEFunctor
{
    LLSelectedTEGetMatData(bool for_override);

    bool apply(LLViewerObject* objectp, S32 te_index);

    bool mIsOverride;
    bool mIdenticalTexColor;
    bool mIdenticalTexMetal;
    bool mIdenticalTexEmissive;
    bool mIdenticalTexNormal;
    bool mFirst;
    LLUUID mTexColorId;
    LLUUID mTexMetalId;
    LLUUID mTexEmissiveId;
    LLUUID mTexNormalId;
    LLUUID mObjectId;
    LLViewerObject* mObject = nullptr;
    S32 mObjectTE;
    LLUUID mMaterialId;
    LLPointer<LLGLTFMaterial> mMaterial;
    LLPointer<LLLocalGLTFMaterial> mLocalMaterial;
};

LLSelectedTEGetMatData::LLSelectedTEGetMatData(bool for_override)
    : mIsOverride(for_override)
    , mIdenticalTexColor(true)
    , mIdenticalTexMetal(true)
    , mIdenticalTexEmissive(true)
    , mIdenticalTexNormal(true)
    , mObjectTE(-1)
    , mFirst(true)
{}

bool LLSelectedTEGetMatData::apply(LLViewerObject* objectp, S32 te_index)
{
    if (!objectp)
    {
        return false;
    }
    LLUUID mat_id = objectp->getRenderMaterialID(te_index);
    mMaterialId = mat_id;
    bool can_use = mIsOverride ? objectp->permModify() : objectp->permCopy();
    LLTextureEntry *tep = objectp->getTE(te_index);
    // We might want to disable this entirely if at least
    // something in selection is no-copy or no modify
    // or has no base material
    if (can_use && tep && mat_id.notNull())
    {
        if (mIsOverride)
        {
            LLPointer<LLGLTFMaterial> mat = tep->getGLTFRenderMaterial();

            LLUUID tex_color_id;
            LLUUID tex_metal_id;
            LLUUID tex_emissive_id;
            LLUUID tex_normal_id;
            llassert(mat.notNull()); // by this point shouldn't be null
            if (mat.notNull())
            {
                tex_color_id = mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR];
                tex_metal_id = mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS];
                tex_emissive_id = mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE];
                tex_normal_id = mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL];
            }
            if (mFirst)
            {
                mMaterial = mat;
                mTexColorId = tex_color_id;
                mTexMetalId = tex_metal_id;
                mTexEmissiveId = tex_emissive_id;
                mTexNormalId = tex_normal_id;
                mObjectTE = te_index;
                mObject = objectp;
                mObjectId = objectp->getID();
                mFirst = false;
            }
            else
            {
                if (mTexColorId != tex_color_id)
                {
                    mIdenticalTexColor = false;
                }
                if (mTexMetalId != tex_metal_id)
                {
                    mIdenticalTexMetal = false;
                }
                if (mTexEmissiveId != tex_emissive_id)
                {
                    mIdenticalTexEmissive = false;
                }
                if (mTexNormalId != tex_normal_id)
                {
                    mIdenticalTexNormal = false;
                }
            }
        }
        else
        {
            LLGLTFMaterial *mat = tep->getGLTFMaterial();
            LLLocalGLTFMaterial *local_mat = dynamic_cast<LLLocalGLTFMaterial*>(mat);

            mObject = objectp;
            mObjectId = objectp->getID();
            if (local_mat)
            {
                mLocalMaterial = local_mat;
            }
            mMaterial = tep->getGLTFRenderMaterial();

            if (mMaterial.isNull())
            {
                // Shouldn't be possible?
                LL_WARNS("MaterialEditor") << "Object has material id, but no material" << LL_ENDL;
                mMaterial = gGLTFMaterialList.getMaterial(mat_id);
            }
        }
        return true;
    }
    return false;
}

class LLSelectedTEUpdateOverrides: public LLSelectedNodeFunctor
{
public:
    LLSelectedTEUpdateOverrides(LLMaterialEditor* me) : mEditor(me) {}

    virtual bool apply(LLSelectNode* nodep);

    LLMaterialEditor* mEditor;
};

bool LLSelectedTEUpdateOverrides::apply(LLSelectNode* nodep)
{
    LLViewerObject* objectp = nodep->getObject();
    if (!objectp)
    {
        return false;
    }
    S32 num_tes = llmin((S32)objectp->getNumTEs(), (S32)objectp->getNumFaces()); // avatars have TEs but no faces
    for (S32 te_index = 0; te_index < num_tes; ++te_index)
    {

        LLTextureEntry* tep = objectp->getTE(te_index);
        LLGLTFMaterial* override_mat = tep->getGLTFMaterialOverride();
        if (mEditor->updateMaterialLocalSubscription(override_mat))
        {
            LLGLTFMaterial* render_mat = tep->getGLTFRenderMaterial();
            mEditor->updateMaterialLocalSubscription(render_mat);
        }
    }

    return true;
}

///----------------------------------------------------------------------------
/// Class LLMaterialEditor
///----------------------------------------------------------------------------

// Default constructor
LLMaterialEditor::LLMaterialEditor(const LLSD& key)
    : LLPreview(key)
    , mUnsavedChanges(0)
    , mRevertedChanges(0)
    , mExpectedUploadCost(0)
    , mUploadingTexturesCount(0)
    , mUploadingTexturesFailure(false)
{
    const LLInventoryItem* item = getItem();
    if (item)
    {
        mAssetID = item->getAssetUUID();
    }
}

LLMaterialEditor::~LLMaterialEditor()
{
}

void LLMaterialEditor::setObjectID(const LLUUID& object_id)
{
    LLPreview::setObjectID(object_id);
    const LLInventoryItem* item = getItem();
    if (item)
    {
        mAssetID = item->getAssetUUID();
    }
}

void LLMaterialEditor::setAuxItem(const LLInventoryItem* item)
{
    LLPreview::setAuxItem(item);
    if (item)
    {
        mAssetID = item->getAssetUUID();
    }
}

BOOL LLMaterialEditor::postBuild()
{
    // if this is a 'live editor' instance, it is also
    // single instance and uses live overrides
    mIsOverride = getIsSingleInstance();

    mBaseColorTextureCtrl = getChild<LLTextureCtrl>("base_color_texture");
    mMetallicTextureCtrl = getChild<LLTextureCtrl>("metallic_roughness_texture");
    mEmissiveTextureCtrl = getChild<LLTextureCtrl>("emissive_texture");
    mNormalTextureCtrl = getChild<LLTextureCtrl>("normal_texture");
    mBaseColorCtrl = getChild<LLColorSwatchCtrl>("base color");
    mEmissiveColorCtrl = getChild<LLColorSwatchCtrl>("emissive color");

    if (!gAgent.isGodlike())
    {
        // Only allow fully permissive textures
        mBaseColorTextureCtrl->setImmediateFilterPermMask(PERM_COPY | PERM_TRANSFER);
        mMetallicTextureCtrl->setImmediateFilterPermMask(PERM_COPY | PERM_TRANSFER);
        mEmissiveTextureCtrl->setImmediateFilterPermMask(PERM_COPY | PERM_TRANSFER);
        mNormalTextureCtrl->setImmediateFilterPermMask(PERM_COPY | PERM_TRANSFER);
    }

    // Texture callback
    mBaseColorTextureCtrl->setCommitCallback(boost::bind(&LLMaterialEditor::onCommitTexture, this, _1, _2, MATERIAL_BASE_COLOR_TEX_DIRTY));
    mMetallicTextureCtrl->setCommitCallback(boost::bind(&LLMaterialEditor::onCommitTexture, this, _1, _2, MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY));
    mEmissiveTextureCtrl->setCommitCallback(boost::bind(&LLMaterialEditor::onCommitTexture, this, _1, _2, MATERIAL_EMISIVE_TEX_DIRTY));
    mNormalTextureCtrl->setCommitCallback(boost::bind(&LLMaterialEditor::onCommitTexture, this, _1, _2, MATERIAL_NORMAL_TEX_DIRTY));

    if (mIsOverride)
    {
        // Live editing needs a recovery mechanism on cancel
        mBaseColorTextureCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_BASE_COLOR_TEX_DIRTY));
        mMetallicTextureCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY));
        mEmissiveTextureCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_EMISIVE_TEX_DIRTY));
        mNormalTextureCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_NORMAL_TEX_DIRTY));

        // Save applied changes on 'OK' to our recovery mechanism.
        mBaseColorTextureCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_BASE_COLOR_TEX_DIRTY));
        mMetallicTextureCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY));
        mEmissiveTextureCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_EMISIVE_TEX_DIRTY));
        mNormalTextureCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_NORMAL_TEX_DIRTY));
    }
    else
    {
        mBaseColorTextureCtrl->setCanApplyImmediately(false);
        mMetallicTextureCtrl->setCanApplyImmediately(false);
        mEmissiveTextureCtrl->setCanApplyImmediately(false);
        mNormalTextureCtrl->setCanApplyImmediately(false);
    }

    if (!mIsOverride)
    {
        childSetAction("save", boost::bind(&LLMaterialEditor::onClickSave, this));
        childSetAction("save_as", boost::bind(&LLMaterialEditor::onClickSaveAs, this));
        childSetAction("cancel", boost::bind(&LLMaterialEditor::onClickCancel, this));
    }

    if (mIsOverride)
    {
        childSetVisible("base_color_upload_fee", FALSE);
        childSetVisible("metallic_upload_fee", FALSE);
        childSetVisible("emissive_upload_fee", FALSE);
        childSetVisible("normal_upload_fee", FALSE);
    }
    else
    {
        S32 upload_cost = LLAgentBenefitsMgr::current().getTextureUploadCost();
        getChild<LLUICtrl>("base_color_upload_fee")->setTextArg("[FEE]", llformat("%d", upload_cost));
        getChild<LLUICtrl>("metallic_upload_fee")->setTextArg("[FEE]", llformat("%d", upload_cost));
        getChild<LLUICtrl>("emissive_upload_fee")->setTextArg("[FEE]", llformat("%d", upload_cost));
        getChild<LLUICtrl>("normal_upload_fee")->setTextArg("[FEE]", llformat("%d", upload_cost));
    }

    boost::function<void(LLUICtrl*, void*)> changes_callback = [this](LLUICtrl * ctrl, void* userData)
    {
        const U32 *flag = (const U32*)userData;
        markChangesUnsaved(*flag);
        // Apply changes to object live
        applyToSelection();
    };
 
    childSetCommitCallback("double sided", changes_callback, (void*)&MATERIAL_DOUBLE_SIDED_DIRTY);

    // BaseColor
    mBaseColorCtrl->setCommitCallback(changes_callback, (void*)&MATERIAL_BASE_COLOR_DIRTY);
    if (mIsOverride)
    {
        mBaseColorCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_BASE_COLOR_DIRTY));
        mBaseColorCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_BASE_COLOR_DIRTY));
    }
    else
    {
        mBaseColorCtrl->setCanApplyImmediately(false);
    }
    // transparency is a part of base color
    childSetCommitCallback("transparency", changes_callback, (void*)&MATERIAL_BASE_COLOR_DIRTY);
    childSetCommitCallback("alpha mode", changes_callback, (void*)&MATERIAL_ALPHA_MODE_DIRTY);
    childSetCommitCallback("alpha cutoff", changes_callback, (void*)&MATERIAL_ALPHA_CUTOFF_DIRTY);

    // Metallic-Roughness
    childSetCommitCallback("metalness factor", changes_callback, (void*)&MATERIAL_METALLIC_ROUGHTNESS_METALNESS_DIRTY);
    childSetCommitCallback("roughness factor", changes_callback, (void*)&MATERIAL_METALLIC_ROUGHTNESS_ROUGHNESS_DIRTY);

    // Emissive
    mEmissiveColorCtrl->setCommitCallback(changes_callback, (void*)&MATERIAL_EMISIVE_COLOR_DIRTY);
    if (mIsOverride)
    {
        mEmissiveColorCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_EMISIVE_COLOR_DIRTY));
        mEmissiveColorCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_EMISIVE_COLOR_DIRTY));
    }
    else
    {
        mEmissiveColorCtrl->setCanApplyImmediately(false);
    }

    if (!mIsOverride)
    {
        // "unsaved_changes" doesn't exist in live editor
        childSetVisible("unsaved_changes", mUnsavedChanges);

        // Doesn't exist in live editor
        getChild<LLUICtrl>("total_upload_fee")->setTextArg("[FEE]", llformat("%d", 0));
    }

    // Todo:
    // Disable/enable setCanApplyImmediately() based on
    // working from inventory, upload or editing inworld

	return LLPreview::postBuild();
}

void LLMaterialEditor::onClickCloseBtn(bool app_quitting)
{
    if (app_quitting || mIsOverride)
    {
        closeFloater(app_quitting);
    }
    else
    {
        onClickCancel();
    }
}

void LLMaterialEditor::onClose(bool app_quitting)
{
    if (mSelectionUpdateSlot.connected())
    {
        mSelectionUpdateSlot.disconnect();
    }
    for (mat_connection_map_t::value_type &cn : mTextureChangesUpdates)
    {
        cn.second.mConnection.disconnect();
    }
    mTextureChangesUpdates.clear();

    LLPreview::onClose(app_quitting);
}

void LLMaterialEditor::draw()
{
    if (mIsOverride)
    {
        if (mSelectionNeedsUpdate)
        {
            mSelectionNeedsUpdate = false;
            clearTextures();
            setFromSelection();
        }
    }
    LLPreview::draw();
}

void LLMaterialEditor::handleReshape(const LLRect& new_rect, bool by_user)
{
    if (by_user)
    {
        const LLRect old_rect = getRect();
        LLRect clamp_rect(new_rect);
        clamp_rect.mRight = clamp_rect.mLeft + old_rect.getWidth();
        LLPreview::handleReshape(clamp_rect, by_user);
    }
    else
    {
        LLPreview::handleReshape(new_rect, by_user);
    }
}

LLUUID LLMaterialEditor::getBaseColorId()
{
    return mBaseColorTextureCtrl->getValue().asUUID();
}

void LLMaterialEditor::setBaseColorId(const LLUUID& id)
{
    mBaseColorTextureCtrl->setValue(id);
    mBaseColorTextureCtrl->setDefaultImageAssetID(id);
    mBaseColorTextureCtrl->setTentative(FALSE);
}

void LLMaterialEditor::setBaseColorUploadId(const LLUUID& id)
{
    // Might be better to use local textures and
    // assign a fee in case of a local texture
    if (id.notNull())
    {
        // todo: this does not account for posibility of texture
        // being from inventory, need to check that
        childSetValue("base_color_upload_fee", getString("upload_fee_string"));
        // Only set if we will need to upload this texture
        mBaseColorTextureUploadId = id;
    }
    markChangesUnsaved(MATERIAL_BASE_COLOR_TEX_DIRTY);
}

LLColor4 LLMaterialEditor::getBaseColor()
{
    LLColor4 ret = linearColor4(LLColor4(mBaseColorCtrl->getValue()));
    ret.mV[3] = getTransparency();
    return ret;
}

void LLMaterialEditor::setBaseColor(const LLColor4& color)
{
    mBaseColorCtrl->setValue(srgbColor4(color).getValue());
    setTransparency(color.mV[3]);
}

F32 LLMaterialEditor::getTransparency()
{
    return childGetValue("transparency").asReal();
}

void LLMaterialEditor::setTransparency(F32 transparency)
{
    childSetValue("transparency", transparency);
}

std::string LLMaterialEditor::getAlphaMode()
{
    return childGetValue("alpha mode").asString();
}

void LLMaterialEditor::setAlphaMode(const std::string& alpha_mode)
{
    childSetValue("alpha mode", alpha_mode);
}

F32 LLMaterialEditor::getAlphaCutoff()
{
    return childGetValue("alpha cutoff").asReal();
}

void LLMaterialEditor::setAlphaCutoff(F32 alpha_cutoff)
{
    childSetValue("alpha cutoff", alpha_cutoff);
}

void LLMaterialEditor::setMaterialName(const std::string &name)
{
    setTitle(name);
    mMaterialName = name;
}

LLUUID LLMaterialEditor::getMetallicRoughnessId()
{
    return mMetallicTextureCtrl->getValue().asUUID();
}

void LLMaterialEditor::setMetallicRoughnessId(const LLUUID& id)
{
    mMetallicTextureCtrl->setValue(id);
    mMetallicTextureCtrl->setDefaultImageAssetID(id);
    mMetallicTextureCtrl->setTentative(FALSE);
}

void LLMaterialEditor::setMetallicRoughnessUploadId(const LLUUID& id)
{
    if (id.notNull())
    {
        // todo: this does not account for posibility of texture
        // being from inventory, need to check that
        childSetValue("metallic_upload_fee", getString("upload_fee_string"));
        mMetallicTextureUploadId = id;
    }
    markChangesUnsaved(MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY);
}

F32 LLMaterialEditor::getMetalnessFactor()
{
    return childGetValue("metalness factor").asReal();
}

void LLMaterialEditor::setMetalnessFactor(F32 factor)
{
    childSetValue("metalness factor", factor);
}

F32 LLMaterialEditor::getRoughnessFactor()
{
    return childGetValue("roughness factor").asReal();
}

void LLMaterialEditor::setRoughnessFactor(F32 factor)
{
    childSetValue("roughness factor", factor);
}

LLUUID LLMaterialEditor::getEmissiveId()
{
    return mEmissiveTextureCtrl->getValue().asUUID();
}

void LLMaterialEditor::setEmissiveId(const LLUUID& id)
{
    mEmissiveTextureCtrl->setValue(id);
    mEmissiveTextureCtrl->setDefaultImageAssetID(id);
    mEmissiveTextureCtrl->setTentative(FALSE);
}

void LLMaterialEditor::setEmissiveUploadId(const LLUUID& id)
{
    if (id.notNull())
    {
        // todo: this does not account for posibility of texture
        // being from inventory, need to check that
        childSetValue("emissive_upload_fee", getString("upload_fee_string"));
        mEmissiveTextureUploadId = id;
    }
    markChangesUnsaved(MATERIAL_EMISIVE_TEX_DIRTY);
}

LLColor4 LLMaterialEditor::getEmissiveColor()
{
    return linearColor4(LLColor4(mEmissiveColorCtrl->getValue()));
}

void LLMaterialEditor::setEmissiveColor(const LLColor4& color)
{
    mEmissiveColorCtrl->setValue(srgbColor4(color).getValue());
}

LLUUID LLMaterialEditor::getNormalId()
{
    return mNormalTextureCtrl->getValue().asUUID();
}

void LLMaterialEditor::setNormalId(const LLUUID& id)
{
    mNormalTextureCtrl->setValue(id);
    mNormalTextureCtrl->setDefaultImageAssetID(id);
    mNormalTextureCtrl->setTentative(FALSE);
}

void LLMaterialEditor::setNormalUploadId(const LLUUID& id)
{
    if (id.notNull())
    {
        // todo: this does not account for posibility of texture
        // being from inventory, need to check that
        childSetValue("normal_upload_fee", getString("upload_fee_string"));
        mNormalTextureUploadId = id;
    }
    markChangesUnsaved(MATERIAL_NORMAL_TEX_DIRTY);
}

bool LLMaterialEditor::getDoubleSided()
{
    return childGetValue("double sided").asBoolean();
}

void LLMaterialEditor::setDoubleSided(bool double_sided)
{
    childSetValue("double sided", double_sided);
}

void LLMaterialEditor::resetUnsavedChanges()
{
    mUnsavedChanges = 0;
    mRevertedChanges = 0;
    if (!mIsOverride)
    {
        childSetVisible("unsaved_changes", false);
        setCanSave(false);

        mExpectedUploadCost = 0;
        getChild<LLUICtrl>("total_upload_fee")->setTextArg("[FEE]", llformat("%d", mExpectedUploadCost));
    }
}

void LLMaterialEditor::markChangesUnsaved(U32 dirty_flag)
{
    mUnsavedChanges |= dirty_flag;
    if (mIsOverride)
    {
        // at the moment live editing (mIsOverride) applies everything 'live'
        // and "unsaved_changes", save/cancel buttons don't exist there
        return;
    }

    childSetVisible("unsaved_changes", mUnsavedChanges);

    if (mUnsavedChanges)
    {
        const LLInventoryItem* item = getItem();
        if (item)
        {
            //LLPermissions perm(item->getPermissions());
            bool allow_modify = canModify(mObjectUUID, item);
            bool source_library = mObjectUUID.isNull() && gInventory.isObjectDescendentOf(mItemUUID, gInventory.getLibraryRootFolderID());
            bool source_notecard = mNotecardInventoryID.notNull();

            setCanSave(allow_modify && !source_library && !source_notecard);
        }
    }
    else
    {
        setCanSave(false);
    }

    S32 upload_texture_count = 0;
    if (mBaseColorTextureUploadId.notNull() && mBaseColorTextureUploadId == getBaseColorId())
    {
        upload_texture_count++;
    }
    if (mMetallicTextureUploadId.notNull() && mMetallicTextureUploadId == getMetallicRoughnessId())
    {
        upload_texture_count++;
    }
    if (mEmissiveTextureUploadId.notNull() && mEmissiveTextureUploadId == getEmissiveId())
    {
        upload_texture_count++;
    }
    if (mNormalTextureUploadId.notNull() && mNormalTextureUploadId == getNormalId())
    {
        upload_texture_count++;
    }

    mExpectedUploadCost = upload_texture_count * LLAgentBenefitsMgr::current().getTextureUploadCost();
    getChild<LLUICtrl>("total_upload_fee")->setTextArg("[FEE]", llformat("%d", mExpectedUploadCost));
}

void LLMaterialEditor::setCanSaveAs(bool value)
{
    if (!mIsOverride)
    {
        childSetEnabled("save_as", value);
    }
}

void LLMaterialEditor::setCanSave(bool value)
{
    if (!mIsOverride)
    {
        childSetEnabled("save", value);
    }
}

void LLMaterialEditor::setEnableEditing(bool can_modify)
{
    childSetEnabled("double sided", can_modify);

    // BaseColor
    childSetEnabled("base color", can_modify);
    childSetEnabled("transparency", can_modify);
    childSetEnabled("alpha mode", can_modify);
    childSetEnabled("alpha cutoff", can_modify);

    // Metallic-Roughness
    childSetEnabled("metalness factor", can_modify);
    childSetEnabled("roughness factor", can_modify);

    // Metallic-Roughness
    childSetEnabled("metalness factor", can_modify);
    childSetEnabled("roughness factor", can_modify);

    // Emissive
    childSetEnabled("emissive color", can_modify);

    mBaseColorTextureCtrl->setEnabled(can_modify);
    mMetallicTextureCtrl->setEnabled(can_modify);
    mEmissiveTextureCtrl->setEnabled(can_modify);
    mNormalTextureCtrl->setEnabled(can_modify);
}

void LLMaterialEditor::subscribeToLocalTexture(S32 dirty_flag, const LLUUID& tracking_id)
{
    if (mTextureChangesUpdates[dirty_flag].mTrackingId != tracking_id)
    {
        mTextureChangesUpdates[dirty_flag].mConnection.disconnect();
        mTextureChangesUpdates[dirty_flag].mTrackingId = tracking_id;
        mTextureChangesUpdates[dirty_flag].mConnection = LLLocalBitmapMgr::getInstance()->setOnChangedCallback(tracking_id,
                                                                                                               [this, dirty_flag](const LLUUID& tracking_id, const LLUUID& old_id, const LLUUID& new_id)
                                                                                                               {
                                                                                                                   if (new_id.isNull())
                                                                                                                   {
                                                                                                                       mTextureChangesUpdates[dirty_flag].mConnection.disconnect();
                                                                                                                       //mTextureChangesUpdates.erase(dirty_flag);
                                                                                                                   }
                                                                                                                   else
                                                                                                                   {
                                                                                                                       replaceLocalTexture(old_id, new_id);
                                                                                                                   }
                                                                                                               });
    }
}

LLUUID LLMaterialEditor::getLocalTextureTrackingIdFromFlag(U32 flag)
{
    mat_connection_map_t::iterator found = mTextureChangesUpdates.find(flag);
    if (found != mTextureChangesUpdates.end())
    {
        return found->second.mTrackingId;
    }
    return LLUUID();
}

bool LLMaterialEditor::updateMaterialLocalSubscription(LLGLTFMaterial* mat)
{
    if (!mat)
    {
        return false;
    }

    bool res = false;
    for (mat_connection_map_t::value_type& cn : mTextureChangesUpdates)
    {
        LLUUID world_id = LLLocalBitmapMgr::getInstance()->getWorldID(cn.second.mTrackingId);
        if (world_id == mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR])
        {
            LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(cn.second.mTrackingId, mat);
            res = true;
            continue;
        }
        if (world_id == mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS])
        {
            LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(cn.second.mTrackingId, mat);
            res = true;
            continue;
        }
        if (world_id == mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE])
        {
            LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(cn.second.mTrackingId, mat);
            res = true;
            continue;
        }
        if (world_id == mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL])
        {
            LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(cn.second.mTrackingId, mat);
            res = true;
            continue;
        }
    }
    return res;
}

void LLMaterialEditor::replaceLocalTexture(const LLUUID& old_id, const LLUUID& new_id)
{
    // todo: might be a good idea to set mBaseColorTextureUploadId here
    // and when texturectrl picks a local texture
    if (getBaseColorId() == old_id)
    {
        mBaseColorTextureCtrl->setValue(new_id);
    }
    if (mBaseColorTextureCtrl->getDefaultImageAssetID() == old_id)
    {
        mBaseColorTextureCtrl->setDefaultImageAssetID(new_id);
    }

    if (getMetallicRoughnessId() == old_id)
    {
        mMetallicTextureCtrl->setValue(new_id);
    }
    if (mMetallicTextureCtrl->getDefaultImageAssetID() == old_id)
    {
        mMetallicTextureCtrl->setDefaultImageAssetID(new_id);
    }

    if (getEmissiveId() == old_id)
    {
        mEmissiveTextureCtrl->setValue(new_id);
    }
    if (mEmissiveTextureCtrl->getDefaultImageAssetID() == old_id)
    {
        mEmissiveTextureCtrl->setDefaultImageAssetID(new_id);
    }

    if (getNormalId() == old_id)
    {
        mNormalTextureCtrl->setValue(new_id);
    }
    if (mNormalTextureCtrl->getDefaultImageAssetID() == old_id)
    {
        mNormalTextureCtrl->setDefaultImageAssetID(new_id);
    }
}

void LLMaterialEditor::onCommitTexture(LLUICtrl* ctrl, const LLSD& data, S32 dirty_flag)
{
    if (!mIsOverride)
    {
        std::string upload_fee_ctrl_name;
        LLUUID old_uuid;

        switch (dirty_flag)
        {
        case MATERIAL_BASE_COLOR_TEX_DIRTY:
        {
            upload_fee_ctrl_name = "base_color_upload_fee";
            old_uuid = mBaseColorTextureUploadId;
            break;
        }
        case MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY:
        {
            upload_fee_ctrl_name = "metallic_upload_fee";
            old_uuid = mMetallicTextureUploadId;
            break;
        }
        case MATERIAL_EMISIVE_TEX_DIRTY:
        {
            upload_fee_ctrl_name = "emissive_upload_fee";
            old_uuid = mEmissiveTextureUploadId;
            break;
        }
        case MATERIAL_NORMAL_TEX_DIRTY:
        {
            upload_fee_ctrl_name = "normal_upload_fee";
            old_uuid = mNormalTextureUploadId;
            break;
        }
        default:
            break;
        }
        LLUUID new_val = ctrl->getValue().asUUID();
        if (new_val == old_uuid && old_uuid.notNull())
        {
            childSetValue(upload_fee_ctrl_name, getString("upload_fee_string"));
        }
        else
        {
            // Texture picker has 'apply now' with 'cancel' support.
            // Don't clean mBaseColorJ2C and mBaseColorFetched, it's our
            // storage in case user decides to cancel changes.
            // Without mBaseColorFetched, viewer will eventually cleanup
            // the texture that is not in use
            childSetValue(upload_fee_ctrl_name, getString("no_upload_fee_string"));
        }
    }

    LLTextureCtrl* tex_ctrl = (LLTextureCtrl*)ctrl;
    if (tex_ctrl->isImageLocal())
    {
        subscribeToLocalTexture(dirty_flag, tex_ctrl->getLocalTrackingID());
    }
    else
    {
        // unsubcribe potential old callabck
        mat_connection_map_t::iterator found = mTextureChangesUpdates.find(dirty_flag);
        if (found != mTextureChangesUpdates.end())
        {
            found->second.mConnection.disconnect();
        }
    }

    markChangesUnsaved(dirty_flag);
    applyToSelection();
}

void LLMaterialEditor::onCancelCtrl(LLUICtrl* ctrl, const LLSD& data, S32 dirty_flag)
{
    mRevertedChanges |= dirty_flag;
    applyToSelection();
}

void update_local_texture(LLUICtrl* ctrl, LLGLTFMaterial* mat)
{
    LLTextureCtrl* tex_ctrl = (LLTextureCtrl*)ctrl;
    if (tex_ctrl->isImageLocal())
    {
        // subscrive material to updates of local textures
        LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(tex_ctrl->getLocalTrackingID(), mat);
    }
}

void LLMaterialEditor::onSelectCtrl(LLUICtrl* ctrl, const LLSD& data, S32 dirty_flag)
{
    mUnsavedChanges |= dirty_flag;
    applyToSelection();

    struct f : public LLSelectedNodeFunctor
    {
        f(LLUICtrl* ctrl, S32 dirty_flag) : mCtrl(ctrl), mDirtyFlag(dirty_flag)
        {
        }

        virtual bool apply(LLSelectNode* nodep)
        {
            LLViewerObject* objectp = nodep->getObject();
            if (!objectp)
            {
                return false;
            }
            S32 num_tes = llmin((S32)objectp->getNumTEs(), (S32)objectp->getNumFaces()); // avatars have TEs but no faces
            for (S32 te = 0; te < num_tes; ++te)
            {
                if (nodep->isTESelected(te) && nodep->mSavedGLTFOverrideMaterials.size() > te)
                {
                    if (nodep->mSavedGLTFOverrideMaterials[te].isNull())
                    {
                        // populate with default values, default values basically mean 'not in use'
                        nodep->mSavedGLTFOverrideMaterials[te] = new LLGLTFMaterial();
                    }

                    switch (mDirtyFlag)
                    {
                    //Textures
                    case MATERIAL_BASE_COLOR_TEX_DIRTY:
                    {
                        nodep->mSavedGLTFOverrideMaterials[te]->setBaseColorId(mCtrl->getValue().asUUID(), true);
                        update_local_texture(mCtrl, nodep->mSavedGLTFOverrideMaterials[te].get());
                        break;
                    }
                    case MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY:
                    {
                        nodep->mSavedGLTFOverrideMaterials[te]->setOcclusionRoughnessMetallicId(mCtrl->getValue().asUUID(), true);
                        update_local_texture(mCtrl, nodep->mSavedGLTFOverrideMaterials[te].get());
                        break;
                    }
                    case MATERIAL_EMISIVE_TEX_DIRTY:
                    {
                        nodep->mSavedGLTFOverrideMaterials[te]->setEmissiveId(mCtrl->getValue().asUUID(), true);
                        update_local_texture(mCtrl, nodep->mSavedGLTFOverrideMaterials[te].get());
                        break;
                    }
                    case MATERIAL_NORMAL_TEX_DIRTY:
                    {
                        nodep->mSavedGLTFOverrideMaterials[te]->setNormalId(mCtrl->getValue().asUUID(), true);
                        update_local_texture(mCtrl, nodep->mSavedGLTFOverrideMaterials[te].get());
                        break;
                    }
                    // Colors
                    case MATERIAL_BASE_COLOR_DIRTY:
                    {
                        LLColor4 ret = linearColor4(LLColor4(mCtrl->getValue()));
                        // except transparency
                        ret.mV[3] = nodep->mSavedGLTFOverrideMaterials[te]->mBaseColor.mV[3];
                        nodep->mSavedGLTFOverrideMaterials[te]->setBaseColorFactor(ret, true);
                        break;
                    }
                    case MATERIAL_EMISIVE_COLOR_DIRTY:
                    {
                        nodep->mSavedGLTFOverrideMaterials[te]->setEmissiveColorFactor(LLColor3(mCtrl->getValue()), true);
                        break;
                    }
                    default:
                        break;
                    }
                }
            }
            return true;
        }

        LLUICtrl* mCtrl;
        S32 mDirtyFlag;
    } func(ctrl, dirty_flag);

    LLSelectMgr::getInstance()->getSelection()->applyToNodes(&func);
}

void LLMaterialEditor::onClickSave()
{
    if (!capabilitiesAvailable())
    {
        LLNotificationsUtil::add("MissingMaterialCaps");
        return;
    }
    if (!can_afford_transaction(mExpectedUploadCost))
    {
        LLSD args;
        args["COST"] = llformat("%d", mExpectedUploadCost);
        LLNotificationsUtil::add("ErrorCannotAffordUpload", args);
        return;
    }

    applyToSelection();
    saveIfNeeded();
}

std::string LLMaterialEditor::getEncodedAsset()
{
    LLSD asset;
    asset["version"] = LLGLTFMaterial::ASSET_VERSION;
    asset["type"] = LLGLTFMaterial::ASSET_TYPE;
    LLGLTFMaterial mat;
    getGLTFMaterial(&mat);
    asset["data"] = mat.asJSON();

    std::ostringstream str;
    LLSDSerialize::serialize(asset, str, LLSDSerialize::LLSD_BINARY);

    return str.str();
}

bool LLMaterialEditor::decodeAsset(const std::vector<char>& buffer)
{
    LLSD asset;
    
    std::istrstream str(&buffer[0], buffer.size());
    if (LLSDSerialize::deserialize(asset, str, buffer.size()))
    {
        if (asset.has("version") && LLGLTFMaterial::isAcceptedVersion(asset["version"].asString()))
        {
            if (asset.has("type") && asset["type"] == LLGLTFMaterial::ASSET_TYPE)
            {
                if (asset.has("data") && asset["data"].isString())
                {
                    std::string data = asset["data"];

                    tinygltf::TinyGLTF gltf;
                    tinygltf::TinyGLTF loader;
                    std::string        error_msg;
                    std::string        warn_msg;

                    tinygltf::Model model_in;

                    if (loader.LoadASCIIFromString(&model_in, &error_msg, &warn_msg, data.c_str(), data.length(), ""))
                    {
                        // assets are only supposed to have one item
                        // *NOTE: This duplicates some functionality from
                        // LLGLTFMaterial::fromJSON, but currently does the job
                        // better for the material editor use case.
                        // However, LLGLTFMaterial::asJSON should always be
                        // used when uploading materials, to ensure the
                        // asset is valid.
                        return setFromGltfModel(model_in, 0, true);
                    }
                    else
                    {
                        LL_WARNS("MaterialEditor") << "Floater " << getKey() << " Failed to decode material asset: " << LL_NEWLINE
                         << warn_msg << LL_NEWLINE
                         << error_msg << LL_ENDL;
                    }
                }
            }
        }
        else
        {
            LL_WARNS("MaterialEditor") << "Invalid LLSD content "<< asset << " for flaoter " << getKey() << LL_ENDL;
        }
    }
    else
    {
        LL_WARNS("MaterialEditor") << "Failed to deserialize material LLSD for flaoter " << getKey() << LL_ENDL;
    }

    return false;
}

/**
 * Build a description of the material we just imported.
 * Currently this means a list of the textures present but we
 * may eventually want to make it more complete - will be guided
 * by what the content creators say they need.
 */
const std::string LLMaterialEditor::buildMaterialDescription()
{
    std::ostringstream desc;
    desc << LLTrans::getString("Material Texture Name Header");

    // add the texture names for each just so long as the material
    // we loaded has an entry for it (i think testing the texture 
    // control UUI for NULL is a valid metric for if it was loaded
    // or not but I suspect this code will change a lot so may need
    // to revisit
    if (!mBaseColorTextureCtrl->getValue().asUUID().isNull())
    {
        desc << mBaseColorName;
        desc << ", ";
    }
    if (!mMetallicTextureCtrl->getValue().asUUID().isNull())
    {
        desc << mMetallicRoughnessName;
        desc << ", ";
    }
    if (!mEmissiveTextureCtrl->getValue().asUUID().isNull())
    {
        desc << mEmissiveName;
        desc << ", ";
    }
    if (!mNormalTextureCtrl->getValue().asUUID().isNull())
    {
        desc << mNormalName;
    }

    // trim last char if it's a ',' in case there is no normal texture
    // present and the code above inserts one
    // (no need to check for string length - always has initial string)
    std::string::iterator iter = desc.str().end() - 1;
    if (*iter == ',')
    {
        desc.str().erase(iter);
    }

    // sanitize the material description so that it's compatible with the inventory
    // note: split this up because clang doesn't like operating directly on the
    // str() - error: lvalue reference to type 'basic_string<...>' cannot bind to a
    // temporary of type 'basic_string<...>'
    std::string inv_desc = desc.str();
    LLInventoryObject::correctInventoryName(inv_desc);

    return inv_desc;
}

bool LLMaterialEditor::saveIfNeeded()
{
    if (mUploadingTexturesCount > 0)
    {
        // Upload already in progress, wait until
        // textures upload will retry saving on callback.
        // Also should prevent some failure-callbacks
        return true;
    }

    if (saveTextures() > 0)
    {
        // started texture upload
        setEnabled(false);
        return true;
    }

    std::string buffer = getEncodedAsset();

    const LLInventoryItem* item = getItem();
    // save it out to database
    if (item)
    {
        if (!updateInventoryItem(buffer, mItemUUID, mObjectUUID))
        {
            return false;
        }

        if (mCloseAfterSave)
        {
            closeFloater();
        }
        else
        {
            mAssetStatus = PREVIEW_ASSET_LOADING;
            setEnabled(false);
        }
    }
    else
    { 
        // Make a new inventory item and set upload permissions
        LLPermissions local_permissions;
        local_permissions.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);

        U32 everyone_perm = LLFloaterPerms::getEveryonePerms("Materials");
        U32 group_perm = LLFloaterPerms::getGroupPerms("Materials");
        U32 next_owner_perm = LLFloaterPerms::getNextOwnerPerms("Materials");
        local_permissions.initMasks(PERM_ALL, PERM_ALL, everyone_perm, group_perm, next_owner_perm);

        std::string res_desc = buildMaterialDescription();
        createInventoryItem(buffer, mMaterialName, res_desc, local_permissions);

        // We do not update floater with uploaded asset yet, so just close it.
        closeFloater();
    }
    
    return true;
}

// static
bool LLMaterialEditor::updateInventoryItem(const std::string &buffer, const LLUUID &item_id, const LLUUID &task_id)
{
    const LLViewerRegion* region = gAgent.getRegion();
    if (!region)
    {
        LL_WARNS("MaterialEditor") << "Not connected to a region, cannot save material." << LL_ENDL;
        return false;
    }
    std::string agent_url = region->getCapability("UpdateMaterialAgentInventory");
    std::string task_url = region->getCapability("UpdateMaterialTaskInventory");

    if (!agent_url.empty() && !task_url.empty())
    {
        std::string url;
        LLResourceUploadInfo::ptr_t uploadInfo;

        if (task_id.isNull() && !agent_url.empty())
        {
            uploadInfo = std::make_shared<LLBufferedAssetUploadInfo>(item_id, LLAssetType::AT_MATERIAL, buffer,
                [](LLUUID itemId, LLUUID newAssetId, LLUUID newItemId, LLSD)
                {
                    // done callback
                    LLMaterialEditor::finishInventoryUpload(itemId, newAssetId, newItemId);
                },
                [](LLUUID itemId, LLUUID taskId, LLSD response, std::string reason)
                {
                    // failure callback
                    LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", LLSD(itemId));
                    if (me)
                    {
                        me->setEnabled(true);
                    }
                    return true;
                }
                );
            url = agent_url;
        }
        else if (!task_id.isNull() && !task_url.empty())
        {
            uploadInfo = std::make_shared<LLBufferedAssetUploadInfo>(task_id, item_id, LLAssetType::AT_MATERIAL, buffer,
                [](LLUUID itemId, LLUUID task_id, LLUUID newAssetId, LLSD)
                {
                    // done callback
                    LLMaterialEditor::finishTaskUpload(itemId, newAssetId, task_id);
                },
                [](LLUUID itemId, LLUUID task_id, LLSD response, std::string reason)
                {
                    // failure callback
                    LLSD floater_key;
                    floater_key["taskid"] = task_id;
                    floater_key["itemid"] = itemId;
                    LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", floater_key);
                    if (me)
                    {
                        me->setEnabled(true);
                    }
                    return true;
                }
                );
            url = task_url;
        }

        if (!url.empty() && uploadInfo)
        {
            LLViewerAssetUpload::EnqueueInventoryUpload(url, uploadInfo);
        }
        else
        {
            return false;
        }

    }
    else // !gAssetStorage
    {
        LL_WARNS("MaterialEditor") << "Not connected to an materials capable region." << LL_ENDL;
        return false;
    }

    // todo: apply permissions from textures here if server doesn't
    // if any texture is 'no transfer', material should be 'no transfer' as well

    return true;
}

// Callback intended for when a material is saved from an object and needs to
// be modified to reflect the new asset/name.
class LLObjectsMaterialItemCallback : public LLInventoryCallback
{
public:
    LLObjectsMaterialItemCallback(const LLPermissions& permissions, const std::string& asset_data, const std::string& new_name)
        : mPermissions(permissions),
        mAssetData(asset_data),
        mNewName(new_name)
    {
    }

    void fire(const LLUUID& inv_item_id) override
    {
        LLViewerInventoryItem* item = gInventory.getItem(inv_item_id);
        if (!item)
        {
            return;
        }

        // create_inventory_item/copy_inventory_item don't allow presetting some permissions, fix it now
        item->setPermissions(mPermissions);
        item->updateServer(FALSE);
        gInventory.updateItem(item);
        gInventory.notifyObservers();

        if (item->getName() != mNewName)
        {
            LLSD updates;
            updates["name"] = mNewName;
            update_inventory_item(inv_item_id, updates, NULL);
        }

        // from reference in LLSettingsVOBase::createInventoryItem()/updateInventoryItem()
        LLResourceUploadInfo::ptr_t uploadInfo =
            std::make_shared<LLBufferedAssetUploadInfo>(
                inv_item_id,
                LLAssetType::AT_MATERIAL,
                mAssetData,
                [](LLUUID item_id, LLUUID new_asset_id, LLUUID new_item_id, LLSD response)
                {
                    // done callback
                    LL_INFOS("Material") << "inventory item uploaded.  item: " << item_id << " new_item_id: " << new_item_id << " response: " << response << LL_ENDL;
                },
                nullptr // failure callback, floater already closed
            );

        const LLViewerRegion* region = gAgent.getRegion();
        if (region)
        {
            std::string agent_url(region->getCapability("UpdateMaterialAgentInventory"));
            if (agent_url.empty())
            {
                LL_ERRS("MaterialEditor") << "missing required agent inventory cap url" << LL_ENDL;
            }
            LLViewerAssetUpload::EnqueueInventoryUpload(agent_url, uploadInfo);
        }
    }
private:
    LLPermissions mPermissions;
    std::string mAssetData;
    std::string mNewName;
};

void LLMaterialEditor::createInventoryItem(const std::string &buffer, const std::string &name, const std::string &desc, const LLPermissions& permissions)
{
    // gen a new uuid for this asset
    LLTransactionID tid;
    tid.generate();     // timestamp-based randomization + uniquification
    LLUUID parent = gInventory.findUserDefinedCategoryUUIDForType(LLFolderType::FT_MATERIAL);
    const U8 subtype = NO_INV_SUBTYPE;  // TODO maybe use AT_SETTINGS and LLSettingsType::ST_MATERIAL ?

    LLPointer<LLObjectsMaterialItemCallback> cb = new LLObjectsMaterialItemCallback(permissions, buffer, name);
    create_inventory_item(gAgent.getID(), gAgent.getSessionID(), parent, tid, name, desc,
        LLAssetType::AT_MATERIAL, LLInventoryType::IT_MATERIAL, subtype, permissions.getMaskNextOwner(),
        cb);
}

void LLMaterialEditor::finishInventoryUpload(LLUUID itemId, LLUUID newAssetId, LLUUID newItemId)
{
    // Update the UI with the new asset.
    LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", LLSD(itemId));
    if (me)
    {
        if (newItemId.isNull())
        {
            me->setAssetId(newAssetId);
            me->refreshFromInventory();
        }
        else if (newItemId.notNull())
        {
            // Not supposed to happen?
            me->refreshFromInventory(newItemId);
        }
        else
        {
            me->refreshFromInventory(itemId);
        }

        if (me && !me->mTextureChangesUpdates.empty())
        {
            const LLInventoryItem* item = me->getItem();
            if (item)
            {
                // local materials were assigned, force load material and init tracking
                LLGLTFMaterial* mat = gGLTFMaterialList.getMaterial(item->getAssetUUID());
                for (mat_connection_map_t::value_type &val : me->mTextureChangesUpdates)
                {
                    LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(val.second.mTrackingId, mat);
                }
            }
        }
    }
}

void LLMaterialEditor::finishTaskUpload(LLUUID itemId, LLUUID newAssetId, LLUUID taskId)
{
    LLSD floater_key;
    floater_key["taskid"] = taskId;
    floater_key["itemid"] = itemId;
    LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", floater_key);
    if (me)
    {
        me->setAssetId(newAssetId);
        me->refreshFromInventory();
        me->setEnabled(true);

        if (me && !me->mTextureChangesUpdates.empty())
        {
            // local materials were assigned, force load material and init tracking
            LLGLTFMaterial* mat = gGLTFMaterialList.getMaterial(newAssetId);
            for (mat_connection_map_t::value_type &val : me->mTextureChangesUpdates)
            {
                LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(val.second.mTrackingId, mat);
            }
        }
    }
}

void LLMaterialEditor::finishSaveAs(
    const LLSD &oldKey,
    const LLUUID &newItemId,
    const std::string &buffer,
    bool has_unsaved_changes)
{
    LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", oldKey);
    LLViewerInventoryItem* item = gInventory.getItem(newItemId);
    if (item)
    {
        if (me)
        {
            me->mItemUUID = newItemId;
            me->mObjectUUID = LLUUID::null;
            me->mNotecardInventoryID = LLUUID::null;
            me->mNotecardObjectID = LLUUID::null;
            me->mAuxItem = nullptr;
            me->setKey(LLSD(newItemId)); // for findTypedInstance
            me->setMaterialName(item->getName());
            if (has_unsaved_changes)
            {
                if (!updateInventoryItem(buffer, newItemId, LLUUID::null))
                {
                    me->setEnabled(true);
                }
            }
            else
            {
                me->loadAsset();
                me->setEnabled(true);

                // Local texure support
                if (!me->mTextureChangesUpdates.empty())
                {
                    // local materials were assigned, force load material and init tracking
                    LLGLTFMaterial* mat = gGLTFMaterialList.getMaterial(item->getAssetUUID());
                    for (mat_connection_map_t::value_type &val : me->mTextureChangesUpdates)
                    {
                        LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(val.second.mTrackingId, mat);
                    }
                }
            }
        }
        else if(has_unsaved_changes)
        {
            updateInventoryItem(buffer, newItemId, LLUUID::null);
        }
    }
    else if (me)
    {
        me->setEnabled(true);
        LL_WARNS("MaterialEditor") << "Item does not exist, floater " << me->getKey() << LL_ENDL;
    }
}

void LLMaterialEditor::refreshFromInventory(const LLUUID& new_item_id)
{
    if (mIsOverride)
    {
        // refreshFromInventory shouldn't be called for overrides,
        // but just in case.
        LL_WARNS("MaterialEditor") << "Tried to refresh from inventory for live editor" << LL_ENDL;
        return;
    }
    LLSD old_key = getKey();
    if (new_item_id.notNull())
    {
        mItemUUID = new_item_id;
        if (mNotecardInventoryID.notNull())
        {
            LLSD floater_key;
            floater_key["objectid"] = mNotecardObjectID;
            floater_key["notecardid"] = mNotecardInventoryID;
            setKey(floater_key);
        }
        else if (mObjectUUID.notNull())
        {
            LLSD floater_key;
            floater_key["taskid"] = new_item_id;
            floater_key["itemid"] = mObjectUUID;
            setKey(floater_key);
        }
        else
        {
            setKey(LLSD(new_item_id));
        }
    }
    LL_DEBUGS("MaterialEditor") << "New floater key: " << getKey() << " Old key: " << old_key << LL_ENDL;
    loadAsset();
}


void LLMaterialEditor::onClickSaveAs()
{
    if (!LLMaterialEditor::capabilitiesAvailable())
    {
        LLNotificationsUtil::add("MissingMaterialCaps");
        return;
    }

    if (!can_afford_transaction(mExpectedUploadCost))
    {
        LLSD args;
        args["COST"] = llformat("%d", mExpectedUploadCost);
        LLNotificationsUtil::add("ErrorCannotAffordUpload", args);
        return;
    }

    LLSD args;
    args["DESC"] = mMaterialName;

    LLNotificationsUtil::add("SaveMaterialAs", args, LLSD(), boost::bind(&LLMaterialEditor::onSaveAsMsgCallback, this, _1, _2));
}

void LLMaterialEditor::onSaveAsMsgCallback(const LLSD& notification, const LLSD& response)
{
    S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
    if (0 == option)
    {
        std::string new_name = response["message"].asString();
        LLInventoryObject::correctInventoryName(new_name);
        if (!new_name.empty())
        {
            const LLInventoryItem* item;
            if (mNotecardInventoryID.notNull())
            {
                item = mAuxItem.get();
            }
            else
            {
                item = getItem();
            }
            if (item)
            {
                const LLUUID &marketplacelistings_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_MARKETPLACE_LISTINGS);
                LLUUID parent_id = item->getParentUUID();
                if (mObjectUUID.notNull() || marketplacelistings_id == parent_id || gInventory.isObjectDescendentOf(item->getUUID(), gInventory.getLibraryRootFolderID()))
                {
                    parent_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_MATERIAL);
                }

                // A two step process, first copy an existing item, then create new asset
                if (mNotecardInventoryID.notNull())
                {
                    LLPointer<LLInventoryCallback> cb = new LLMaterialEditorCopiedCallback(getKey(), new_name);
                    copy_inventory_from_notecard(parent_id,
                        mNotecardObjectID,
                        mNotecardInventoryID,
                        mAuxItem.get(),
                        gInventoryCallbacks.registerCB(cb));
                }
                else
                {
                    std::string buffer = getEncodedAsset();
                    LLPointer<LLInventoryCallback> cb = new LLMaterialEditorCopiedCallback(buffer, getKey(), mUnsavedChanges);
                    copy_inventory_item(
                        gAgent.getID(),
                        item->getPermissions().getOwner(),
                        item->getUUID(),
                        parent_id,
                        new_name,
                        cb);
                }

                mAssetStatus = PREVIEW_ASSET_LOADING;
                setEnabled(false);
            }
            else
            {
                setMaterialName(new_name);
                onClickSave();
            }
        }
        else
        {
            LLNotificationsUtil::add("InvalidMaterialName", LLSD(), LLSD(), [this](const LLSD& notification, const LLSD& response) 
                {
                    LLNotificationsUtil::add("SaveMaterialAs", LLSD().with("DESC", mMaterialName), LLSD(),
                        boost::bind(&LLMaterialEditor::onSaveAsMsgCallback, this, _1, _2));
                });
        }
    }
}

void LLMaterialEditor::onClickCancel()
{
    if (mUnsavedChanges)
    {
        LLNotificationsUtil::add("UsavedMaterialChanges", LLSD(), LLSD(), boost::bind(&LLMaterialEditor::onCancelMsgCallback, this, _1, _2));
    }
    else
    {
        closeFloater();
    }
}

void LLMaterialEditor::onCancelMsgCallback(const LLSD& notification, const LLSD& response)
{
    S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
    if (0 == option)
    {
        closeFloater();
    }
}

static void pack_textures(
    LLPointer<LLImageRaw>& base_color_img,
    LLPointer<LLImageRaw>& normal_img,
    LLPointer<LLImageRaw>& mr_img,
    LLPointer<LLImageRaw>& emissive_img,
    LLPointer<LLImageRaw>& occlusion_img,
    LLPointer<LLImageJ2C>& base_color_j2c,
    LLPointer<LLImageJ2C>& normal_j2c,
    LLPointer<LLImageJ2C>& mr_j2c,
    LLPointer<LLImageJ2C>& emissive_j2c)
{
    // NOTE : remove log spam and lossless vs lossy comparisons when the logs are no longer useful

    if (base_color_img)
    {
        base_color_j2c = LLViewerTextureList::convertToUploadFile(base_color_img);
        LL_DEBUGS("MaterialEditor") << "BaseColor: " << base_color_j2c->getDataSize() << LL_ENDL;
    }

    if (normal_img)
    {
        // create a losslessly compressed version of the normal map
        normal_j2c = LLViewerTextureList::convertToUploadFile(normal_img, 1024, false, true);
        LL_DEBUGS("MaterialEditor") << "Normal: " << normal_j2c->getDataSize() << LL_ENDL;
    }

    if (mr_img)
    {
        mr_j2c = LLViewerTextureList::convertToUploadFile(mr_img);
        LL_DEBUGS("MaterialEditor") << "Metallic/Roughness: " << mr_j2c->getDataSize() << LL_ENDL;
    }

    if (emissive_img)
    {
        emissive_j2c = LLViewerTextureList::convertToUploadFile(emissive_img);
        LL_DEBUGS("MaterialEditor") << "Emissive: " << emissive_j2c->getDataSize() << LL_ENDL;
    }
}

void LLMaterialEditor::uploadMaterialFromModel(const std::string& filename, tinygltf::Model& model_in, S32 index)
{
    if (index < 0 || !LLMaterialEditor::capabilitiesAvailable())
    {
        return;
    }

    if (model_in.materials.empty())
    {
        // materials are missing
        return;
    }

    if (index >= 0 && model_in.materials.size() <= index)
    {
        // material is missing
        return;
    }

    // Todo: no point in loading whole editor
    // This uses 'filename' to make sure multiple bulk uploads work
    // instead of fighting for a single instance.
    LLMaterialEditor* me = (LLMaterialEditor*)LLFloaterReg::getInstance("material_editor", LLSD().with("filename", filename).with("index", LLSD::Integer(index)));
    me->loadMaterial(model_in, filename, index, false);
    me->saveIfNeeded();
}


void LLMaterialEditor::loadMaterialFromFile(const std::string& filename, S32 index)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_UI;

    tinygltf::TinyGLTF loader;
    std::string        error_msg;
    std::string        warn_msg;

    bool loaded = false;
    tinygltf::Model model_in;

    std::string filename_lc = filename;
    LLStringUtil::toLower(filename_lc);

    // Load a tinygltf model fom a file. Assumes that the input filename has already been
    // been sanitized to one of (.gltf , .glb) extensions, so does a simple find to distinguish.
    if (std::string::npos == filename_lc.rfind(".gltf"))
    {  // file is binary
        loaded = loader.LoadBinaryFromFile(&model_in, &error_msg, &warn_msg, filename);
    }
    else
    {  // file is ascii
        loaded = loader.LoadASCIIFromFile(&model_in, &error_msg, &warn_msg, filename);
    }

    if (!loaded)
    {
        LLNotificationsUtil::add("CannotUploadMaterial");
        return;
    }

    if (model_in.materials.empty())
    {
        // materials are missing
        LLNotificationsUtil::add("CannotUploadMaterial");
        return;
    }

    if (index >= 0 && model_in.materials.size() <= index)
    {
        // material is missing
        LLNotificationsUtil::add("CannotUploadMaterial");
        return;
    }

    LLMaterialEditor* me = (LLMaterialEditor*)LLFloaterReg::getInstance("material_editor");

    if (index >= 0)
    {
        // Prespecified material
        me->loadMaterial(model_in, filename, index);
    }
    else if (model_in.materials.size() == 1)
    {
        // Only one, just load it
        me->loadMaterial(model_in, filename, 0);
    }
    else
    {
        // Promt user to select material
        std::list<std::string> material_list;
        std::vector<tinygltf::Material>::const_iterator mat_iter = model_in.materials.begin();
        std::vector<tinygltf::Material>::const_iterator mat_end = model_in.materials.end();
        
        for (; mat_iter != mat_end; mat_iter++)
        {
            std::string mat_name = mat_iter->name;
            if (mat_name.empty())
            {
                material_list.push_back("Material " + std::to_string(material_list.size()));
            }
            else
            {
                material_list.push_back(mat_name);
            }
        }

        material_list.push_back(me->getString("material_batch_import_text"));

        LLFloaterComboOptions::showUI(
            [me, model_in, filename](const std::string& option, S32 index)
        {
            me->loadMaterial(model_in, filename, index);
        },
            me->getString("material_selection_title"),
            me->getString("material_selection_text"),
            material_list
            );
    }
}

void LLMaterialEditor::onSelectionChanged()
{
    // Drop selection updates if we are waiting for
    // overrides to finish applying to not reset values
    // (might need a timeout)
    if (!mOverrideInProgress)
    {
        // mUpdateSignal triggers a lot per frame, breakwater
        mSelectionNeedsUpdate = true;
    }
}

void LLMaterialEditor::updateLive()
{
    mSelectionNeedsUpdate = true;
    mOverrideInProgress = false;
}

void LLMaterialEditor::loadLive()
{
    LLMaterialEditor* me = (LLMaterialEditor*)LLFloaterReg::getInstance("live_material_editor");
    if (me)
    {
        me->mOverrideInProgress = false;
        me->setFromSelection();

        // Set up for selection changes updates
        if (!me->mSelectionUpdateSlot.connected())
        {
            me->mSelectionUpdateSlot = LLSelectMgr::instance().mUpdateSignal.connect(boost::bind(&LLMaterialEditor::onSelectionChanged, me));
        }

        me->openFloater();
        me->setFocus(TRUE);
    }
}

// *NOTE: permissions_out includes user preferences for new item creation (LLFloaterPerms)
bool can_use_objects_material(LLSelectedTEGetMatData& func, const std::vector<PermissionBit>& ops, LLPermissions& permissions_out, LLViewerInventoryItem*& item_out)
{
    if (!LLMaterialEditor::capabilitiesAvailable())
    {
        return false;
    }

    // func.mIsOverride=true is used for the singleton material editor floater
    // associated with the build floater. This flag also excludes objects from
    // the selection that do not satisfy PERM_MODIFY.
    llassert(func.mIsOverride);
    LLSelectMgr::getInstance()->getSelection()->applyToTEs(&func, true /*first applicable*/);

    LLViewerObject* selected_object = func.mObject;
    if (!selected_object)
    {
        // LLSelectedTEGetMatData can fail if there are no selected faces
        // with materials, but we expect at least some object is selected.
        llassert(LLSelectMgr::getInstance()->getSelection()->getFirstObject());
        return false;
    }
    if (selected_object->isInventoryPending())
    {
        return false;
    }
    for (PermissionBit op : ops)
    {
        if (op == PERM_MODIFY && selected_object->isPermanentEnforced())
        {
            return false;
        }
    }

    item_out = selected_object->getInventoryItemByAsset(func.mMaterialId);

    LLPermissions item_permissions;
    if (item_out)
    {
        item_permissions.set(item_out->getPermissions());
        for (PermissionBit op : ops)
        {
            if (!gAgent.allowOperation(op, item_permissions, GP_OBJECT_MANIPULATE))
            {
                return false;
            }
        }
        // Update flags for new owner
        if (!item_permissions.setOwnerAndGroup(LLUUID::null, gAgent.getID(), LLUUID::null, true))
        {
            llassert(false);
            return false;
        }
    }
    else
    {
        item_permissions.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
    }

    // Use root object for permissions checking
    LLViewerObject* root_object = selected_object->getRootEdit();
    LLPermissions* object_permissions_p = LLSelectMgr::getInstance()->findObjectPermissions(root_object);
    LLPermissions object_permissions;
    if (object_permissions_p)
    {
        object_permissions.set(*object_permissions_p);
        for (PermissionBit op : ops)
        {
            if (!gAgent.allowOperation(op, object_permissions, GP_OBJECT_MANIPULATE))
            {
                return false;
            }
        }
        // Update flags for new owner
        if (!object_permissions.setOwnerAndGroup(LLUUID::null, gAgent.getID(), LLUUID::null, true))
        {
            llassert(false);
            return false;
        }
    }
    else
    {
        object_permissions.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
    }

    LLPermissions floater_perm;
    floater_perm.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
    floater_perm.setMaskEveryone(LLFloaterPerms::getEveryonePerms("Materials"));
    floater_perm.setMaskGroup(LLFloaterPerms::getGroupPerms("Materials"));
    floater_perm.setMaskNext(LLFloaterPerms::getNextOwnerPerms("Materials"));

    // *NOTE: A close inspection of LLPermissions::accumulate shows that
    // conflicting UUIDs will be unset. This is acceptable behavior for now.
    // The server will populate creator info based on the item creation method
    // used.
    // *NOTE: As far as I'm aware, there is currently no good way to preserve
    // creation history when there's no material item present. In that case,
    // the agent who saved the material will be considered the creator.
    // -Cosmic,2023-08-07
    if (item_out)
    {
        permissions_out.set(item_permissions);
    }
    else
    {
        permissions_out.set(object_permissions);
    }
    permissions_out.accumulate(floater_perm);

    return true;
}

bool LLMaterialEditor::canModifyObjectsMaterial()
{
    LLSelectedTEGetMatData func(true);
    LLPermissions permissions;
    LLViewerInventoryItem* item_out;
    return can_use_objects_material(func, std::vector({PERM_MODIFY}), permissions, item_out);
}

bool LLMaterialEditor::canSaveObjectsMaterial()
{
    LLSelectedTEGetMatData func(true);
    LLPermissions permissions;
    LLViewerInventoryItem* item_out;
    return can_use_objects_material(func, std::vector({PERM_COPY, PERM_MODIFY}), permissions, item_out);
}

bool LLMaterialEditor::canClipboardObjectsMaterial()
{
    if (LLSelectMgr::getInstance()->getSelection()->getObjectCount() != 1)
    {
        return false;
    }

    struct LLSelectedTEGetNullMat : public LLSelectedTEFunctor
    {
        bool apply(LLViewerObject* objectp, S32 te_index)
        {
            return objectp->getRenderMaterialID(te_index).isNull();
        }
    } null_func;

    if (LLSelectMgr::getInstance()->getSelection()->applyToTEs(&null_func))
    {
        return true;
    }

    LLSelectedTEGetMatData func(true);
    LLPermissions permissions;
    LLViewerInventoryItem* item_out;
    return can_use_objects_material(func, std::vector({PERM_COPY, PERM_MODIFY, PERM_TRANSFER}), permissions, item_out);
}

void LLMaterialEditor::saveObjectsMaterialAs()
{
    LLSelectedTEGetMatData func(true);
    LLPermissions permissions;
    LLViewerInventoryItem* item = nullptr;
    bool allowed = can_use_objects_material(func, std::vector({PERM_COPY, PERM_MODIFY}), permissions, item);
    if (!allowed)
    {
        LL_WARNS("MaterialEditor") << "Failed to save GLTF material from object" << LL_ENDL;
        return;
    }
    const LLUUID item_id = item ? item->getUUID() : LLUUID::null;
    saveObjectsMaterialAs(func.mMaterial, func.mLocalMaterial, permissions, func.mObjectId, item_id);
}


void LLMaterialEditor::saveObjectsMaterialAs(const LLGLTFMaterial* render_material, const LLLocalGLTFMaterial *local_material, const LLPermissions& permissions, const LLUUID& object_id, const LLUUID& item_id)
{
    if (local_material)
    {
        // This is a local material, reload it from file
        // so that user won't end up with grey textures
        // on next login.
        LLMaterialEditor::loadMaterialFromFile(local_material->getFilename(), local_material->getIndexInFile());

        LLMaterialEditor* me = (LLMaterialEditor*)LLFloaterReg::getInstance("material_editor");
        if (me)
        {
            // don't use override material here, it has 'hacked ids'
            // and values, use end result, apply it on top of local.
            const LLColor4& base_color = render_material->mBaseColor;
            me->setBaseColor(LLColor3(base_color));
            me->setTransparency(base_color[VW]);
            me->setMetalnessFactor(render_material->mMetallicFactor);
            me->setRoughnessFactor(render_material->mRoughnessFactor);
            me->setEmissiveColor(render_material->mEmissiveColor);
            me->setDoubleSided(render_material->mDoubleSided);
            me->setAlphaMode(render_material->getAlphaMode());
            me->setAlphaCutoff(render_material->mAlphaCutoff);

            // most things like colors we can apply without verifying
            // but texture ids are going to be different from both, base and override
            // so only apply override id if there is actually a difference
            if (local_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR] != render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR])
            {
                me->setBaseColorId(render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]);
                me->childSetValue("base_color_upload_fee", me->getString("no_upload_fee_string"));
            }
            if (local_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL] != render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL])
            {
                me->setNormalId(render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL]);
                me->childSetValue("normal_upload_fee", me->getString("no_upload_fee_string"));
            }
            if (local_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS] != render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS])
            {
                me->setMetallicRoughnessId(render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS]);
                me->childSetValue("metallic_upload_fee", me->getString("no_upload_fee_string"));
            }
            if (local_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE] != render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE])
            {
                me->setEmissiveId(render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE]);
                me->childSetValue("emissive_upload_fee", me->getString("no_upload_fee_string"));
            }

            // recalculate upload prices
            me->markChangesUnsaved(0);
        }

        return;
    }

    LLSD payload;
    if (render_material)
    {
        // Make a copy of the render material with unsupported transforms removed
        LLGLTFMaterial asset_material = *render_material;
        asset_material.sanitizeAssetMaterial();
        // Serialize the sanitized render material
        payload["data"] = asset_material.asJSON();
    }
    else
    {
        // Menu shouldn't allow this, but as a fallback
        // pick defaults from a blank material
        LLGLTFMaterial blank_mat;
        payload["data"] = blank_mat.asJSON();
        LL_WARNS() << "Got no material when trying to save material" << LL_ENDL;
    }

    LLSD args;
    args["DESC"] = LLTrans::getString("New Material");

    if (local_material)
    {
        LLPermissions local_permissions;
        local_permissions.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
        LLNotificationsUtil::add("SaveMaterialAs", args, payload, boost::bind(&LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback, _1, _2, local_permissions));
    }
    else
    {
        if (item_id.notNull())
        {
            // Copy existing item from object inventory, and create new composite asset on top of it
            LLNotificationsUtil::add("SaveMaterialAs", args, payload, boost::bind(&LLMaterialEditor::onCopyObjectsMaterialAsMsgCallback, _1, _2, permissions, object_id, item_id));
        }
        else
        {
            LLNotificationsUtil::add("SaveMaterialAs", args, payload, boost::bind(&LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback, _1, _2, permissions));
        }
    }
}

// static
void LLMaterialEditor::onCopyObjectsMaterialAsMsgCallback(const LLSD& notification, const LLSD& response, const LLPermissions& permissions, const LLUUID& object_id, const LLUUID& item_id)
{
    S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
    if (0 != option)
    {
        return;
    }

    LLSD asset;
    asset["version"] = LLGLTFMaterial::ASSET_VERSION;
    asset["type"] = LLGLTFMaterial::ASSET_TYPE;
    // This is the string serialized from LLGLTFMaterial::asJSON
    asset["data"] = notification["payload"]["data"];

    std::ostringstream str;
    LLSDSerialize::serialize(asset, str, LLSDSerialize::LLSD_BINARY);

    LLViewerObject* object = gObjectList.findObject(object_id);
    if (!object)
    {
        return;
    }
    const LLInventoryItem* item = object->getInventoryItem(item_id);
    if (!item)
    {
        return;
    }

    std::string new_name = response["message"].asString();
    LLInventoryObject::correctInventoryName(new_name);
    if (new_name.empty())
    {
        return;
    }

    const LLUUID destination_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_MATERIAL);

    LLPointer<LLInventoryCallback> cb = new LLObjectsMaterialItemCallback(permissions, str.str(), new_name);
    // NOTE: This should be an item copy. Saving a material to an inventory should be disabled when the associated material is no-copy.
    move_or_copy_inventory_from_object(destination_id,
                                       object_id,
                                       item_id,
                                       cb);
}

// static
void LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback(const LLSD& notification, const LLSD& response, const LLPermissions& permissions)
{
    S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
    if (0 != option)
    {
        return;
    }

    LLSD asset;
    asset["version"] = LLGLTFMaterial::ASSET_VERSION;
    asset["type"] = LLGLTFMaterial::ASSET_TYPE;
    // This is the string serialized from LLGLTFMaterial::asJSON
    asset["data"] = notification["payload"]["data"];

    std::ostringstream str;
    LLSDSerialize::serialize(asset, str, LLSDSerialize::LLSD_BINARY);

    std::string new_name = response["message"].asString();
    LLInventoryObject::correctInventoryName(new_name);
    if (new_name.empty())
    {
        return;
    }

    createInventoryItem(str.str(), new_name, std::string(), permissions);
}

const void upload_bulk(const std::vector<std::string>& filenames, LLFilePicker::ELoadFilter type);

void LLMaterialEditor::loadMaterial(const tinygltf::Model &model_in, const std::string &filename, S32 index, bool open_floater)
{
    if (index == model_in.materials.size())
    {
        // bulk upload all the things
        upload_bulk({ filename }, LLFilePicker::FFLOAD_MATERIAL);
        return;
    }

    if (model_in.materials.size() <= index)
    {
        return;
    }
    std::string folder = gDirUtilp->getDirName(filename);

    tinygltf::Material material_in = model_in.materials[index];

    tinygltf::Model  model_out;
    model_out.asset.version = "2.0";
    model_out.materials.resize(1);

    // get base color texture
    LLPointer<LLImageRaw> base_color_img = LLTinyGLTFHelper::getTexture(folder, model_in, material_in.pbrMetallicRoughness.baseColorTexture.index, mBaseColorName);
    // get normal map
    LLPointer<LLImageRaw> normal_img = LLTinyGLTFHelper::getTexture(folder, model_in, material_in.normalTexture.index, mNormalName);
    // get metallic-roughness texture
    LLPointer<LLImageRaw> mr_img = LLTinyGLTFHelper::getTexture(folder, model_in, material_in.pbrMetallicRoughness.metallicRoughnessTexture.index, mMetallicRoughnessName);
    // get emissive texture
    LLPointer<LLImageRaw> emissive_img = LLTinyGLTFHelper::getTexture(folder, model_in, material_in.emissiveTexture.index, mEmissiveName);
    // get occlusion map if needed
    LLPointer<LLImageRaw> occlusion_img;
    if (material_in.occlusionTexture.index != material_in.pbrMetallicRoughness.metallicRoughnessTexture.index)
    {
        std::string tmp;
        occlusion_img = LLTinyGLTFHelper::getTexture(folder, model_in, material_in.occlusionTexture.index, tmp);
    }

    LLTinyGLTFHelper::initFetchedTextures(material_in, base_color_img, normal_img, mr_img, emissive_img, occlusion_img,
        mBaseColorFetched, mNormalFetched, mMetallicRoughnessFetched, mEmissiveFetched);
    pack_textures(base_color_img, normal_img, mr_img, emissive_img, occlusion_img,
        mBaseColorJ2C, mNormalJ2C, mMetallicRoughnessJ2C, mEmissiveJ2C);

    LLUUID base_color_id;
    if (mBaseColorFetched.notNull())
    {
        mBaseColorFetched->forceToSaveRawImage(0, F32_MAX);
        base_color_id = mBaseColorFetched->getID();

        if (mBaseColorName.empty())
        {
            mBaseColorName = MATERIAL_BASE_COLOR_DEFAULT_NAME;
        }
    }

    LLUUID normal_id;
    if (mNormalFetched.notNull())
    {
        mNormalFetched->forceToSaveRawImage(0, F32_MAX);
        normal_id = mNormalFetched->getID();

        if (mNormalName.empty())
        {
            mNormalName = MATERIAL_NORMAL_DEFAULT_NAME;
        }
    }

    LLUUID mr_id;
    if (mMetallicRoughnessFetched.notNull())
    {
        mMetallicRoughnessFetched->forceToSaveRawImage(0, F32_MAX);
        mr_id = mMetallicRoughnessFetched->getID();

        if (mMetallicRoughnessName.empty())
        {
            mMetallicRoughnessName = MATERIAL_METALLIC_DEFAULT_NAME;
        }
    }

    LLUUID emissive_id;
    if (mEmissiveFetched.notNull())
    {
        mEmissiveFetched->forceToSaveRawImage(0, F32_MAX);
        emissive_id = mEmissiveFetched->getID();

        if (mEmissiveName.empty())
        {
            mEmissiveName = MATERIAL_EMISSIVE_DEFAULT_NAME;
        }
    }

    setBaseColorId(base_color_id);
    setBaseColorUploadId(base_color_id);
    setMetallicRoughnessId(mr_id);
    setMetallicRoughnessUploadId(mr_id);
    setEmissiveId(emissive_id);
    setEmissiveUploadId(emissive_id);
    setNormalId(normal_id);
    setNormalUploadId(normal_id);

    setFromGltfModel(model_in, index);

    setFromGltfMetaData(filename, model_in, index);

    if (getDoubleSided())
    {
        // SL-19392 Double sided materials double the number of pixels that must be rasterized,
        // and a great many tools that export GLTF simply leave double sided enabled whether
        // or not it is necessary.
        LL_DEBUGS("MaterialEditor") << "Defaulting Double Sided to disabled on import" << LL_ENDL;
        setDoubleSided(false);
    }

    markChangesUnsaved(U32_MAX);

    if (open_floater)
    {
        openFloater(getKey());
        setFocus(TRUE);
        setCanSave(true);
        setCanSaveAs(true);

        applyToSelection();
    }
}

bool LLMaterialEditor::setFromGltfModel(const tinygltf::Model& model, S32 index, bool set_textures)
{
    if (model.materials.size() > index)
    {
        const tinygltf::Material& material_in = model.materials[index];

        if (set_textures)
        {
            S32 index;
            LLUUID id;

            // get base color texture
            index = material_in.pbrMetallicRoughness.baseColorTexture.index;
            if (index >= 0)
            {
                id.set(model.images[index].uri);
                setBaseColorId(id);
            }
            else
            {
                setBaseColorId(LLUUID::null);
            }

            // get normal map
            index = material_in.normalTexture.index;
            if (index >= 0)
            {
                id.set(model.images[index].uri);
                setNormalId(id);
            }
            else
            {
                setNormalId(LLUUID::null);
            }

            // get metallic-roughness texture
            index = material_in.pbrMetallicRoughness.metallicRoughnessTexture.index;
            if (index >= 0)
            {
                id.set(model.images[index].uri);
                setMetallicRoughnessId(id);
            }
            else
            {
                setMetallicRoughnessId(LLUUID::null);
            }

            // get emissive texture
            index = material_in.emissiveTexture.index;
            if (index >= 0)
            {
                id.set(model.images[index].uri);
                setEmissiveId(id);
            }
            else
            {
                setEmissiveId(LLUUID::null);
            }
        }

        setAlphaMode(material_in.alphaMode);
        setAlphaCutoff(material_in.alphaCutoff);

        setBaseColor(LLTinyGLTFHelper::getColor(material_in.pbrMetallicRoughness.baseColorFactor));
        setEmissiveColor(LLTinyGLTFHelper::getColor(material_in.emissiveFactor));

        setMetalnessFactor(material_in.pbrMetallicRoughness.metallicFactor);
        setRoughnessFactor(material_in.pbrMetallicRoughness.roughnessFactor);

        setDoubleSided(material_in.doubleSided);
    }

    return true;
}

/**
 * Build a texture name from the contents of the (in tinyGLFT parlance) 
 * Image URI. This often is filepath to the original image on the users'
 *  local file system.
 */
const std::string LLMaterialEditor::getImageNameFromUri(std::string image_uri, const std::string texture_type)
{
    // getBaseFileName() works differently on each platform and file patchs 
    // can contain both types of delimiter so unify them then extract the 
    // base name (no path or extension)
    std::replace(image_uri.begin(), image_uri.end(), '\\', gDirUtilp->getDirDelimiter()[0]);
    std::replace(image_uri.begin(), image_uri.end(), '/', gDirUtilp->getDirDelimiter()[0]);
    const bool strip_extension = true;
    std::string stripped_uri = gDirUtilp->getBaseFileName(image_uri, strip_extension);

    // sometimes they can be really long and unwieldy - 64 chars is enough for anyone :) 
    const int max_texture_name_length = 64;
    if (stripped_uri.length() > max_texture_name_length)
    {
        stripped_uri = stripped_uri.substr(0, max_texture_name_length - 1);
    }

    // We intend to append the type of texture (base color, emissive etc.) to the 
    // name of the texture but sometimes the creator already did that.  To try
    // to avoid repeats (not perfect), we look for the texture type in the name
    // and if we find it, do not append the type, later on. One way this fails
    // (and it's fine for now) is I see some texture/image uris have a name like
    // "metallic roughness" and of course, that doesn't match our predefined
    // name "metallicroughness" - consider fix later..
    bool name_includes_type = false;
    std::string stripped_uri_lower = stripped_uri;
    LLStringUtil::toLower(stripped_uri_lower);
    stripped_uri_lower.erase(std::remove_if(stripped_uri_lower.begin(), stripped_uri_lower.end(), isspace), stripped_uri_lower.end());
    std::string texture_type_lower = texture_type;
    LLStringUtil::toLower(texture_type_lower);
    texture_type_lower.erase(std::remove_if(texture_type_lower.begin(), texture_type_lower.end(), isspace), texture_type_lower.end());
    if (stripped_uri_lower.find(texture_type_lower) != std::string::npos)
    {
        name_includes_type = true;
    }

    // uri doesn't include the type at all
    if (name_includes_type == false)
    {
        // uri doesn't include the type and the uri is not empty
        // so we can include everything
        if (stripped_uri.length() > 0)
        {
            // example "DamagedHelmet: base layer"
            return STRINGIZE(
                mMaterialNameShort <<
                ": " <<
                stripped_uri <<
                " (" <<
                texture_type <<
                ")"
            );
        }
        else
        // uri doesn't include the type (because the uri is empty)
        // so we must reorganize the string a bit to include the name
        // and an explicit name type
        {
            // example "DamagedHelmet: (Emissive)"
            return STRINGIZE(
                mMaterialNameShort <<
                " (" <<
                texture_type <<
                ")"
            );
        }
    }
    else
    // uri includes the type so just use it directly with the
    // name of the material
    {
        return STRINGIZE(
            // example: AlienBust: normal_layer
            mMaterialNameShort <<
            ": " <<
            stripped_uri
        );
    }
}

/**
 * Update the metadata for the material based on what we find in the loaded
 * file (along with some assumptions and interpretations...). Fields include
 * the name of the material, a material description and the names of the 
 * composite textures.
 */
void LLMaterialEditor::setFromGltfMetaData(const std::string& filename, const tinygltf::Model& model, S32 index)
{
    // Use the name (without any path/extension) of the file that was 
    // uploaded as the base of the material name. Then if the name of the 
    // scene is present and not blank, append that and use the result as
    // the name of the material. This is a first pass at creating a 
    // naming scheme that is useful to real content creators and hopefully
    // avoid 500 materials in your inventory called "scene" or "Default"
    const bool strip_extension = true;
    std::string base_filename = gDirUtilp->getBaseFileName(filename, strip_extension);

    // Extract the name of the scene. Note it is often blank or some very
    // generic name like "Scene" or "Default" so using this in the name
    // is less useful than you might imagine.
    std::string material_name;
    if (model.materials.size() > index && !model.materials[index].name.empty())
    {
        material_name = model.materials[index].name;
    }
    else if (model.scenes.size() > 0)
    {
        const tinygltf::Scene& scene_in = model.scenes[0];
        if (scene_in.name.length())
        {
            material_name = scene_in.name;
        }
        else
        {
            // scene name is empty so no point using it
        }
    }
    else
    {
        // scene name isn't present so no point using it
    }

    // If we have a valid material or scene name, use it to build the short and 
    // long versions of the material name. The long version is used 
    // as you might expect, for the material name. The short version is
    // used as part of the image/texture name - the theory is that will 
    // allow content creators to track the material and the corresponding
    // textures
    if (material_name.length())
    {
        mMaterialNameShort = base_filename;

        mMaterialName = STRINGIZE(
            base_filename << 
            " " << 
            "(" << 
            material_name <<
            ")"
        );
    }
    else
    // otherwise, just use the trimmed filename as is
    {
        mMaterialNameShort = base_filename;
        mMaterialName = base_filename;
    }

    // sanitize the material name so that it's compatible with the inventory
    LLInventoryObject::correctInventoryName(mMaterialName);
    LLInventoryObject::correctInventoryName(mMaterialNameShort);

    // We also set the title of the floater to match the 
    // name of the material
    setTitle(mMaterialName);

    /**
     * Extract / derive the names of each composite texture. For each, the 
     * index is used to to determine which of the "Images" is used. If the index
     * is -1 then that texture type is not present in the material (Seems to be 
     * quite common that a material is missing 1 or more types of texture)
     */
    if (model.materials.size() > index)
    {
        const tinygltf::Material& first_material = model.materials[index];

        mBaseColorName = MATERIAL_BASE_COLOR_DEFAULT_NAME;
        // note: unlike the other textures, base color doesn't have its own entry 
        // in the tinyGLTF Material struct. Rather, it is taken from a 
        // sub-texture in the pbrMetallicRoughness member
        int index = first_material.pbrMetallicRoughness.baseColorTexture.index;
        if (index > -1 && index < model.images.size())
        {
            // sanitize the name we decide to use for each texture
            std::string texture_name = getImageNameFromUri(model.images[index].uri, MATERIAL_BASE_COLOR_DEFAULT_NAME);
            LLInventoryObject::correctInventoryName(texture_name);
            mBaseColorName = texture_name;
        }

        mEmissiveName = MATERIAL_EMISSIVE_DEFAULT_NAME;
        index = first_material.emissiveTexture.index;
        if (index > -1 && index < model.images.size())
        {
            std::string texture_name = getImageNameFromUri(model.images[index].uri, MATERIAL_EMISSIVE_DEFAULT_NAME);
            LLInventoryObject::correctInventoryName(texture_name);
            mEmissiveName = texture_name;
        }

        mMetallicRoughnessName = MATERIAL_METALLIC_DEFAULT_NAME;
        index = first_material.pbrMetallicRoughness.metallicRoughnessTexture.index;
        if (index > -1 && index < model.images.size())
        {
            std::string texture_name = getImageNameFromUri(model.images[index].uri, MATERIAL_METALLIC_DEFAULT_NAME);
            LLInventoryObject::correctInventoryName(texture_name);
            mMetallicRoughnessName = texture_name;
        }

        mNormalName = MATERIAL_NORMAL_DEFAULT_NAME;
        index = first_material.normalTexture.index;
        if (index > -1 && index < model.images.size())
        {
            std::string texture_name = getImageNameFromUri(model.images[index].uri, MATERIAL_NORMAL_DEFAULT_NAME);
            LLInventoryObject::correctInventoryName(texture_name);
            mNormalName = texture_name;
        }
    }
}

void LLMaterialEditor::importMaterial()
{
    LLFilePickerReplyThread::startPicker(
        [](const std::vector<std::string>& filenames, LLFilePicker::ELoadFilter load_filter, LLFilePicker::ESaveFilter save_filter)
            {
                if (LLAppViewer::instance()->quitRequested())
                {
                    return;
                }
                if (filenames.size() > 0)
                {
                    LLMaterialEditor::loadMaterialFromFile(filenames[0], -1);
                }
            },
        LLFilePicker::FFLOAD_MATERIAL,
        true);
}

class LLRenderMaterialFunctor : public LLSelectedTEFunctor
{
public:
    LLRenderMaterialFunctor(const LLUUID &id)
        : mMatId(id)
    {
    }

    bool apply(LLViewerObject* objectp, S32 te) override
    {
        if (objectp && objectp->permModify() && objectp->getVolume())
        {
            LLVOVolume* vobjp = (LLVOVolume*)objectp;
            vobjp->setRenderMaterialID(te, mMatId, false /*preview only*/);
            vobjp->updateTEMaterialTextures(te);
        }
        return true;
    }
private:
    LLUUID mMatId;
};

class LLRenderMaterialOverrideFunctor : public LLSelectedNodeFunctor
{
public:
    LLRenderMaterialOverrideFunctor(
        LLMaterialEditor * me,
        const LLUUID &report_on_object_id,
        S32 report_on_te)
    : mEditor(me)
    , mSuccess(false)
    , mObjectId(report_on_object_id)
    , mObjectTE(report_on_te)
    {
    }

    virtual bool apply(LLSelectNode* nodep) override
    {
        LLViewerObject* objectp = nodep->getObject();
        if (!objectp || !objectp->permModify() || !objectp->getVolume())
        {
            return false;
        }
        S32 num_tes = llmin((S32)objectp->getNumTEs(), (S32)objectp->getNumFaces()); // avatars have TEs but no faces

        // post override from given object and te to the simulator
        // requestData should have:
        //  object_id - UUID of LLViewerObject
        //  side - S32 index of texture entry
        //  gltf_json - String of GLTF json for override data

        for (S32 te = 0; te < num_tes; ++te)
        {
            if (!nodep->isTESelected(te))
            {
                continue;
            }

            // Get material from object
            // Selection can cover multiple objects, and live editor is
            // supposed to overwrite changed values only
            LLTextureEntry* tep = objectp->getTE(te);

            if (tep->getGLTFMaterial() == nullptr)
            {
                // overrides are not supposed to work or apply if
                // there is no base material to work from
                continue;
            }

            LLPointer<LLGLTFMaterial> material = tep->getGLTFMaterialOverride();
            // make a copy to not invalidate existing
            // material for multiple objects
            if (material.isNull())
            {
                // Start with a material override which does not make any changes
                material = new LLGLTFMaterial();
            }
            else
            {
                material = new LLGLTFMaterial(*material);
            }

            U32 changed_flags = mEditor->getUnsavedChangesFlags();
            U32 reverted_flags = mEditor->getRevertedChangesFlags();

            LLPointer<LLGLTFMaterial> revert_mat;
            if (nodep->mSavedGLTFOverrideMaterials.size() > te)
            {
                if (nodep->mSavedGLTFOverrideMaterials[te].notNull())
                {
                    revert_mat = nodep->mSavedGLTFOverrideMaterials[te];
                }
                else
                {
                    // mSavedGLTFOverrideMaterials[te] being present but null
                    // means we need to use a default value 
                    revert_mat = new LLGLTFMaterial();
                }
            }
            // else can not revert at all

            // Override object's values with values from editor where appropriate
            if (changed_flags & MATERIAL_BASE_COLOR_DIRTY)
            {
                material->setBaseColorFactor(mEditor->getBaseColor(), true);
            }
            else if ((reverted_flags & MATERIAL_BASE_COLOR_DIRTY) && revert_mat.notNull())
            {
                material->setBaseColorFactor(revert_mat->mBaseColor, false);
            }

            if (changed_flags & MATERIAL_BASE_COLOR_TEX_DIRTY)
            {
                material->setBaseColorId(mEditor->getBaseColorId(), true);
                LLUUID tracking_id = mEditor->getLocalTextureTrackingIdFromFlag(MATERIAL_BASE_COLOR_TEX_DIRTY);
                if (tracking_id.notNull())
                {
                    LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(tracking_id, material);
                }
            }
            else if ((reverted_flags & MATERIAL_BASE_COLOR_TEX_DIRTY) && revert_mat.notNull())
            {
                material->setBaseColorId(revert_mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR], false);
                LLUUID tracking_id = mEditor->getLocalTextureTrackingIdFromFlag(MATERIAL_BASE_COLOR_TEX_DIRTY);
                if (tracking_id.notNull())
                {
                    LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(tracking_id, material);
                }
            }

            if (changed_flags & MATERIAL_NORMAL_TEX_DIRTY)
            {
                material->setNormalId(mEditor->getNormalId(), true);
                LLUUID tracking_id = mEditor->getLocalTextureTrackingIdFromFlag(MATERIAL_NORMAL_TEX_DIRTY);
                if (tracking_id.notNull())
                {
                    LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(tracking_id, material);
                }
            }
            else if ((reverted_flags & MATERIAL_NORMAL_TEX_DIRTY) && revert_mat.notNull())
            {
                material->setNormalId(revert_mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL], false);
                LLUUID tracking_id = mEditor->getLocalTextureTrackingIdFromFlag(MATERIAL_NORMAL_TEX_DIRTY);
                if (tracking_id.notNull())
                {
                    LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(tracking_id, material);
                }
            }

            if (changed_flags & MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY)
            {
                material->setOcclusionRoughnessMetallicId(mEditor->getMetallicRoughnessId(), true);
                LLUUID tracking_id = mEditor->getLocalTextureTrackingIdFromFlag(MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY);
                if (tracking_id.notNull())
                {
                    LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(tracking_id, material);
                }
            }
            else if ((reverted_flags & MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY) && revert_mat.notNull())
            {
                material->setOcclusionRoughnessMetallicId(revert_mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS], false);
                LLUUID tracking_id = mEditor->getLocalTextureTrackingIdFromFlag(MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY);
                if (tracking_id.notNull())
                {
                    LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(tracking_id, material);
                }
            }

            if (changed_flags & MATERIAL_METALLIC_ROUGHTNESS_METALNESS_DIRTY)
            {
                material->setMetallicFactor(mEditor->getMetalnessFactor(), true);
            }
            else if ((reverted_flags & MATERIAL_METALLIC_ROUGHTNESS_METALNESS_DIRTY) && revert_mat.notNull())
            {
                material->setMetallicFactor(revert_mat->mMetallicFactor, false);
            }

            if (changed_flags & MATERIAL_METALLIC_ROUGHTNESS_ROUGHNESS_DIRTY)
            {
                material->setRoughnessFactor(mEditor->getRoughnessFactor(), true);
            }
            else if ((reverted_flags & MATERIAL_METALLIC_ROUGHTNESS_ROUGHNESS_DIRTY) && revert_mat.notNull())
            {
                material->setRoughnessFactor(revert_mat->mRoughnessFactor, false);
            }

            if (changed_flags & MATERIAL_EMISIVE_COLOR_DIRTY)
            {
                material->setEmissiveColorFactor(LLColor3(mEditor->getEmissiveColor()), true);
            }
            else if ((reverted_flags & MATERIAL_EMISIVE_COLOR_DIRTY) && revert_mat.notNull())
            {
                material->setEmissiveColorFactor(revert_mat->mEmissiveColor, false);
            }

            if (changed_flags & MATERIAL_EMISIVE_TEX_DIRTY)
            {
                material->setEmissiveId(mEditor->getEmissiveId(), true);
                LLUUID tracking_id = mEditor->getLocalTextureTrackingIdFromFlag(MATERIAL_EMISIVE_TEX_DIRTY);
                if (tracking_id.notNull())
                {
                    LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(tracking_id, material);
                }
            }
            else if ((reverted_flags & MATERIAL_EMISIVE_TEX_DIRTY) && revert_mat.notNull())
            {
                material->setEmissiveId(revert_mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE], false);
                LLUUID tracking_id = mEditor->getLocalTextureTrackingIdFromFlag(MATERIAL_EMISIVE_TEX_DIRTY);
                if (tracking_id.notNull())
                {
                    LLLocalBitmapMgr::getInstance()->associateGLTFMaterial(tracking_id, material);
                }
            }

            if (changed_flags & MATERIAL_DOUBLE_SIDED_DIRTY)
            {
                material->setDoubleSided(mEditor->getDoubleSided(), true);
            }
            else if ((reverted_flags & MATERIAL_DOUBLE_SIDED_DIRTY) && revert_mat.notNull())
            {
                material->setDoubleSided(revert_mat->mDoubleSided, false);
            }

            if (changed_flags & MATERIAL_ALPHA_MODE_DIRTY)
            {
                material->setAlphaMode(mEditor->getAlphaMode(), true);
            }
            else if ((reverted_flags & MATERIAL_ALPHA_MODE_DIRTY) && revert_mat.notNull())
            {
                material->setAlphaMode(revert_mat->mAlphaMode, false);
            }

            if (changed_flags & MATERIAL_ALPHA_CUTOFF_DIRTY)
            {
                material->setAlphaCutoff(mEditor->getAlphaCutoff(), true);
            }
            else if ((reverted_flags & MATERIAL_ALPHA_CUTOFF_DIRTY) && revert_mat.notNull())
            {
                material->setAlphaCutoff(revert_mat->mAlphaCutoff, false);
            }

            if (mObjectTE == te
                && mObjectId == objectp->getID())
            {
                mSuccess = true;
            }
            LLGLTFMaterialList::queueModify(objectp, te, material);
        }
        return true;
    }

    static void modifyCallback(bool success)
    {
        if (!success)
        {
            // something went wrong update selection
            LLMaterialEditor::updateLive();
        }
        // else we will get updateLive() from panel face
    }

    bool getResult() { return mSuccess; }

private:
    LLMaterialEditor * mEditor;
    LLUUID mObjectId;
    S32 mObjectTE;
    bool mSuccess;
};

void LLMaterialEditor::applyToSelection()
{
    if (!mIsOverride)
    {
        // Only apply if working with 'live' materials
        // Might need a better way to distinguish 'live' mode.
        // But only one live edit is supposed to work at a time
        // as a pair to tools floater.
        return;
    }

    std::string url = gAgent.getRegionCapability("ModifyMaterialParams");
    if (!url.empty())
    {
        // Don't send data if there is nothing to send.
        // Some UI elements will cause multiple commits,
        // like spin ctrls on click and on down
        if (mUnsavedChanges != 0 || mRevertedChanges != 0)
        {
            mOverrideInProgress = true;
            LLObjectSelectionHandle selected_objects = LLSelectMgr::getInstance()->getSelection();
            LLRenderMaterialOverrideFunctor override_func(this, mOverrideObjectId, mOverrideObjectTE);
            selected_objects->applyToNodes(&override_func);

            void(*done_callback)(bool) = LLRenderMaterialOverrideFunctor::modifyCallback;

            LLGLTFMaterialList::flushUpdates(done_callback);

            if (!override_func.getResult())
            {
                // OverrideFunctor didn't find expected object or face
                mOverrideInProgress = false;
            }

            // we posted all changes
            mUnsavedChanges = 0;
            mRevertedChanges = 0;
        }
    }
    else
    {
        LL_WARNS("MaterialEditor") << "Not connected to materials capable region, missing ModifyMaterialParams cap" << LL_ENDL;

        // Fallback local preview. Will be removed once override systems is finished and new cap is deployed everywhere.
        LLPointer<LLFetchedGLTFMaterial> mat = new LLFetchedGLTFMaterial();
        getGLTFMaterial(mat);
        static const LLUUID placeholder("984e183e-7811-4b05-a502-d79c6f978a98");
        gGLTFMaterialList.addMaterial(placeholder, mat);
        LLRenderMaterialFunctor mat_func(placeholder);
        LLObjectSelectionHandle selected_objects = LLSelectMgr::getInstance()->getSelection();
        selected_objects->applyToTEs(&mat_func);
    }
}

// Get a dump of the json representation of the current state of the editor UI
// in GLTF format, excluding transforms as they are not supported in material
// assets. (See also LLGLTFMaterial::sanitizeAssetMaterial())
void LLMaterialEditor::getGLTFMaterial(LLGLTFMaterial* mat)
{
    mat->mBaseColor = getBaseColor();
    mat->mBaseColor.mV[3] = getTransparency();
    mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR] = getBaseColorId();

    mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL] = getNormalId();

    mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS] = getMetallicRoughnessId();
    mat->mMetallicFactor = getMetalnessFactor();
    mat->mRoughnessFactor = getRoughnessFactor();

    mat->mEmissiveColor = getEmissiveColor();
    mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE] = getEmissiveId();

    mat->mDoubleSided = getDoubleSided();
    mat->setAlphaMode(getAlphaMode());
    mat->mAlphaCutoff = getAlphaCutoff();
}

void LLMaterialEditor::setFromGLTFMaterial(LLGLTFMaterial* mat)
{
    setBaseColor(mat->mBaseColor);
    setBaseColorId(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]);
    setNormalId(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL]);

    setMetallicRoughnessId(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS]);
    setMetalnessFactor(mat->mMetallicFactor);
    setRoughnessFactor(mat->mRoughnessFactor);

    setEmissiveColor(mat->mEmissiveColor);
    setEmissiveId(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE]);

    setDoubleSided(mat->mDoubleSided);
    setAlphaMode(mat->getAlphaMode());
    setAlphaCutoff(mat->mAlphaCutoff);

    if (mat->hasLocalTextures())
    {
        for (LLGLTFMaterial::local_tex_map_t::value_type &val : mat->mTrackingIdToLocalTexture)
        {
            LLUUID world_id = LLLocalBitmapMgr::getInstance()->getWorldID(val.first);
            if (val.second != world_id)
            {
                LL_WARNS() << "world id mismatch" << LL_ENDL;
            }
            if (world_id == mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR])
            {
                subscribeToLocalTexture(MATERIAL_BASE_COLOR_TEX_DIRTY, val.first);
            }
            if (world_id == mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS])
            {
                subscribeToLocalTexture(MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY, val.first);
            }
            if (world_id == mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE])
            {
                subscribeToLocalTexture(MATERIAL_EMISIVE_TEX_DIRTY, val.first);
            }
            if (world_id == mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL])
            {
                subscribeToLocalTexture(MATERIAL_NORMAL_TEX_DIRTY, val.first);
            }
        }
    }
}

bool LLMaterialEditor::setFromSelection()
{
    LLObjectSelectionHandle selected_objects = LLSelectMgr::getInstance()->getSelection();
    LLSelectedTEGetMatData func(mIsOverride);

    selected_objects->applyToTEs(&func);
    mHasSelection = !selected_objects->isEmpty();
    mSelectionNeedsUpdate = false;

    if (func.mMaterial.notNull())
    {
        setFromGLTFMaterial(func.mMaterial);
        LLViewerObject* selected_object = func.mObject;
        const LLViewerInventoryItem* item = selected_object->getInventoryItemByAsset(func.mMaterialId);
        const bool allow_modify = !item || canModify(selected_object, item);
        setEnableEditing(allow_modify);

        // todo: apply local texture data to all materials in selection
    }
    else
    {
        // pick defaults from a blank material;
        LLGLTFMaterial blank_mat;
        setFromGLTFMaterial(&blank_mat);
        if (mIsOverride)
        {
            setEnableEditing(false);
        }
    }

    if (mIsOverride)
    {
        mBaseColorTextureCtrl->setTentative(!func.mIdenticalTexColor);
        mMetallicTextureCtrl->setTentative(!func.mIdenticalTexMetal);
        mEmissiveTextureCtrl->setTentative(!func.mIdenticalTexEmissive);
        mNormalTextureCtrl->setTentative(!func.mIdenticalTexNormal);

        // Memorize selection data for filtering further updates
        mOverrideObjectId = func.mObjectId;
        mOverrideObjectTE = func.mObjectTE;

        // Ovverdired might have been updated,
        // refresh state of local textures in overrides
        // 
        // Todo: this probably shouldn't be here, but in localbitmap,
        // subscried to all material overrides if we want copied
        // objects to get properly updated as well
        LLSelectedTEUpdateOverrides local_tex_func(this);
        selected_objects->applyToNodes(&local_tex_func);
    }

    return func.mMaterial.notNull();
}


void LLMaterialEditor::loadAsset()
{
    const LLInventoryItem* item;
    if (mNotecardInventoryID.notNull())
    {
        item = mAuxItem.get();
    }
    else
    {
        item = getItem();
    }
    
    bool fail = false;

    if (item)
    {
        LLPermissions perm(item->getPermissions());
        bool allow_copy = gAgent.allowOperation(PERM_COPY, perm, GP_OBJECT_MANIPULATE);
        bool allow_modify = canModify(mObjectUUID, item);
        bool source_library = mObjectUUID.isNull() && gInventory.isObjectDescendentOf(mItemUUID, gInventory.getLibraryRootFolderID());

        setCanSaveAs(allow_copy);
        setMaterialName(item->getName());

        {
            mAssetID = item->getAssetUUID();

            if (mAssetID.isNull())
            {
                mAssetStatus = PREVIEW_ASSET_LOADED;
                loadDefaults();
                resetUnsavedChanges();
                setEnableEditing(allow_modify && !source_library);
            }
            else
            {
                LLHost source_sim = LLHost();
                LLSD* user_data = new LLSD();

                if (mNotecardInventoryID.notNull())
                {
                    user_data->with("objectid", mNotecardObjectID).with("notecardid", mNotecardInventoryID);
                }
                else if (mObjectUUID.notNull())
                {
                    LLViewerObject* objectp = gObjectList.findObject(mObjectUUID);
                    if (objectp && objectp->getRegion())
                    {
                        source_sim = objectp->getRegion()->getHost();
                    }
                    else
                    {
                        // The object that we're trying to look at disappeared, bail.
                        LL_WARNS("MaterialEditor") << "Can't find object " << mObjectUUID << " associated with material." << LL_ENDL;
                        mAssetID.setNull();
                        mAssetStatus = PREVIEW_ASSET_LOADED;
                        resetUnsavedChanges();
                        setEnableEditing(allow_modify && !source_library);
                        return;
                    }
                    user_data->with("taskid", mObjectUUID).with("itemid", mItemUUID);
                }
                else
                {
                    user_data = new LLSD(mItemUUID);
                }

                setEnableEditing(false); // wait for it to load

                mAssetStatus = PREVIEW_ASSET_LOADING;

                // May callback immediately
                gAssetStorage->getAssetData(item->getAssetUUID(),
                    LLAssetType::AT_MATERIAL,
                    &onLoadComplete,
                    (void*)user_data,
                    TRUE);
            }
        }
    }
    else if (mObjectUUID.notNull() && mItemUUID.notNull())
    {
        LLViewerObject* objectp = gObjectList.findObject(mObjectUUID);
        if (objectp && (objectp->isInventoryPending() || objectp->isInventoryDirty()))
        {
            // It's a material in object's inventory and we failed to get it because inventory is not up to date.
            // Subscribe for callback and retry at inventoryChanged()
            registerVOInventoryListener(objectp, NULL); //removes previous listener

            if (objectp->isInventoryDirty())
            {
                objectp->requestInventory();
            }
        }
        else
        {
            fail = true;
        }
    }
    else
    {
        fail = true;
    }

    if (fail)
    {
        /*editor->setText(LLStringUtil::null);
        editor->makePristine();
        editor->setEnabled(TRUE);*/
        // Don't set asset status here; we may not have set the item id yet
        // (e.g. when this gets called initially)
        //mAssetStatus = PREVIEW_ASSET_LOADED;
    }
}

// static
void LLMaterialEditor::onLoadComplete(const LLUUID& asset_uuid,
    LLAssetType::EType type,
    void* user_data, S32 status, LLExtStat ext_status)
{
    LLSD* floater_key = (LLSD*)user_data;
    LL_DEBUGS("MaterialEditor") << "loading " << asset_uuid << " for " << *floater_key << LL_ENDL;
    LLMaterialEditor* editor = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", *floater_key);
    if (editor)
    {
        if (asset_uuid != editor->mAssetID)
        {
            LL_WARNS("MaterialEditor") << "Asset id mismatch, expected: " << editor->mAssetID << " got: " << asset_uuid << LL_ENDL;
        }
        if (0 == status)
        {
            LLFileSystem file(asset_uuid, type, LLFileSystem::READ);

            S32 file_length = file.getSize();

            std::vector<char> buffer(file_length + 1);
            file.read((U8*)&buffer[0], file_length);

            editor->decodeAsset(buffer);

            BOOL allow_modify = editor->canModify(editor->mObjectUUID, editor->getItem());
            BOOL source_library = editor->mObjectUUID.isNull() && gInventory.isObjectDescendentOf(editor->mItemUUID, gInventory.getLibraryRootFolderID());
            editor->setEnableEditing(allow_modify && !source_library);
            editor->resetUnsavedChanges();
            editor->mAssetStatus = PREVIEW_ASSET_LOADED;
            editor->setEnabled(true); // ready for use
        }
        else
        {
            if (LL_ERR_ASSET_REQUEST_NOT_IN_DATABASE == status ||
                LL_ERR_FILE_EMPTY == status)
            {
                LLNotificationsUtil::add("MaterialMissing");
            }
            else if (LL_ERR_INSUFFICIENT_PERMISSIONS == status)
            {
                // Not supposed to happen?
                LL_WARNS("MaterialEditor") << "No permission to view material " << asset_uuid << LL_ENDL;
                LLNotificationsUtil::add("MaterialNoPermissions");
            }
            else
            {
                LLNotificationsUtil::add("UnableToLoadMaterial");
            }
            editor->setEnableEditing(false);

            LL_WARNS("MaterialEditor") << "Problem loading material: " << status << LL_ENDL;
            editor->mAssetStatus = PREVIEW_ASSET_ERROR;
        }
    }
    else
    {
        LL_DEBUGS("MaterialEditor") << "Floater " << *floater_key << " does not exist." << LL_ENDL;
    }
    delete floater_key;
}

void LLMaterialEditor::inventoryChanged(LLViewerObject* object,
    LLInventoryObject::object_list_t* inventory,
    S32 serial_num,
    void* user_data)
{
    removeVOInventoryListener();
    loadAsset();
}


void LLMaterialEditor::saveTexture(LLImageJ2C* img, const std::string& name, const LLUUID& asset_id, upload_callback_f cb)
{
    if (asset_id.isNull()
        || img == nullptr
        || img->getDataSize() == 0)
    {
        return;
    }

    // copy image bytes into string
    std::string buffer;
    buffer.assign((const char*) img->getData(), img->getDataSize());

    U32 expected_upload_cost = LLAgentBenefitsMgr::current().getTextureUploadCost();

    LLSD key = getKey();
    std::function<bool(LLUUID itemId, LLSD response, std::string reason)> failed_upload([key](LLUUID assetId, LLSD response, std::string reason)
    {
        LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", key);
        if (me)
        {
            me->setFailedToUploadTexture();
        }
        return true; // handled
    });

    LLResourceUploadInfo::ptr_t uploadInfo(std::make_shared<LLNewBufferedResourceUploadInfo>(
        buffer,
        asset_id,
        name, 
        name, 
        0,
        LLFolderType::FT_TEXTURE, 
        LLInventoryType::IT_TEXTURE,
        LLAssetType::AT_TEXTURE,
        LLFloaterPerms::getNextOwnerPerms("Uploads"),
        LLFloaterPerms::getGroupPerms("Uploads"),
        LLFloaterPerms::getEveryonePerms("Uploads"),
        expected_upload_cost, 
        false,
        cb,
        failed_upload));

    upload_new_resource(uploadInfo);
}

void LLMaterialEditor::setFailedToUploadTexture()
{
    mUploadingTexturesFailure = true;
    mUploadingTexturesCount--;
    if (mUploadingTexturesCount == 0)
    {
        setEnabled(true);
    }
}

S32 LLMaterialEditor::saveTextures()
{
    mUploadingTexturesFailure = false; // not supposed to get here if already uploading

    S32 work_count = 0;
    LLSD key = getKey(); // must be locally declared for lambda's capture to work
    if (mBaseColorTextureUploadId == getBaseColorId() && mBaseColorTextureUploadId.notNull())
    {
        mUploadingTexturesCount++;
        work_count++;

        // For ease of inventory management, we prepend the material name.
        std::string name = mMaterialName + ": " + mBaseColorName;

        saveTexture(mBaseColorJ2C, name, mBaseColorTextureUploadId, [key](LLUUID newAssetId, LLSD response)
        {
            LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", key);
            if (me)
            {
                if (response["success"].asBoolean())
                {
                    me->setBaseColorId(newAssetId);

                    // discard upload buffers once texture have been saved
                    me->mBaseColorJ2C = nullptr;
                    me->mBaseColorFetched = nullptr;
                    me->mBaseColorTextureUploadId.setNull();

                    me->mUploadingTexturesCount--;

                    if (!me->mUploadingTexturesFailure)
                    {
                        // try saving
                        me->saveIfNeeded();
                    }
                    else if (me->mUploadingTexturesCount == 0)
                    {
                        me->setEnabled(true);
                    }
                }
                else
                {
                    // stop upload if possible, unblock and let user decide
                    me->setFailedToUploadTexture();
                }
            }
        });
    }
    if (mNormalTextureUploadId == getNormalId() && mNormalTextureUploadId.notNull())
    {
        mUploadingTexturesCount++;
        work_count++;

        // For ease of inventory management, we prepend the material name.
        std::string name = mMaterialName + ": " + mNormalName;

        saveTexture(mNormalJ2C, name, mNormalTextureUploadId, [key](LLUUID newAssetId, LLSD response)
        {
            LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", key);
            if (me)
            {
                if (response["success"].asBoolean())
                {
                    me->setNormalId(newAssetId);

                    // discard upload buffers once texture have been saved
                    me->mNormalJ2C = nullptr;
                    me->mNormalFetched = nullptr;
                    me->mNormalTextureUploadId.setNull();

                    me->mUploadingTexturesCount--;

                    if (!me->mUploadingTexturesFailure)
                    {
                        // try saving
                        me->saveIfNeeded();
                    }
                    else if (me->mUploadingTexturesCount == 0)
                    {
                        me->setEnabled(true);
                    }
                }
                else
                {
                    // stop upload if possible, unblock and let user decide
                    me->setFailedToUploadTexture();
                }
            }
        });
    }
    if (mMetallicTextureUploadId == getMetallicRoughnessId() && mMetallicTextureUploadId.notNull())
    {
        mUploadingTexturesCount++;
        work_count++;

        // For ease of inventory management, we prepend the material name.
        std::string name = mMaterialName + ": " + mMetallicRoughnessName;

        saveTexture(mMetallicRoughnessJ2C, name, mMetallicTextureUploadId, [key](LLUUID newAssetId, LLSD response)
        {
            LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", key);
            if (me)
            {
                if (response["success"].asBoolean())
                {
                    me->setMetallicRoughnessId(newAssetId);

                    // discard upload buffers once texture have been saved
                    me->mMetallicRoughnessJ2C = nullptr;
                    me->mMetallicRoughnessFetched = nullptr;
                    me->mMetallicTextureUploadId.setNull();

                    me->mUploadingTexturesCount--;

                    if (!me->mUploadingTexturesFailure)
                    {
                        // try saving
                        me->saveIfNeeded();
                    }
                    else if (me->mUploadingTexturesCount == 0)
                    {
                        me->setEnabled(true);
                    }
                }
                else
                {
                    // stop upload if possible, unblock and let user decide
                    me->setFailedToUploadTexture();
                }
            }
        });
    }

    if (mEmissiveTextureUploadId == getEmissiveId() && mEmissiveTextureUploadId.notNull())
    {
        mUploadingTexturesCount++;
        work_count++;

        // For ease of inventory management, we prepend the material name.
        std::string name = mMaterialName + ": " + mEmissiveName;

        saveTexture(mEmissiveJ2C, name, mEmissiveTextureUploadId, [key](LLUUID newAssetId, LLSD response)
        {
            LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", LLSD(key));
            if (me)
            {
                if (response["success"].asBoolean())
                {
                    me->setEmissiveId(newAssetId);

                    // discard upload buffers once texture have been saved
                    me->mEmissiveJ2C = nullptr;
                    me->mEmissiveFetched = nullptr;
                    me->mEmissiveTextureUploadId.setNull();

                    me->mUploadingTexturesCount--;

                    if (!me->mUploadingTexturesFailure)
                    {
                        // try saving
                        me->saveIfNeeded();
                    }
                    else if (me->mUploadingTexturesCount == 0)
                    {
                        me->setEnabled(true);
                    }
                }
                else
                {
                    // stop upload if possible, unblock and let user decide
                    me->setFailedToUploadTexture();
                }
            }
        });
    }

    if (!work_count)
    {
        // Discard upload buffers once textures have been confirmed as saved.
        // Otherwise we keep buffers for potential upload failure recovery.
        clearTextures();
    }

    // asset storage can callback immediately, causing a decrease
    // of mUploadingTexturesCount, report amount of work scheduled
    // not amount of work remaining
    return work_count;
}

void LLMaterialEditor::clearTextures()
{
    mBaseColorJ2C = nullptr;
    mNormalJ2C = nullptr;
    mEmissiveJ2C = nullptr;
    mMetallicRoughnessJ2C = nullptr;

    mBaseColorFetched = nullptr;
    mNormalFetched = nullptr;
    mMetallicRoughnessFetched = nullptr;
    mEmissiveFetched = nullptr;

    mBaseColorTextureUploadId.setNull();
    mNormalTextureUploadId.setNull();
    mMetallicTextureUploadId.setNull();
    mEmissiveTextureUploadId.setNull();
}

void LLMaterialEditor::loadDefaults()
{
    tinygltf::Model model_in;
    model_in.materials.resize(1);
    setFromGltfModel(model_in, 0, true);
}

bool LLMaterialEditor::capabilitiesAvailable()
{
    const LLViewerRegion* region = gAgent.getRegion();
    if (!region)
    {
        LL_WARNS("MaterialEditor") << "Not connected to a region, cannot save material." << LL_ENDL;
        return false;
    }
    std::string agent_url = region->getCapability("UpdateMaterialAgentInventory");
    std::string task_url = region->getCapability("UpdateMaterialTaskInventory");

    return (!agent_url.empty() && !task_url.empty());
}

