/**
 * @file llcurrencyuimanager.cpp
 * @brief LLCurrencyUIManager class implementation
 *
 * $LicenseInfo:firstyear=2006&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 "lluictrlfactory.h"
#include "lltextbox.h"
#include "lllineeditor.h"
#include "llresmgr.h" // for LLLocale
#include "lltrans.h"
#include "llviewercontrol.h"
#include "llversioninfo.h"

#include "llcurrencyuimanager.h"

// viewer includes
#include "llagent.h"
#include "llconfirmationmanager.h"
#include "llframetimer.h"
#include "lllineeditor.h"
#include "llviewchildren.h"
#include "llxmlrpctransaction.h"
#include "llviewernetwork.h"
#include "llpanel.h"
#include "stringize.h"


const F64 CURRENCY_ESTIMATE_FREQUENCY = 2.0;
    // how long of a pause in typing a currency buy amount before an
    // esimate is fetched from the server

class LLCurrencyUIManager::Impl
{
public:
    Impl(LLPanel& dialog);
    virtual ~Impl();

    LLPanel&        mPanel;

    bool            mHidden;

    bool            mError;
    std::string     mErrorMessage;
    std::string     mErrorURI;

    std::string     mZeroMessage;

    // user's choices
    S32             mUserCurrencyBuy;
    bool            mUserEnteredCurrencyBuy;

    // from website

    // pre-viewer 2.0, the server returned estimates as an
    // integer US cents value, e.g., "1000" for $10.00
    // post-viewer 2.0, the server may also return estimates
    // as a string with currency embedded, e.g., "10.00 Euros"
    bool            mUSDCurrencyEstimated;
    S32             mUSDCurrencyEstimatedCost;
    bool            mLocalCurrencyEstimated;
    std::string     mLocalCurrencyEstimatedCost;
    bool            mSupportsInternationalBilling;
    std::string     mSiteConfirm;

    bool            mBought;

    enum TransactionType
    {
        TransactionNone,
        TransactionCurrency,
        TransactionBuy
    };

    TransactionType      mTransactionType;
    LLXMLRPCTransaction* mTransaction;

    bool         mCurrencyChanged;
    LLFrameTimer mCurrencyKeyTimer;


    void updateCurrencyInfo();
    void finishCurrencyInfo();

    void startCurrencyBuy(const std::string& password);
    void finishCurrencyBuy();

    void clearEstimate();
    bool hasEstimate() const;
    std::string getLocalEstimate() const;

    void startTransaction(TransactionType type, const char* method, const LLSD& params);

    // return true if update needed
    bool checkTransaction();

    void setError(const std::string& message, const std::string& uri);
    void clearError();

    // return true if update needed
    bool considerUpdateCurrency();

    void currencyKey(S32);
    static void onCurrencyKey(LLLineEditor* caller, void* data);

    void prepare();
    void updateUI();
};

// is potentially not fully constructed.
LLCurrencyUIManager::Impl::Impl(LLPanel& dialog)
    : mPanel(dialog),
    mHidden(false),
    mError(false),
    mUserCurrencyBuy(2000), // note, this is a default, real value set in llfloaterbuycurrency.cpp
    mUserEnteredCurrencyBuy(false),
    mSupportsInternationalBilling(false),
    mBought(false),
    mTransactionType(TransactionNone), mTransaction(0),
    mCurrencyChanged(false)
{
    clearEstimate();
}

LLCurrencyUIManager::Impl::~Impl()
{
    delete mTransaction;
}

void LLCurrencyUIManager::Impl::updateCurrencyInfo()
{
    clearEstimate();
    mBought = false;
    mCurrencyChanged = false;

    if (mUserCurrencyBuy == 0)
    {
        mLocalCurrencyEstimated = true;
        return;
    }

    const LLVersionInfo& vi(LLVersionInfo::instance());

    LLSD params = LLSD::emptyMap();
    params["agentId"] = gAgent.getID().asString();
    params["secureSessionId"] = gAgent.getSecureSessionID().asString();
    params["language"] = LLUI::getLanguage();
    params["currencyBuy"] = mUserCurrencyBuy;
    params["viewerChannel"] = vi.getChannel();
    params["viewerMajorVersion"] = vi.getMajor();
    params["viewerMinorVersion"] = vi.getMinor();
    params["viewerPatchVersion"] = vi.getPatch();
    // With GitHub builds, the build number is too big to fit in a 32-bit int,
    // and XMLRPC value doesn't deal with integers wider than int. Use string.
    params["viewerBuildVersion"] = std::to_string(vi.getBuild());

    startTransaction(TransactionCurrency, "getCurrencyQuote", params);
}

void LLCurrencyUIManager::Impl::finishCurrencyInfo()
{
    const LLSD& result = mTransaction->response();

    bool success = result["success"].asBoolean();
    if (!success)
    {
        setError(
            result["errorMessage"].asString(),
            result["errorURI"].asString()
        );
        return;
    }

    const LLSD& currency = result["currency"];

    // old XML-RPC server: estimatedCost = value in US cents
    mUSDCurrencyEstimated = currency.has("estimatedCost");
    if (mUSDCurrencyEstimated)
    {
        mUSDCurrencyEstimatedCost = currency["estimatedCost"].asInteger();
    }

    // newer XML-RPC server: estimatedLocalCost = local currency string
    mLocalCurrencyEstimated = currency.has("estimatedLocalCost");
    if (mLocalCurrencyEstimated)
    {
        mLocalCurrencyEstimatedCost = currency["estimatedLocalCost"].asString();
        mSupportsInternationalBilling = true;
    }

    S32 newCurrencyBuy = currency["currencyBuy"].asInteger();
    if (newCurrencyBuy != mUserCurrencyBuy)
    {
        mUserCurrencyBuy = newCurrencyBuy;
        mUserEnteredCurrencyBuy = false;
    }

    mSiteConfirm = result["confirm"].asString();
}

void LLCurrencyUIManager::Impl::startCurrencyBuy(const std::string& password)
{
    const LLVersionInfo& vi(LLVersionInfo::instance());

    LLSD params = LLSD::emptyMap();
    params["agentId"] = gAgent.getID().asString();
    params["secureSessionId"] = gAgent.getSecureSessionID().asString();
    params["language"] = LLUI::getLanguage();
    params["currencyBuy"] = mUserCurrencyBuy;
    params["confirm"] = mSiteConfirm;
    params["viewerChannel"] = vi.getChannel();
    params["viewerMajorVersion"] = vi.getMajor();
    params["viewerMinorVersion"] = vi.getMinor();
    params["viewerPatchVersion"] = vi.getPatch();
    // With GitHub builds, the build number is too big to fit in a 32-bit int,
    // and XMLRPC value doesn't deal with integers wider than int. Use string.
    params["viewerBuildVersion"] = std::to_string(vi.getBuild());

    if (mUSDCurrencyEstimated)
    {
        params["estimatedCost"] = mUSDCurrencyEstimatedCost;
    }

    if (mLocalCurrencyEstimated)
    {
        params["estimatedLocalCost"] = mLocalCurrencyEstimatedCost;
    }

    if (!password.empty())
    {
        params["password"] = password;
    }

    startTransaction(TransactionBuy, "buyCurrency", params);

    clearEstimate();
    mCurrencyChanged = false;
}

void LLCurrencyUIManager::Impl::finishCurrencyBuy()
{
    const LLSD& result = mTransaction->response();

    bool success = result["success"].asBoolean();
    if (!success)
    {
        setError(
            result["errorMessage"].asString(),
            result["errorURI"].asString()
        );
    }
    else
    {
        mUserCurrencyBuy = 0;
        mUserEnteredCurrencyBuy = false;
        mBought = true;
    }
}

void LLCurrencyUIManager::Impl::startTransaction(TransactionType type,
        const char* method, const LLSD& params)
{
    static std::string transactionURI;
    if (transactionURI.empty())
    {
        transactionURI = LLGridManager::getInstance()->getHelperURI() + "currency.php";
    }

    delete mTransaction;

    mTransactionType = type;
    mTransaction = new LLXMLRPCTransaction(transactionURI, method, params);

    clearError();
}

void LLCurrencyUIManager::Impl::clearEstimate()
{
    mUSDCurrencyEstimated = false;
    mUSDCurrencyEstimatedCost = 0;
    mLocalCurrencyEstimated = false;
    mLocalCurrencyEstimatedCost = "0";
}

bool LLCurrencyUIManager::Impl::hasEstimate() const
{
    return (mUSDCurrencyEstimated || mLocalCurrencyEstimated);
}

std::string LLCurrencyUIManager::Impl::getLocalEstimate() const
{
    if (mLocalCurrencyEstimated)
    {
        // we have the new-style local currency string
        return mLocalCurrencyEstimatedCost;
    }
    if (mUSDCurrencyEstimated)
    {
        // we have the old-style USD-specific value
        LLStringUtil::format_map_t args;
        {
            LLLocale locale_override(LLStringUtil::getLocale());
            args["[AMOUNT]"] = llformat("%#.2f", mUSDCurrencyEstimatedCost / 100.0);
        }
        return LLTrans::getString("LocalEstimateUSD", args);
    }
    return "";
}

bool LLCurrencyUIManager::Impl::checkTransaction()
{
    if (!mTransaction)
    {
        return false;
    }

    if (!mTransaction->process())
    {
        return false;
    }

    if (mTransaction->status(NULL) != LLXMLRPCTransaction::StatusComplete)
    {
        setError(mTransaction->statusMessage(), mTransaction->statusURI());
    }
    else
    {
        switch (mTransactionType)
        {
        case TransactionCurrency:
            finishCurrencyInfo();
            break;
        case TransactionBuy:
            finishCurrencyBuy();
            break;
        default:;
        }
    }

    delete mTransaction;
    mTransaction = NULL;
    mTransactionType = TransactionNone;

    return true;
}

void LLCurrencyUIManager::Impl::setError(
    const std::string& message, const std::string& uri)
{
    mError = true;
    mErrorMessage = message;
    mErrorURI = uri;
}

void LLCurrencyUIManager::Impl::clearError()
{
    mError = false;
    mErrorMessage.clear();
    mErrorURI.clear();
}

bool LLCurrencyUIManager::Impl::considerUpdateCurrency()
{
    if (mCurrencyChanged && !mTransaction &&
        mCurrencyKeyTimer.getElapsedTimeF32() >= CURRENCY_ESTIMATE_FREQUENCY)
    {
        updateCurrencyInfo();
        return true;
    }

    return false;
}

void LLCurrencyUIManager::Impl::currencyKey(S32 value)
{
    mUserEnteredCurrencyBuy = true;
    mCurrencyKeyTimer.reset();

    if (mUserCurrencyBuy == value)
    {
        return;
    }

    mUserCurrencyBuy = value;

    if (hasEstimate())
    {
        clearEstimate();
        //cannot just simply refresh the whole UI, as the edit field will
        // get reset and the cursor will change...

        mPanel.getChildView("currency_est")->setVisible(false);
        mPanel.getChildView("getting_data")->setVisible(true);
    }

    mCurrencyChanged = true;
}

// static
void LLCurrencyUIManager::Impl::onCurrencyKey(LLLineEditor* caller, void* data)
{
    S32 value = atoi(caller->getText().c_str());
    LLCurrencyUIManager::Impl* self = (LLCurrencyUIManager::Impl*)data;
    self->currencyKey(value);
}

void LLCurrencyUIManager::Impl::prepare()
{
    LLLineEditor* lindenAmount = mPanel.getChild<LLLineEditor>("currency_amt");
    if (lindenAmount)
    {
        lindenAmount->setPrevalidate(LLTextValidate::validateNonNegativeS32);
        lindenAmount->setKeystrokeCallback(onCurrencyKey, this);
    }
}

void LLCurrencyUIManager::Impl::updateUI()
{
    if (mHidden)
    {
        mPanel.getChildView("currency_action")->setVisible(false);
        mPanel.getChildView("currency_amt")->setVisible(false);
        mPanel.getChildView("currency_est")->setVisible(false);
        return;
    }

    mPanel.getChildView("currency_action")->setVisible(true);

    LLLineEditor* lindenAmount = mPanel.getChild<LLLineEditor>("currency_amt");
    if (lindenAmount)
    {
        lindenAmount->setVisible(true);
        lindenAmount->setLabel(mZeroMessage);

        if (!mUserEnteredCurrencyBuy)
        {
            if (mUserCurrencyBuy == 0)
            {
                lindenAmount->setText(LLStringUtil::null);
            }
            else
            {
                lindenAmount->setText(llformat("%d", mUserCurrencyBuy));
            }

            lindenAmount->selectAll();
        }
    }

    std::string estimated = (mUserCurrencyBuy == 0) ? mPanel.getString("estimated_zero") : getLocalEstimate();
    mPanel.getChild<LLUICtrl>("currency_est")->setTextArg("[LOCALAMOUNT]", estimated);
    mPanel.getChildView("currency_est")->setVisible( hasEstimate() || mUserCurrencyBuy == 0);

    mPanel.getChildView("currency_links")->setVisible( mSupportsInternationalBilling);
    mPanel.getChildView("exchange_rate_note")->setVisible( mSupportsInternationalBilling);

    if (mPanel.getChildView("buy_btn")->getEnabled()
        ||mPanel.getChildView("currency_est")->getVisible()
        || mPanel.getChildView("error_web")->getVisible())
    {
        mPanel.getChildView("getting_data")->setVisible(false);
    }
}



LLCurrencyUIManager::LLCurrencyUIManager(LLPanel& dialog)
    : impl(* new Impl(dialog))
{
}

LLCurrencyUIManager::~LLCurrencyUIManager()
{
    delete &impl;
}

void LLCurrencyUIManager::setAmount(int amount, bool noEstimate)
{
    impl.mUserCurrencyBuy = amount;
    impl.mUserEnteredCurrencyBuy = false;
    impl.updateUI();

    impl.mCurrencyChanged = !noEstimate;
}

int LLCurrencyUIManager::getAmount()
{
    return impl.mUserCurrencyBuy;
}

void LLCurrencyUIManager::setZeroMessage(const std::string& message)
{
    impl.mZeroMessage = message;
}

void LLCurrencyUIManager::setUSDEstimate(int amount)
{
    impl.mUSDCurrencyEstimatedCost = amount;
    impl.mUSDCurrencyEstimated = true;
    impl.updateUI();

    impl.mCurrencyChanged = false;
}

int LLCurrencyUIManager::getUSDEstimate()
{
    return impl.mUSDCurrencyEstimated ? impl.mUSDCurrencyEstimatedCost : 0;
}

void LLCurrencyUIManager::setLocalEstimate(const std::string &amount)
{
    impl.mLocalCurrencyEstimatedCost = amount;
    impl.mLocalCurrencyEstimated = true;
    impl.updateUI();

    impl.mCurrencyChanged = false;
}

std::string LLCurrencyUIManager::getLocalEstimate() const
{
    return impl.getLocalEstimate();
}

void LLCurrencyUIManager::prepare()
{
    impl.prepare();
}

void LLCurrencyUIManager::updateUI(bool show)
{
    impl.mHidden = !show;
    impl.updateUI();
}

bool LLCurrencyUIManager::process()
{
    bool changed = false;
    changed |= impl.checkTransaction();
    changed |= impl.considerUpdateCurrency();
    return changed;
}

void LLCurrencyUIManager::buy(const std::string& buy_msg)
{
    if (!canBuy())
    {
        return;
    }

    LLUIString msg = buy_msg;
    msg.setArg("[LINDENS]", llformat("%d", impl.mUserCurrencyBuy));
    msg.setArg("[LOCALAMOUNT]", getLocalEstimate());
    LLConfirmationManager::confirm(impl.mSiteConfirm,
                                   msg,
                                   impl,
                                   &LLCurrencyUIManager::Impl::startCurrencyBuy);
}


bool LLCurrencyUIManager::inProcess()
{
    return impl.mTransactionType != Impl::TransactionNone;
}

bool LLCurrencyUIManager::canCancel()
{
    return !buying();
}

bool LLCurrencyUIManager::canBuy()
{
    return !inProcess() && impl.hasEstimate() && impl.mUserCurrencyBuy > 0;
}

bool LLCurrencyUIManager::buying()
{
    return impl.mTransactionType == Impl::TransactionBuy;
}

bool LLCurrencyUIManager::bought()
{
    return impl.mBought;
}

void LLCurrencyUIManager::clearError()
{
    impl.clearError();
}

bool LLCurrencyUIManager::hasError()
{
    return impl.mError;
}

std::string LLCurrencyUIManager::errorMessage()
{
    return impl.mErrorMessage;
}

std::string LLCurrencyUIManager::errorURI()
{
    return impl.mErrorURI;
}