/**
 * @file llimagej2c.cpp
 *
 * $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 "llapr.h"
#include "lldir.h"
#include "llimagej2c.h"
#include "lltimer.h"
#include "llmath.h"
#include "llmemory.h"
#include "llsd.h"
#include <boost/scoped_ptr.hpp>

// Declare the prototype for this factory function here. It is implemented in
// other files which define a LLImageJ2CImpl subclass, but only ONE static
// library which has the implementation for this function should ever be
// linked.
LLImageJ2CImpl* fallbackCreateLLImageJ2CImpl();

// Test data gathering handle
LLImageCompressionTester* LLImageJ2C::sTesterp = NULL ;
const std::string sTesterName("ImageCompressionTester");

//static
std::string LLImageJ2C::getEngineInfo()
{
    // All known LLImageJ2CImpl implementation subclasses are cheap to
    // construct.
    std::unique_ptr<LLImageJ2CImpl> impl(fallbackCreateLLImageJ2CImpl());
    return impl->getEngineInfo();
}

LLImageJ2C::LLImageJ2C() :  LLImageFormatted(IMG_CODEC_J2C),
                            mMaxBytes(0),
                            mRawDiscardLevel(-1),
                            mRate(DEFAULT_COMPRESSION_RATE),
                            mReversible(false),
                            mAreaUsedForDataSizeCalcs(0)
{
    mImpl.reset(fallbackCreateLLImageJ2CImpl());

    // Clear data size table
    for( S32 i = 0; i <= MAX_DISCARD_LEVEL; i++)
    {   // Array size is MAX_DISCARD_LEVEL+1
        mDataSizes[i] = 0;
    }

    // If that test log has ben requested but not yet created, create it
    if (LLMetricPerformanceTesterBasic::isMetricLogRequested(sTesterName) && !LLMetricPerformanceTesterBasic::getTester(sTesterName))
    {
        sTesterp = new LLImageCompressionTester() ;
        if (!sTesterp->isValid())
        {
            delete sTesterp;
            sTesterp = NULL;
        }
    }
}

// virtual
LLImageJ2C::~LLImageJ2C() {}

// virtual
void LLImageJ2C::resetLastError()
{
    mLastError.clear();
}

//virtual
void LLImageJ2C::setLastError(const std::string& message, const std::string& filename)
{
    mLastError = message;
    if (!filename.empty())
        mLastError += std::string(" FILE: ") + filename;
}

// virtual
S8  LLImageJ2C::getRawDiscardLevel()
{
    return mRawDiscardLevel;
}

bool LLImageJ2C::updateData()
{
    bool res = true;
    resetLastError();

    // Check to make sure that this instance has been initialized with data
    if (!getData() || (getDataSize() < 16))
    {
        setLastError("LLImageJ2C uninitialized");
        res = false;
    }
    else
    {
        res = mImpl->getMetadata(*this);
    }

    if (res)
    {
        // SJB: override discard based on mMaxBytes elsewhere
        S32 max_bytes = getDataSize(); // mMaxBytes ? mMaxBytes : getDataSize();
        S32 discard = calcDiscardLevelBytes(max_bytes);
        setDiscardLevel(discard);
    }

    if (!mLastError.empty())
    {
        LLImage::setLastError(mLastError);
    }
    return res;
}

bool LLImageJ2C::initDecode(LLImageRaw &raw_image, int discard_level, int* region)
{
    setDiscardLevel(discard_level != -1 ? discard_level : 0);
    return mImpl->initDecode(*this,raw_image,discard_level,region);
}

bool LLImageJ2C::initEncode(LLImageRaw &raw_image, int blocks_size, int precincts_size, int levels)
{
    return mImpl->initEncode(*this,raw_image,blocks_size,precincts_size,levels);
}

bool LLImageJ2C::decode(LLImageRaw *raw_imagep, F32 decode_time)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
    return decodeChannels(raw_imagep, decode_time, 0, 4);
}


// Returns true to mean done, whether successful or not.
bool LLImageJ2C::decodeChannels(LLImageRaw *raw_imagep, F32 decode_time, S32 first_channel, S32 max_channel_count )
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE;
    LLTimer elapsed;

    bool res = true;

    resetLastError();

    // Check to make sure that this instance has been initialized with data
    if (!getData() || (getDataSize() < 16))
    {
        setLastError("LLImageJ2C uninitialized");
        res = true; // done
    }
    else
    {
        // Update the raw discard level
        updateRawDiscardLevel();
        mDecoding = true;
        res = mImpl->decodeImpl(*this, *raw_imagep, decode_time, first_channel, max_channel_count);
    }

    if (res)
    {
        if (!mDecoding)
        {
            // Failed
            raw_imagep->deleteData();
        }
        else
        {
            mDecoding = false;
        }
    }

    if (!mLastError.empty())
    {
        LLImage::setLastError(mLastError);
    }

    LLImageCompressionTester* tester = (LLImageCompressionTester*)LLMetricPerformanceTesterBasic::getTester(sTesterName);
    if (tester)
    {
        // Decompression stat gathering
        // Note that we *do not* take into account the decompression failures data so we might overestimate the time spent processing

        // Always add the decompression time to the stat
        tester->updateDecompressionStats(elapsed.getElapsedTimeF32()) ;
        if (res)
        {
            // The whole data stream is finally decompressed when res is returned as true
            tester->updateDecompressionStats(this->getDataSize(), raw_imagep->getDataSize()) ;
        }
    }

    return res;
}


bool LLImageJ2C::encode(const LLImageRaw *raw_imagep, F32 encode_time)
{
    return encode(raw_imagep, NULL, encode_time);
}


bool LLImageJ2C::encode(const LLImageRaw *raw_imagep, const char* comment_text, F32 encode_time)
{
    LLTimer elapsed;
    resetLastError();
    bool res = mImpl->encodeImpl(*this, *raw_imagep, comment_text, encode_time, mReversible);
    if (!mLastError.empty())
    {
        LLImage::setLastError(mLastError);
    }

    LLImageCompressionTester* tester = (LLImageCompressionTester*)LLMetricPerformanceTesterBasic::getTester(sTesterName);
    if (tester)
    {
        // Compression stat gathering
        // Note that we *do not* take into account the compression failures cases so we night overestimate the time spent processing

        // Always add the compression time to the stat
        tester->updateCompressionStats(elapsed.getElapsedTimeF32()) ;
        if (res)
        {
            // The whole data stream is finally compressed when res is returned as true
            tester->updateCompressionStats(this->getDataSize(), raw_imagep->getDataSize()) ;
        }
    }

    return res;
}

//static
S32 LLImageJ2C::calcHeaderSizeJ2C()
{
    return FIRST_PACKET_SIZE; // Hack. just needs to be >= actual header size...
}

//static
S32 LLImageJ2C::calcDataSizeJ2C(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate)
{
    // Note: This provides an estimation for the first to last quality layer of a given discard level
    // This is however an efficient approximation, as the true discard level boundary would be
    // in general too big for fast fetching.
    // For details about the equation used here, see https://wiki.lindenlab.com/wiki/THX1138_KDU_Improvements#Byte_Range_Study

    // Estimate the number of layers. This is consistent with what's done for j2c encoding in LLImageJ2CKDU::encodeImpl().
    S32 nb_layers = 1;
    S32 surface = w*h;
    S32 s = 64*64;
    while (surface > s)
    {
        nb_layers++;
        s *= 4;
    }
    F32 layer_factor =  3.0f * (7 - llclamp(nb_layers,1,6));

    // Compute w/pow(2,discard_level) and h/pow(2,discard_level)
    w >>= discard_level;
    h >>= discard_level;
    w = llmax(w, 1);
    h = llmax(h, 1);

    // Temporary: compute both new and old range and pick one according to the settings TextureNewByteRange
    // *TODO: Take the old code out once we have enough tests done
    S32 bytes;
    S32 new_bytes = (S32) (sqrt((F32)(w*h))*(F32)(comp)*rate*1000.f/layer_factor);
    S32 old_bytes = (S32)((F32)(w*h*comp)*rate);
    bytes = (LLImage::useNewByteRange() && (new_bytes < old_bytes) ? new_bytes : old_bytes);
    bytes = llmax(bytes, calcHeaderSizeJ2C());
    return bytes;
}

S32 LLImageJ2C::calcHeaderSize()
{
    return calcHeaderSizeJ2C();
}

// calcDataSize() returns how many bytes to read to load discard_level (including header)
S32 LLImageJ2C::calcDataSize(S32 discard_level)
{
    discard_level = llclamp(discard_level, 0, MAX_DISCARD_LEVEL);
    if ( mAreaUsedForDataSizeCalcs != (getHeight() * getWidth())
        || (mDataSizes[0] == 0))
    {
        mAreaUsedForDataSizeCalcs = getHeight() * getWidth();

        S32 level = MAX_DISCARD_LEVEL;  // Start at the highest discard
        while ( level >= 0 )
        {
            mDataSizes[level] = calcDataSizeJ2C(getWidth(), getHeight(), getComponents(), level, mRate);
            level--;
        }
    }
    return mDataSizes[discard_level];
}

S32 LLImageJ2C::calcDiscardLevelBytes(S32 bytes)
{
    llassert(bytes >= 0);
    S32 discard_level = 0;
    if (bytes == 0)
    {
        return MAX_DISCARD_LEVEL;
    }
    while (1)
    {
        S32 bytes_needed = calcDataSize(discard_level);
        // Use TextureReverseByteRange percent (see settings.xml) of the optimal size to qualify as correct rendering for the given discard level
        if (bytes >= (bytes_needed*LLImage::getReverseByteRangePercent()/100))
        {
            break;
        }
        discard_level++;
        if (discard_level >= MAX_DISCARD_LEVEL)
        {
            break;
        }
    }
    return discard_level;
}

void LLImageJ2C::setMaxBytes(S32 max_bytes)
{
    mMaxBytes = max_bytes;
}

void LLImageJ2C::setReversible(const bool reversible)
{
    mReversible = reversible;
}


bool LLImageJ2C::loadAndValidate(const std::string &filename)
{
    bool res = true;

    resetLastError();

    S32 file_size = 0;
    LLAPRFile infile ;
    infile.open(filename, LL_APR_RB, NULL, &file_size);
    apr_file_t* apr_file = infile.getFileHandle() ;
    if (!apr_file)
    {
        setLastError("Unable to open file for reading", filename);
        res = false;
    }
    else if (file_size == 0)
    {
        setLastError("File is empty",filename);
        res = false;
    }
    else
    {
        U8 *data = (U8*)ll_aligned_malloc_16(file_size);
        if (!data)
        {
            infile.close();
            setLastError("Out of memory", filename);
            res = false;
        }
        else
        {
            apr_size_t bytes_read = file_size;
            apr_status_t s = apr_file_read(apr_file, data, &bytes_read); // modifies bytes_read
            infile.close();

            if (s != APR_SUCCESS || (S32)bytes_read != file_size)
            {
                ll_aligned_free_16(data);
                setLastError("Unable to read entire file");
                res = false;
            }
            else
            {
                res = validate(data, file_size);
            }
        }
    }

    if (!mLastError.empty())
    {
        LLImage::setLastError(mLastError);
    }

    return res;
}


bool LLImageJ2C::validate(U8 *data, U32 file_size)
{

    resetLastError();

    setData(data, file_size);

    bool res = updateData();
    if ( res )
    {
        // Check to make sure that this instance has been initialized with data
        if (!getData() || (0 == getDataSize()))
        {
            setLastError("LLImageJ2C uninitialized");
            res = false;
        }
        else
        {
            res = mImpl->getMetadata(*this);
        }
    }

    if (!mLastError.empty())
    {
        LLImage::setLastError(mLastError);
    }
    return res;
}

void LLImageJ2C::decodeFailed()
{
    mDecoding = false;
}

void LLImageJ2C::updateRawDiscardLevel()
{
    mRawDiscardLevel = mMaxBytes ? calcDiscardLevelBytes(mMaxBytes) : mDiscardLevel;
}

LLImageJ2CImpl::~LLImageJ2CImpl()
{
}

//----------------------------------------------------------------------------------------------
// Start of LLImageCompressionTester
//----------------------------------------------------------------------------------------------
LLImageCompressionTester::LLImageCompressionTester() : LLMetricPerformanceTesterBasic(sTesterName)
{
    addMetric("Time Decompression (s)");
    addMetric("Volume In Decompression (kB)");
    addMetric("Volume Out Decompression (kB)");
    addMetric("Decompression Ratio (x:1)");
    addMetric("Perf Decompression (kB/s)");

    addMetric("Time Compression (s)");
    addMetric("Volume In Compression (kB)");
    addMetric("Volume Out Compression (kB)");
    addMetric("Compression Ratio (x:1)");
    addMetric("Perf Compression (kB/s)");

    mRunBytesInDecompression = 0;
    mRunBytesOutDecompression = 0;
    mRunBytesInCompression = 0;

    mTotalBytesInDecompression = 0;
    mTotalBytesOutDecompression = 0;
    mTotalBytesInCompression = 0;
    mTotalBytesOutCompression = 0;

    mTotalTimeDecompression = 0.0f;
    mTotalTimeCompression = 0.0f;
    mRunTimeDecompression = 0.0f;
}

LLImageCompressionTester::~LLImageCompressionTester()
{
    outputTestResults();
    LLImageJ2C::sTesterp = NULL;
}

//virtual
void LLImageCompressionTester::outputTestRecord(LLSD *sd)
{
    std::string currentLabel = getCurrentLabelName();

    F32 decompressionPerf = 0.0f;
    F32 compressionPerf   = 0.0f;
    F32 decompressionRate = 0.0f;
    F32 compressionRate   = 0.0f;

    F32 totalkBInDecompression  = (F32)(mTotalBytesInDecompression)  / 1000.f;
    F32 totalkBOutDecompression = (F32)(mTotalBytesOutDecompression) / 1000.f;
    F32 totalkBInCompression    = (F32)(mTotalBytesInCompression)    / 1000.f;
    F32 totalkBOutCompression   = (F32)(mTotalBytesOutCompression)   / 1000.f;

    if (!is_approx_zero(mTotalTimeDecompression))
    {
        decompressionPerf = totalkBInDecompression / mTotalTimeDecompression;
    }
    if (!is_approx_zero(totalkBInDecompression))
    {
        decompressionRate = totalkBOutDecompression / totalkBInDecompression;
    }
    if (!is_approx_zero(mTotalTimeCompression))
    {
        compressionPerf = totalkBInCompression / mTotalTimeCompression;
    }
    if (!is_approx_zero(totalkBOutCompression))
    {
        compressionRate = totalkBInCompression / totalkBOutCompression;
    }

    (*sd)[currentLabel]["Time Decompression (s)"]       = (LLSD::Real)mTotalTimeDecompression;
    (*sd)[currentLabel]["Volume In Decompression (kB)"] = (LLSD::Real)totalkBInDecompression;
    (*sd)[currentLabel]["Volume Out Decompression (kB)"]= (LLSD::Real)totalkBOutDecompression;
    (*sd)[currentLabel]["Decompression Ratio (x:1)"]    = (LLSD::Real)decompressionRate;
    (*sd)[currentLabel]["Perf Decompression (kB/s)"]    = (LLSD::Real)decompressionPerf;

    (*sd)[currentLabel]["Time Compression (s)"]         = (LLSD::Real)mTotalTimeCompression;
    (*sd)[currentLabel]["Volume In Compression (kB)"]   = (LLSD::Real)totalkBInCompression;
    (*sd)[currentLabel]["Volume Out Compression (kB)"]  = (LLSD::Real)totalkBOutCompression;
    (*sd)[currentLabel]["Compression Ratio (x:1)"]      = (LLSD::Real)compressionRate;
    (*sd)[currentLabel]["Perf Compression (kB/s)"]      = (LLSD::Real)compressionPerf;
}

void LLImageCompressionTester::updateCompressionStats(const F32 deltaTime)
{
    mTotalTimeCompression += deltaTime;
}

void LLImageCompressionTester::updateCompressionStats(const S32 bytesCompress, const S32 bytesRaw)
{
    mTotalBytesInCompression += bytesRaw;
    mRunBytesInCompression += bytesRaw;
    mTotalBytesOutCompression += bytesCompress;
    if (mRunBytesInCompression > (1000000))
    {
        // Output everything
        outputTestResults();
        // Reset the compression data of the run
        mRunBytesInCompression = 0;
    }
}

void LLImageCompressionTester::updateDecompressionStats(const F32 deltaTime)
{
    mTotalTimeDecompression += deltaTime;
}

void LLImageCompressionTester::updateDecompressionStats(const S32 bytesIn, const S32 bytesOut)
{
    mTotalBytesInDecompression += bytesIn;
    mRunBytesInDecompression += bytesIn;
    mTotalBytesOutDecompression += bytesOut;
    mRunBytesOutDecompression += bytesOut;
    //if (mRunBytesInDecompression > (1000000))
    if (mRunBytesOutDecompression > (10000000))
    //if ((mTotalTimeDecompression - mRunTimeDecompression) >= (5.0f))
    {
        // Output everything
        outputTestResults();
        // Reset the decompression data of the run
        mRunBytesInDecompression = 0;
        mRunBytesOutDecompression = 0;
        mRunTimeDecompression = mTotalTimeDecompression;
    }
}

//----------------------------------------------------------------------------------------------
// End of LLTexturePipelineTester
//----------------------------------------------------------------------------------------------