/**
 * @file lltextureentry.cpp
 * @brief LLTextureEntry base class
 *
 * $LicenseInfo:firstyear=2001&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 "linden_common.h"

#include "lluuid.h"
#include "llmediaentry.h"
#include "lltextureentry.h"
#include "llmaterialid.h"
#include "llsdutil_math.h"
#include "v4color.h"

const U8 DEFAULT_BUMP_CODE = 0;  // no bump or shininess

const LLTextureEntry LLTextureEntry::null;

// Some LLSD keys.  Do not change these!
#define OBJECT_ID_KEY_STR "object_id"
#define TEXTURE_INDEX_KEY_STR "texture_index"
#define OBJECT_MEDIA_VERSION_KEY_STR "object_media_version"
#define OBJECT_MEDIA_DATA_KEY_STR "object_media_data"
#define TEXTURE_MEDIA_DATA_KEY_STR "media_data"

/*static*/ const char* LLTextureEntry::OBJECT_ID_KEY = OBJECT_ID_KEY_STR;
/*static*/ const char* LLTextureEntry::OBJECT_MEDIA_DATA_KEY = OBJECT_MEDIA_DATA_KEY_STR;
/*static*/ const char* LLTextureEntry::MEDIA_VERSION_KEY = OBJECT_MEDIA_VERSION_KEY_STR;
/*static*/ const char* LLTextureEntry::TEXTURE_INDEX_KEY = TEXTURE_INDEX_KEY_STR;
/*static*/ const char* LLTextureEntry::TEXTURE_MEDIA_DATA_KEY = TEXTURE_MEDIA_DATA_KEY_STR;

static const std::string MEDIA_VERSION_STRING_PREFIX = "x-mv:";

// static
LLTextureEntry* LLTextureEntry::newTextureEntry()
{
    return new LLTextureEntry();
}

//===============================================================
LLTextureEntry::LLTextureEntry()
  : mMediaEntry(NULL)
  , mSelected(false)
  , mMaterialUpdatePending(false)
{
    init(LLUUID::null,1.f,1.f,0.f,0.f,0.f,DEFAULT_BUMP_CODE);
}

LLTextureEntry::LLTextureEntry(const LLUUID& tex_id)
  : mMediaEntry(NULL)
  , mSelected(false)
  , mMaterialUpdatePending(false)
{
    init(tex_id,1.f,1.f,0.f,0.f,0.f,DEFAULT_BUMP_CODE);
}

LLTextureEntry::LLTextureEntry(const LLTextureEntry &rhs)
  : mMediaEntry(NULL)
  , mSelected(false)
  , mMaterialUpdatePending(false)
{
    *this = rhs;
}

LLTextureEntry &LLTextureEntry::operator=(const LLTextureEntry &rhs)
{
    if (this != &rhs)
    {
        mID = rhs.mID;
        mScaleS = rhs.mScaleS;
        mScaleT = rhs.mScaleT;
        mOffsetS = rhs.mOffsetS;
        mOffsetT = rhs.mOffsetT;
        mRotation = rhs.mRotation;
        mColor = rhs.mColor;
        mBump = rhs.mBump;
        mMediaFlags = rhs.mMediaFlags;
        mGlow = rhs.mGlow;
        mMaterialID = rhs.mMaterialID;
        mMaterial = rhs.mMaterial;
        if (mMediaEntry != NULL) {
            delete mMediaEntry;
        }
        if (rhs.mMediaEntry != NULL) {
            // Make a copy
            mMediaEntry = new LLMediaEntry(*rhs.mMediaEntry);
        }
        else {
            mMediaEntry = NULL;
        }

        mMaterialID = rhs.mMaterialID;

        if (mGLTFMaterial)
        {
            mGLTFMaterial->removeTextureEntry(this);
        }
        mGLTFMaterial = rhs.mGLTFMaterial;
        if (mGLTFMaterial)
        {
            mGLTFMaterial->addTextureEntry(this);
        }

        if (rhs.mGLTFMaterialOverrides.notNull())
        {
            mGLTFMaterialOverrides = new LLGLTFMaterial(*rhs.mGLTFMaterialOverrides);
        }
        else
        {
            mGLTFMaterialOverrides = nullptr;
        }
    }

    return *this;
}

