/**
* @file llsettingsbase.cpp
* @author optional
* @brief A base class for asset based settings groups.
*
* $LicenseInfo:2011&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2017, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
* $/LicenseInfo$
*/

#include "llsettingsbase.h"

#include "llmath.h"
#include <algorithm>

#include "llsdserialize.h"
#include <boost/bind.hpp>

//=========================================================================
namespace
{
    const LLSettingsBase::TrackPosition BREAK_POINT = 0.5;
}

const LLSettingsBase::TrackPosition LLSettingsBase::INVALID_TRACKPOS(-1.0);
const std::string LLSettingsBase::DEFAULT_SETTINGS_NAME("_default_");

//=========================================================================
std::ostream &operator <<(std::ostream& os, LLSettingsBase &settings)
{
    LLSDSerialize::serialize(settings.getSettings(), os, LLSDSerialize::LLSD_NOTATION);

    return os;
}

//=========================================================================
const std::string LLSettingsBase::SETTING_ID("id");
const std::string LLSettingsBase::SETTING_NAME("name");
const std::string LLSettingsBase::SETTING_HASH("hash");
const std::string LLSettingsBase::SETTING_TYPE("type");
const std::string LLSettingsBase::SETTING_ASSETID("asset_id");
const std::string LLSettingsBase::SETTING_FLAGS("flags");

const U32 LLSettingsBase::FLAG_NOCOPY(0x01 << 0);
const U32 LLSettingsBase::FLAG_NOMOD(0x01 << 1);
const U32 LLSettingsBase::FLAG_NOTRANS(0x01 << 2);
const U32 LLSettingsBase::FLAG_NOSAVE(0x01 << 3);

const U32 LLSettingsBase::Validator::VALIDATION_PARTIAL(0x01 << 0);

//=========================================================================
LLSettingsBase::LLSettingsBase():
    mSettings(LLSD::emptyMap()),
    mDirty(true),
    mBlendedFactor(0.0)
{
}

LLSettingsBase::LLSettingsBase(const LLSD setting) :
    mSettings(setting),
    mDirty(true),
    mBlendedFactor(0.0)
{
}

//=========================================================================
void LLSettingsBase::lerpSettings(const LLSettingsBase &other, F64 mix)
{
    mSettings = interpolateSDMap(mSettings, other.mSettings, other.getParameterMap(), mix);
    setDirtyFlag(true);
}

LLSD LLSettingsBase::combineSDMaps(const LLSD &settings, const LLSD &other) const
{
    LLSD newSettings;

    for (LLSD::map_const_iterator it = settings.beginMap(); it != settings.endMap(); ++it)
    {
        std::string key_name = (*it).first;
        LLSD value = (*it).second;

        LLSD::Type setting_type = value.type();
        switch (setting_type)
        {
        case LLSD::TypeMap:
            newSettings[key_name] = combineSDMaps(value, LLSD());
            break;
        case LLSD::TypeArray:
            newSettings[key_name] = LLSD::emptyArray();
            for (LLSD::array_const_iterator ita = value.beginArray(); ita != value.endArray(); ++ita)
            {
                newSettings[key_name].append(*ita);
            }
            break;
        //case LLSD::TypeInteger:
        //case LLSD::TypeReal:
        //case LLSD::TypeBoolean:
        //case LLSD::TypeString:
        //case LLSD::TypeUUID:
        //case LLSD::TypeURI:
        //case LLSD::TypeDate:
        //case LLSD::TypeBinary:
        default:
            newSettings[key_name] = value;
            break;
        }
    }

    if (!other.isUndefined())
    {
        for (LLSD::map_const_iterator it = other.beginMap(); it != other.endMap(); ++it)
        {
            std::string key_name = (*it).first;
            LLSD value = (*it).second;

            LLSD::Type setting_type = value.type();
            switch (setting_type)
            {
            case LLSD::TypeMap:
                newSettings[key_name] = combineSDMaps(value, LLSD());
                break;
            case LLSD::TypeArray:
                newSettings[key_name] = LLSD::emptyArray();
                for (LLSD::array_const_iterator ita = value.beginArray(); ita != value.endArray(); ++ita)
                {
                    newSettings[key_name].append(*ita);
                }
                break;
            //case LLSD::TypeInteger:
            //case LLSD::TypeReal:
            //case LLSD::TypeBoolean:
            //case LLSD::TypeString:
            //case LLSD::TypeUUID:
            //case LLSD::TypeURI:
            //case LLSD::TypeDate:
            //case LLSD::TypeBinary:
            default:
                newSettings[key_name] = value;
                break;
            }
        }
    }

    return newSettings;
}