void LLTextureEntry::init(const LLUUID& tex_id, F32 scale_s, F32 scale_t, F32 offset_s, F32 offset_t, F32 rotation, U8 bump)
{
    setID(tex_id);

    mScaleS = scale_s;
    mScaleT = scale_t;
    mOffsetS = offset_s;
    mOffsetT = offset_t;
    mRotation = rotation;
    mBump = bump;
    mMediaFlags = 0x0;
    mGlow = 0;
    mMaterialID.clear();

    setColor(LLColor4(1.f, 1.f, 1.f, 1.f));
    if (mMediaEntry != NULL) {
        delete mMediaEntry;
    }
    mMediaEntry = NULL;
}

LLTextureEntry::~LLTextureEntry()
{
    if(mMediaEntry)
    {
        delete mMediaEntry;
        mMediaEntry = NULL;
    }

    if (mGLTFMaterial)
    {
        mGLTFMaterial->removeTextureEntry(this);
        mGLTFMaterial = NULL;
    }
}

bool LLTextureEntry::operator!=(const LLTextureEntry &rhs) const
{
    if (mID != rhs.mID) return(true);
    if (mScaleS != rhs.mScaleS) return(true);
    if (mScaleT != rhs.mScaleT) return(true);
    if (mOffsetS != rhs.mOffsetS) return(true);
    if (mOffsetT != rhs.mOffsetT) return(true);
    if (mRotation != rhs.mRotation) return(true);
    if (mColor != rhs.mColor) return (true);
    if (mBump != rhs.mBump) return (true);
    if (mMediaFlags != rhs.mMediaFlags) return (true);
    if (mGlow != rhs.mGlow) return (true);
    if (mMaterialID != rhs.mMaterialID) return (true);
    return(false);
}

bool LLTextureEntry::operator==(const LLTextureEntry &rhs) const
{
    if (mID != rhs.mID) return(false);
    if (mScaleS != rhs.mScaleS) return(false);
    if (mScaleT != rhs.mScaleT) return(false);
    if (mOffsetS != rhs.mOffsetS) return(false);
    if (mOffsetT != rhs.mOffsetT) return(false);
    if (mRotation != rhs.mRotation) return(false);
    if (mColor != rhs.mColor) return (false);
    if (mBump != rhs.mBump) return (false);
    if (mMediaFlags != rhs.mMediaFlags) return false;
    if (mGlow != rhs.mGlow) return false;
    if (mMaterialID != rhs.mMaterialID) return (false);
    return(true);
}

LLSD LLTextureEntry::asLLSD() const
{
    LLSD sd;
    asLLSD(sd);
    return sd;
}

void LLTextureEntry::asLLSD(LLSD& sd) const
{
    LL_PROFILE_ZONE_SCOPED;
    sd["imageid"] = mID;
    sd["colors"] = ll_sd_from_color4(mColor);
    sd["scales"] = mScaleS;
    sd["scalet"] = mScaleT;
    sd["offsets"] = mOffsetS;
    sd["offsett"] = mOffsetT;
    sd["imagerot"] = mRotation;
    sd["bump"] = getBumpShiny();
    sd["fullbright"] = getFullbright();
    sd["media_flags"] = mMediaFlags;
    if (hasMedia()) {
        LLSD mediaData;
        if (NULL != getMediaData()) {
            getMediaData()->asLLSD(mediaData);
        }
        sd[TEXTURE_MEDIA_DATA_KEY] = mediaData;
    }
    sd["glow"] = mGlow;

    if (mGLTFMaterialOverrides.notNull())
    {
        sd["gltf_override"] = mGLTFMaterialOverrides->asJSON();
    }
}