LLSD LLSettingsBase::interpolateSDMap(const LLSD &settings, const LLSD &other, const parammapping_t& defaults, F64 mix) const
{
    LLSD newSettings;

    stringset_t skip = getSkipInterpolateKeys();
    stringset_t slerps = getSlerpKeys();

    llassert(mix >= 0.0f && mix <= 1.0f);

    for (LLSD::map_const_iterator it = settings.beginMap(); it != settings.endMap(); ++it)
    {
        std::string key_name = (*it).first;
        LLSD value = (*it).second;

        if (skip.find(key_name) != skip.end())
            continue;

        LLSD other_value;
        if (other.has(key_name))
        {
            other_value = other[key_name];
        }
        else
        {
            parammapping_t::const_iterator def_iter = defaults.find(key_name);
            if (def_iter != defaults.end())
            {
                other_value = def_iter->second.getDefaultValue();
            }
            else if (value.type() == LLSD::TypeMap)
            {
                // interpolate in case there are defaults inside (part of legacy)
                other_value = LLSDMap();
            }
            else
            {
                // The other or defaults does not contain this setting, keep the original value
                // TODO: Should I blend this out instead?
                newSettings[key_name] = value;
                continue;
            }
        }

        newSettings[key_name] = interpolateSDValue(key_name, value, other_value, defaults, mix, slerps);
    }

    // Special handling cases
    // Flags
    if (settings.has(SETTING_FLAGS))
    {
        U32 flags = (U32)settings[SETTING_FLAGS].asInteger();
        if (other.has(SETTING_FLAGS))
            flags |= (U32)other[SETTING_FLAGS].asInteger();

        newSettings[SETTING_FLAGS] = LLSD::Integer(flags);
    }

    // Now add anything that is in other but not in the settings
    for (LLSD::map_const_iterator it = other.beginMap(); it != other.endMap(); ++it)
    {
        std::string key_name = (*it).first;

        if (skip.find(key_name) != skip.end())
            continue;

        if (settings.has(key_name))
            continue;

        parammapping_t::const_iterator def_iter = defaults.find(key_name);
        if (def_iter != defaults.end())
        {
            // Blend against default value
            newSettings[key_name] = interpolateSDValue(key_name, def_iter->second.getDefaultValue(), (*it).second, defaults, mix, slerps);
        }
        else if ((*it).second.type() == LLSD::TypeMap)
        {
            // interpolate in case there are defaults inside (part of legacy)
            newSettings[key_name] = interpolateSDValue(key_name, LLSDMap(), (*it).second, defaults, mix, slerps);
        }
        // else do nothing when no known defaults
        // TODO: Should I blend this out instead?
    }

    // Note: writes variables from skip list, bug?
    for (LLSD::map_const_iterator it = other.beginMap(); it != other.endMap(); ++it)
    {
        // TODO: Should I blend this in instead?
        if (skip.find((*it).first) == skip.end())
            continue;

        if (!settings.has((*it).first))
            continue;

        newSettings[(*it).first] = (*it).second;
    }

    return newSettings;
}

LLSD LLSettingsBase::interpolateSDValue(const std::string& key_name, const LLSD &value, const LLSD &other_value, const parammapping_t& defaults, BlendFactor mix, const stringset_t& slerps) const
{
    LLSD new_value;

    LLSD::Type setting_type = value.type();

    if (other_value.type() != setting_type)
    {
        // The data type mismatched between this and other. Hard switch when we pass the break point
        // but issue a warning.
        LL_WARNS("SETTINGS") << "Setting lerp between mismatched types for '" << key_name << "'." << LL_ENDL;
        new_value = (mix > BREAK_POINT) ? other_value : value;
    }

    switch (setting_type)
    {
        case LLSD::TypeInteger:
            // lerp between the two values rounding the result to the nearest integer.
            new_value = LLSD::Integer(llroundf(lerp(value.asReal(), other_value.asReal(), mix)));
            break;
        case LLSD::TypeReal:
            // lerp between the two values.
            new_value = LLSD::Real(lerp(value.asReal(), other_value.asReal(), mix));
            break;
        case LLSD::TypeMap:
            // deep copy.
            new_value = interpolateSDMap(value, other_value, defaults, mix);
            break;

        case LLSD::TypeArray:
        {
            LLSD new_array(LLSD::emptyArray());

            if (slerps.find(key_name) != slerps.end())
            {
                LLQuaternion a(value);
                LLQuaternion b(other_value);
                LLQuaternion q = slerp(mix, a, b);
                new_array = q.getValue();
            }
            else
            {   // TODO: We could expand this to inspect the type and do a deep lerp based on type.
                // for now assume a heterogeneous array of reals.
                size_t len = std::max(value.size(), other_value.size());

                for (size_t i = 0; i < len; ++i)
                {

                    new_array[i] = lerp(value[i].asReal(), other_value[i].asReal(), mix);
                }
            }

            new_value = new_array;
        }

        break;

        case LLSD::TypeUUID:
            new_value = value.asUUID();
            break;

            //      case LLSD::TypeBoolean:
            //      case LLSD::TypeString:
            //      case LLSD::TypeURI:
            //      case LLSD::TypeBinary:
            //      case LLSD::TypeDate:
        default:
            // atomic or unknown data types. Lerping between them does not make sense so switch at the break.
            new_value = (mix > BREAK_POINT) ? other_value : value;
            break;
    }

    return new_value;
}