bool LLTextureEntry::fromLLSD(const LLSD& sd)
{
    LL_PROFILE_ZONE_SCOPED;
    const char *w, *x;
    w = "imageid";
    if (sd.has(w))
    {
        setID( sd[w] );
    } else goto fail;
    w = "colors";
    if (sd.has(w))
    {
        setColor( ll_color4_from_sd(sd["colors"]) );
    } else goto fail;
    w = "scales";
    x = "scalet";
    if (sd.has(w) && sd.has(x))
    {
        setScale( (F32)sd[w].asReal(), (F32)sd[x].asReal() );
    } else goto fail;
    w = "offsets";
    x = "offsett";
    if (sd.has(w) && sd.has(x))
    {
        setOffset( (F32)sd[w].asReal(), (F32)sd[x].asReal() );
    } else goto fail;
    w = "imagerot";
    if (sd.has(w))
    {
        setRotation( (F32)sd[w].asReal() );
    } else goto fail;
    w = "bump";
    if (sd.has(w))
    {
        setBumpShiny( sd[w].asInteger() );
    } else goto fail;
    w = "fullbright";
    if (sd.has(w))
    {
        setFullbright( sd[w].asInteger() );
    } else goto fail;
    w = "media_flags";
    if (sd.has(w))
    {
        setMediaTexGen( sd[w].asInteger() );
    } else goto fail;
    // If the "has media" flag doesn't match the fact that
    // media data exists, updateMediaData will "fix" it
    // by either clearing or setting the flag
    w = TEXTURE_MEDIA_DATA_KEY;
    if (hasMedia() != sd.has(w))
    {
        LL_WARNS() << "LLTextureEntry::fromLLSD: media_flags (" << hasMedia() <<
            ") does not match presence of media_data (" << sd.has(w) << ").  Fixing." << LL_ENDL;
    }
    updateMediaData(sd[w]);

    w = "glow";
    if (sd.has(w))
    {
        setGlow((F32)sd[w].asReal() );
    }

    w = "gltf_override";
    if (sd.has(w))
    {
        if (mGLTFMaterialOverrides.isNull())
        {
            mGLTFMaterialOverrides = new LLGLTFMaterial();
        }

        std::string warn_msg, error_msg;
        if (!mGLTFMaterialOverrides->fromJSON(sd[w].asString(), warn_msg, error_msg))
        {
            LL_WARNS() << llformat("Failed to parse GLTF json: %s -- %s", warn_msg.c_str(), error_msg.c_str()) << LL_ENDL;
            LL_WARNS() << sd[w].asString() << LL_ENDL;

            mGLTFMaterialOverrides = nullptr;
        }
    }

    return true;
fail:
    return false;
}

// virtual
// override this method for each derived class
LLTextureEntry* LLTextureEntry::newBlank() const
{
    return new LLTextureEntry();
}

// virtual
LLTextureEntry* LLTextureEntry::newCopy() const
{
    return new LLTextureEntry(*this);
}