LLSettingsBase::stringset_t LLSettingsBase::getSkipInterpolateKeys() const
{
    static stringset_t skipSet;

    if (skipSet.empty())
    {
        skipSet.insert(SETTING_FLAGS);
        skipSet.insert(SETTING_HASH);
    }

    return skipSet;
}

LLSD LLSettingsBase::getSettings() const
{
    return mSettings;
}

LLSD LLSettingsBase::cloneSettings() const
{
    U32 flags = getFlags();
    LLSD settings (combineSDMaps(getSettings(), LLSD()));
    if (flags)
        settings[SETTING_FLAGS] = LLSD::Integer(flags);
    return settings;
}

size_t LLSettingsBase::getHash() const
{   // get a shallow copy of the LLSD filtering out values to not include in the hash
    LLSD hash_settings = llsd_shallow(getSettings(),
        LLSDMap(SETTING_NAME, false)(SETTING_ID, false)(SETTING_HASH, false)("*", true));

    boost::hash<LLSD> hasher;
    return hasher(hash_settings);
}

bool LLSettingsBase::validate()
{
    validation_list_t validations = getValidationList();

    if (!mSettings.has(SETTING_TYPE))
    {
        mSettings[SETTING_TYPE] = getSettingsType();
    }

    LLSD result = LLSettingsBase::settingValidation(mSettings, validations);

    if (result["errors"].size() > 0)
    {
        LL_WARNS("SETTINGS") << "Validation errors: " << result["errors"] << LL_ENDL;
    }
    if (result["warnings"].size() > 0)
    {
        LL_DEBUGS("SETTINGS") << "Validation warnings: " << result["warnings"] << LL_ENDL;
    }

    return result["success"].asBoolean();
}

LLSD LLSettingsBase::settingValidation(LLSD &settings, validation_list_t &validations, bool partial)
{
    using boost::placeholders::_1, boost::placeholders::_2;
    static Validator  validateName(SETTING_NAME, false, LLSD::TypeString, boost::bind(&Validator::verifyStringLength, _1, _2, 63));
    static Validator  validateId(SETTING_ID, false, LLSD::TypeUUID);
    static Validator  validateHash(SETTING_HASH, false, LLSD::TypeInteger);
    static Validator  validateType(SETTING_TYPE, false, LLSD::TypeString);
    static Validator  validateAssetId(SETTING_ASSETID, false, LLSD::TypeUUID);
    static Validator  validateFlags(SETTING_FLAGS, false, LLSD::TypeInteger);
    stringset_t       validated;
    stringset_t       strip;
    bool              isValid(true);
    LLSD              errors(LLSD::emptyArray());
    LLSD              warnings(LLSD::emptyArray());
    U32               flags(0);

    if (partial)
        flags |= Validator::VALIDATION_PARTIAL;

    // Fields common to all settings.
    if (!validateName.verify(settings, flags))
    {
        errors.append( LLSD::String("Unable to validate 'name'.") );
        isValid = false;
    }
    validated.insert(validateName.getName());

    if (!validateId.verify(settings, flags))
    {
        errors.append( LLSD::String("Unable to validate 'id'.") );
        isValid = false;
    }
    validated.insert(validateId.getName());

    if (!validateHash.verify(settings, flags))
    {
        errors.append( LLSD::String("Unable to validate 'hash'.") );
        isValid = false;
    }
    validated.insert(validateHash.getName());

    if (!validateAssetId.verify(settings, flags))
    {
        errors.append(LLSD::String("Invalid asset Id"));
        isValid = false;
    }
    validated.insert(validateAssetId.getName());

    if (!validateType.verify(settings, flags))
    {
        errors.append( LLSD::String("Unable to validate 'type'.") );
        isValid = false;
    }
    validated.insert(validateType.getName());

    if (!validateFlags.verify(settings, flags))
    {
        errors.append(LLSD::String("Unable to validate 'flags'."));
        isValid = false;
    }
    validated.insert(validateFlags.getName());

    // Fields for specific settings.
    for (validation_list_t::iterator itv = validations.begin(); itv != validations.end(); ++itv)
    {
#ifdef VALIDATION_DEBUG
        LLSD oldvalue;
        if (settings.has((*itv).getName()))
        {
            oldvalue = llsd_clone(mSettings[(*itv).getName()]);
        }
#endif

        if (!(*itv).verify(settings, flags))
        {
            std::stringstream errtext;

            errtext << "Settings LLSD fails validation and could not be corrected for '" << (*itv).getName() << "'!\n";
            errors.append( errtext.str() );
            isValid = false;
        }
        validated.insert((*itv).getName());

#ifdef VALIDATION_DEBUG
        if (!oldvalue.isUndefined())
        {
            if (!compare_llsd(settings[(*itv).getName()], oldvalue))
            {
                LL_WARNS("SETTINGS") << "Setting '" << (*itv).getName() << "' was changed: " << oldvalue << " -> " << settings[(*itv).getName()] << LL_ENDL;
            }
        }
#endif
    }

    // strip extra entries
    for (LLSD::map_const_iterator itm = settings.beginMap(); itm != settings.endMap(); ++itm)
    {
        if (validated.find((*itm).first) == validated.end())
        {
            std::stringstream warntext;

            warntext << "Stripping setting '" << (*itm).first << "'";
            warnings.append( warntext.str() );
            strip.insert((*itm).first);
        }
    }

    for (stringset_t::iterator its = strip.begin(); its != strip.end(); ++its)
    {
        settings.erase(*its);
    }

    return LLSDMap("success", LLSD::Boolean(isValid))
        ("errors", errors)
        ("warnings", warnings);
}

//=========================================================================

bool LLSettingsBase::Validator::verify(LLSD &data, U32 flags)
{
    if (!data.has(mName) || (data.has(mName) && data[mName].isUndefined()))
    {
        if ((flags & VALIDATION_PARTIAL) != 0) // we are doing a partial validation.  Do no attempt to set a default if missing (or fail even if required)
            return true;

        if (!mDefault.isUndefined())
        {
            data[mName] = mDefault;
            return true;
        }
        if (mRequired)
            LL_WARNS("SETTINGS") << "Missing required setting '" << mName << "' with no default." << LL_ENDL;
        return !mRequired;
    }

    if (data[mName].type() != mType)
    {
        LL_WARNS("SETTINGS") << "Setting '" << mName << "' is incorrect type." << LL_ENDL;
        return false;
    }

    if (!mVerify.empty() && !mVerify(data[mName], flags))
    {
        LL_WARNS("SETTINGS") << "Setting '" << mName << "' fails validation." << LL_ENDL;
        return false;
    }

    return true;
}

bool LLSettingsBase::Validator::verifyColor(LLSD &value, U32)
{
    return (value.size() == 3 || value.size() == 4);
}

bool LLSettingsBase::Validator::verifyVector(LLSD &value, U32, S32 length)
{
    return (value.size() == length);
}

bool LLSettingsBase::Validator::verifyVectorNormalized(LLSD &value, U32, S32 length)
{
    if (value.size() != length)
        return false;

    LLSD newvector;

    switch (length)
    {
    case 2:
    {
        LLVector2 vect(value);

        if (is_approx_equal(vect.normalize(), 1.0f))
            return true;
        newvector = vect.getValue();
        break;
    }
    case 3:
    {
        LLVector3 vect(value);

        if (is_approx_equal(vect.normalize(), 1.0f))
            return true;
        newvector = vect.getValue();
        break;
    }
    case 4:
    {
        LLVector4 vect(value);

        if (is_approx_equal(vect.normalize(), 1.0f))
            return true;
        newvector = vect.getValue();
        break;
    }
    default:
        return false;
    }

    return true;
}