S32 LLTextureEntry::setID(const LLUUID &tex_id)
{
    if (mID != tex_id)
    {
        mID = tex_id;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setScale(F32 s, F32 t)
{
    S32 retval = 0;

    if (  (mScaleS != s)
        ||(mScaleT != t))
    {
        mScaleS = s;
        mScaleT = t;

        retval = TEM_CHANGE_TEXTURE;
    }
    return retval;
}

S32 LLTextureEntry::setScaleS(F32 s)
{
    S32 retval = TEM_CHANGE_NONE;
    if (mScaleS != s)
    {
        mScaleS = s;
        retval = TEM_CHANGE_TEXTURE;
    }
    return retval;
}

S32 LLTextureEntry::setScaleT(F32 t)
{
    S32 retval = TEM_CHANGE_NONE;
    if (mScaleT != t)
    {
        mScaleT = t;
        retval = TEM_CHANGE_TEXTURE;
    }
    return retval;
}

S32 LLTextureEntry::setColor(const LLColor4 &color)
{
    if (mColor != color)
    {
        mColor = color;
        return TEM_CHANGE_COLOR;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setColor(const LLColor3 &color)
{
    if (mColor != color)
    {
        // This preserves alpha.
        mColor.setVec(color);
        return TEM_CHANGE_COLOR;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setAlpha(const F32 alpha)
{
    if (mColor.mV[VALPHA] != alpha)
    {
        mColor.mV[VALPHA] = alpha;
        return TEM_CHANGE_COLOR;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setOffset(F32 s, F32 t)
{
    S32 retval = 0;

    if (  (mOffsetS != s)
        ||(mOffsetT != t))
    {
        mOffsetS = s;
        mOffsetT = t;

        retval = TEM_CHANGE_TEXTURE;
    }
    return retval;
}

S32 LLTextureEntry::setOffsetS(F32 s)
{
    S32 retval = 0;
    if (mOffsetS != s)
    {
        mOffsetS = s;
        retval = TEM_CHANGE_TEXTURE;
    }
    return retval;
}

S32 LLTextureEntry::setOffsetT(F32 t)
{
    S32 retval = 0;
    if (mOffsetT != t)
    {
        mOffsetT = t;
        retval = TEM_CHANGE_TEXTURE;
    }
    return retval;
}

S32 LLTextureEntry::setRotation(F32 theta)
{
    if (mRotation != theta && llfinite(theta))
    {
        mRotation = theta;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setBumpShinyFullbright(U8 bump)
{
    if (mBump != bump)
    {
        mBump = bump;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setMediaTexGen(U8 media)
{
    S32 result = TEM_CHANGE_NONE;
    result |= setTexGen(media & TEM_TEX_GEN_MASK);
    result |= setMediaFlags(media & TEM_MEDIA_MASK);
    return result;
}

S32 LLTextureEntry::setBumpmap(U8 bump)
{
    bump &= TEM_BUMP_MASK;
    if (getBumpmap() != bump)
    {
        mBump &= ~TEM_BUMP_MASK;
        mBump |= bump;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setFullbright(U8 fullbright)
{
    fullbright &= TEM_FULLBRIGHT_MASK;
    if (getFullbright() != fullbright)
    {
        mBump &= ~(TEM_FULLBRIGHT_MASK<<TEM_FULLBRIGHT_SHIFT);
        mBump |= fullbright << TEM_FULLBRIGHT_SHIFT;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setShiny(U8 shiny)
{
    shiny &= TEM_SHINY_MASK;
    if (getShiny() != shiny)
    {
        mBump &= ~(TEM_SHINY_MASK<<TEM_SHINY_SHIFT);
        mBump |= shiny << TEM_SHINY_SHIFT;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setBumpShiny(U8 bump_shiny)
{
    bump_shiny &= TEM_BUMP_SHINY_MASK;
    if (getBumpShiny() != bump_shiny)
    {
        mBump &= ~TEM_BUMP_SHINY_MASK;
        mBump |= bump_shiny;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

void LLTextureEntry::setGLTFMaterial(LLGLTFMaterial* material, bool local_origin)
{
    if (material != getGLTFMaterial())
    {
        // assert on precondtion:
        // whether or not mGLTFMaterial is null, any existing override should have been cleared
        // before calling setGLTFMaterial
        // NOTE: if you're hitting this assert, try to make sure calling code is using LLViewerObject::setRenderMaterialID
        //llassert(!local_origin || getGLTFMaterialOverride() == nullptr || getGLTFMaterialOverride()->isClearedForBaseMaterial());

        if (mGLTFMaterial)
        {
            // Local materials have to keep track
            // due to update mechanics
            mGLTFMaterial->removeTextureEntry(this);
        }

        mGLTFMaterial = material;

        if (mGLTFMaterial)
        {
            mGLTFMaterial->addTextureEntry(this);
        }

        if (mGLTFMaterial == nullptr)
        {
            setGLTFRenderMaterial(nullptr);
        }
    }
}

S32 LLTextureEntry::setGLTFMaterialOverride(LLGLTFMaterial* mat)
{
    llassert(mat == nullptr || getGLTFMaterial() != nullptr); // if override is not null, base material must not be null
    if (mat == mGLTFMaterialOverrides)
    {
        return TEM_CHANGE_NONE;
    }

    mGLTFMaterialOverrides = mat;

    return TEM_CHANGE_TEXTURE;
}

S32 LLTextureEntry::setBaseMaterial()
{
    S32 changed = TEM_CHANGE_NONE;

    if (mGLTFMaterialOverrides)
    {
        if (mGLTFMaterialOverrides->setBaseMaterial())
        {
            changed = TEM_CHANGE_TEXTURE;
        }

        if (LLGLTFMaterial::sDefault == *mGLTFMaterialOverrides)
        {
            mGLTFMaterialOverrides = nullptr;
            changed = TEM_CHANGE_TEXTURE;
        }
    }

    return changed;
}

LLGLTFMaterial* LLTextureEntry::getGLTFRenderMaterial() const
{
    if (mGLTFRenderMaterial.notNull())
    {
        return mGLTFRenderMaterial;
    }

    llassert(getGLTFMaterialOverride() == nullptr || getGLTFMaterialOverride()->isClearedForBaseMaterial());
    return getGLTFMaterial();
}

S32 LLTextureEntry::setGLTFRenderMaterial(LLGLTFMaterial* mat)
{
    if (mGLTFRenderMaterial != mat)
    {
        mGLTFRenderMaterial = mat;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setMediaFlags(U8 media_flags)
{
    media_flags &= TEM_MEDIA_MASK;
    if (getMediaFlags() != media_flags)
    {
        mMediaFlags &= ~TEM_MEDIA_MASK;
        mMediaFlags |= media_flags;

        // Special code for media handling
        if( hasMedia() && mMediaEntry == NULL)
        {
            mMediaEntry = new LLMediaEntry;
        }
        else if ( ! hasMedia() && mMediaEntry != NULL)
        {
            delete mMediaEntry;
            mMediaEntry = NULL;
        }

        return TEM_CHANGE_MEDIA;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setTexGen(U8 tex_gen)
{
    tex_gen &= TEM_TEX_GEN_MASK;
    if (getTexGen() != tex_gen)
    {
        mMediaFlags &= ~TEM_TEX_GEN_MASK;
        mMediaFlags |= tex_gen;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setGlow(F32 glow)
{
    if (mGlow != glow)
    {
        mGlow = glow;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setMaterialID(const LLMaterialID& pMaterialID)
{
    if ( (mMaterialID != pMaterialID) || (mMaterialUpdatePending && !mSelected) )
    {
        if (mSelected)
        {
            mMaterialUpdatePending = true;
            mMaterialID = pMaterialID;
            return TEM_CHANGE_TEXTURE;
        }

        mMaterialUpdatePending = false;
        mMaterialID = pMaterialID;
        return TEM_CHANGE_TEXTURE;
    }
    return TEM_CHANGE_NONE;
}

S32 LLTextureEntry::setMaterialParams(const LLMaterialPtr pMaterialParams)
{
    if (mSelected)
    {
        mMaterialUpdatePending = true;
    }
    mMaterial = pMaterialParams;

    return TEM_CHANGE_TEXTURE;
}

void LLTextureEntry::setMediaData(const LLMediaEntry &media_entry)
{
    mMediaFlags |= MF_HAS_MEDIA;
    if (NULL != mMediaEntry)
    {
        delete mMediaEntry;
    }
    mMediaEntry = new LLMediaEntry(media_entry);
}

bool LLTextureEntry::updateMediaData(const LLSD& media_data)
{
    if (media_data.isUndefined())
    {
        // clear the media data
        clearMediaData();
        return false;
    }
    else {
        mMediaFlags |= MF_HAS_MEDIA;
        if (mMediaEntry == NULL)
        {
            mMediaEntry = new LLMediaEntry;
        }
        // *NOTE: this will *clobber* all of the fields in mMediaEntry
        // with whatever fields are present (or not present) in media_data!
        mMediaEntry->fromLLSD(media_data);
        return true;
    }
}

void LLTextureEntry::clearMediaData()
{
    mMediaFlags &= ~MF_HAS_MEDIA;
    if (mMediaEntry != NULL) {
        delete mMediaEntry;
    }
    mMediaEntry = NULL;
}

void LLTextureEntry::mergeIntoMediaData(const LLSD& media_fields)
{
    mMediaFlags |= MF_HAS_MEDIA;
    if (mMediaEntry == NULL)
    {
        mMediaEntry = new LLMediaEntry;
    }
    // *NOTE: this will *merge* the data in media_fields
    // with the data in our media entry
    mMediaEntry->mergeFromLLSD(media_fields);
}

//static
std::string LLTextureEntry::touchMediaVersionString(const std::string &in_version, const LLUUID &agent_id)
{
    // XXX TODO: make media version string binary (base64-encoded?)
    // Media "URL" is a representation of a version and the last-touched agent
    // x-mv:nnnnn/agent-id
    // where "nnnnn" is version number
    // *NOTE: not the most efficient code in the world...
    U32 current_version = getVersionFromMediaVersionString(in_version) + 1;
    const size_t MAX_VERSION_LEN = 10; // 2^32 fits in 10 decimal digits
    char buf[MAX_VERSION_LEN+1];
    snprintf(buf, (int)MAX_VERSION_LEN+1, "%0*u", (int)MAX_VERSION_LEN, current_version);  // added int cast to fix warning/breakage on mac.
    return MEDIA_VERSION_STRING_PREFIX + buf + "/" + agent_id.asString();
}

//static
U32 LLTextureEntry::getVersionFromMediaVersionString(const std::string &version_string)
{
    U32 version = 0;
    if (!version_string.empty())
    {
        size_t found = version_string.find(MEDIA_VERSION_STRING_PREFIX);
        if (found != std::string::npos)
        {
            found = version_string.find_first_of("/", found);
            std::string v = version_string.substr(MEDIA_VERSION_STRING_PREFIX.length(), found);
            version = strtoul(v.c_str(),NULL,10);
        }
    }
    return version;
}

//static
LLUUID LLTextureEntry::getAgentIDFromMediaVersionString(const std::string &version_string)
{
    LLUUID id;
    if (!version_string.empty())
    {
        size_t found = version_string.find(MEDIA_VERSION_STRING_PREFIX);
        if (found != std::string::npos)
        {
            found = version_string.find_first_of("/", found);
            if (found != std::string::npos)
            {
                std::string v = version_string.substr(found + 1);
                id.set(v);
            }
        }
    }
    return id;
}

//static
bool LLTextureEntry::isMediaVersionString(const std::string &version_string)
{
    return std::string::npos != version_string.find(MEDIA_VERSION_STRING_PREFIX);
}