bool LLSettingsBase::Validator::verifyVectorMinMax(LLSD &value, U32, LLSD minvals, LLSD maxvals)
{
    for (S32 index = 0; index < value.size(); ++index)
    {
        if (minvals[index].asString() != "*")
        {
            if (minvals[index].asReal() > value[index].asReal())
            {
                value[index] = minvals[index].asReal();
            }
        }
        if (maxvals[index].asString() != "*")
        {
            if (maxvals[index].asReal() < value[index].asReal())
            {
                value[index] = maxvals[index].asReal();
            }
        }
    }

    return true;
}

bool LLSettingsBase::Validator::verifyQuaternion(LLSD &value, U32)
{
    return (value.size() == 4);
}

bool LLSettingsBase::Validator::verifyQuaternionNormal(LLSD &value, U32)
{
    if (value.size() != 4)
        return false;

    LLQuaternion quat(value);

    if (is_approx_equal(quat.normalize(), 1.0f))
        return true;

    LLSD newquat = quat.getValue();
    for (S32 index = 0; index < 4; ++index)
    {
        value[index] = newquat[index];
    }
    return true;
}

bool LLSettingsBase::Validator::verifyFloatRange(LLSD &value, U32, LLSD range)
{
    F64 real = value.asReal();

    F64 clampedval = llclamp(LLSD::Real(real), range[0].asReal(), range[1].asReal());

    if (is_approx_equal(clampedval, real))
        return true;

    value = LLSD::Real(clampedval);
    return true;
}

bool LLSettingsBase::Validator::verifyIntegerRange(LLSD &value, U32, LLSD range)
{
    S32 ival = value.asInteger();

    S32 clampedval = llclamp(LLSD::Integer(ival), range[0].asInteger(), range[1].asInteger());

    if (clampedval == ival)
        return true;

    value = LLSD::Integer(clampedval);
    return true;
}

bool LLSettingsBase::Validator::verifyStringLength(LLSD &value, U32, S32 length)
{
    std::string sval = value.asString();

    if (!sval.empty())
    {
        sval = sval.substr(0, length);
        value = LLSD::String(sval);
    }
    return true;
}

//=========================================================================
void LLSettingsBlender::update(const LLSettingsBase::BlendFactor& blendf)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_ENVIRONMENT;
    F64 res = setBlendFactor(blendf);
    llassert(res >= 0.0 && res <= 1.0);
    (void)res;
    mTarget->update();
}

F64 LLSettingsBlender::setBlendFactor(const LLSettingsBase::BlendFactor& blendf_in)
{
    LLSettingsBase::TrackPosition blendf = blendf_in;
    llassert(!isnan(blendf));
    if (blendf >= 1.0)
    {
        triggerComplete();
    }
    blendf = llclamp(blendf, 0.0f, 1.0f);

    if (mTarget)
    {
        mTarget->replaceSettings(mInitial->getSettings());
        mTarget->blend(mFinal, blendf);
    }
    else
    {
        LL_WARNS("SETTINGS") << "No target for settings blender." << LL_ENDL;
    }

    return blendf;
}

void LLSettingsBlender::triggerComplete()
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_ENVIRONMENT;
    if (mTarget)
        mTarget->replaceSettings(mFinal->getSettings());
    LLSettingsBlender::ptr_t hold = shared_from_this();   // prevents this from deleting too soon
    mTarget->update();
    mOnFinished(shared_from_this());
}

//-------------------------------------------------------------------------
const LLSettingsBase::BlendFactor LLSettingsBlenderTimeDelta::MIN_BLEND_DELTA(FLT_EPSILON);

LLSettingsBase::BlendFactor LLSettingsBlenderTimeDelta::calculateBlend(const LLSettingsBase::TrackPosition& spanpos, const LLSettingsBase::TrackPosition& spanlen) const
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_ENVIRONMENT;
    return LLSettingsBase::BlendFactor(fmod((F64)spanpos, (F64)spanlen) / (F64)spanlen);
}

bool LLSettingsBlenderTimeDelta::applyTimeDelta(const LLSettingsBase::Seconds& timedelta)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_ENVIRONMENT;
    mTimeSpent += timedelta;

    if (mTimeSpent > mBlendSpan)
    {
        triggerComplete();
        return false;
    }

    LLSettingsBase::BlendFactor blendf = calculateBlend(mTimeSpent, mBlendSpan);

    if (fabs(mLastBlendF - blendf) < mBlendFMinDelta)
    {
        return false;
    }

    mLastBlendF = blendf;
    update(blendf);
    return true;
}