//
//  libavg - Media Playback Engine. 
//  Copyright (C) 2003-2014 Ulrich von Zadow
//
//  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; either
//  version 2 of the License, or (at your option) any later version.
//
//  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
//  Current versions can be found at www.libavg.de
//

#include "GPUFilter.h"
#include "Bitmap.h"

#include "VertexArray.h"
#include "ImagingProjection.h"
#include "GLContext.h"
#include "ShaderRegistry.h"
#include "Filterfliprgb.h"
#include "BitmapLoader.h"

#include "../base/ObjectCounter.h"
#include "../base/Exception.h"
#include "../base/MathHelper.h"

#include <iostream>
#include <string.h>

using namespace std;
using namespace boost;

namespace avg {

GPUFilter::GPUFilter(const string& sShaderID, bool bUseAlpha,
        bool bStandalone, unsigned numTextures, bool bMipmap)
    : m_bStandalone(bStandalone),
      m_NumTextures(numTextures),
      m_bMipmap(bMipmap),
      m_SrcSize(0,0),
      m_DestRect(0,0,0,0)
{
    m_PFSrc = BitmapLoader::get()->getDefaultPixelFormat(bUseAlpha);
    m_PFDest = m_PFSrc;
    createShader(sShaderID);

    m_pShader = avg::getShader(sShaderID);
    ObjectCounter::get()->incRef(&typeid(*this));
}

GPUFilter::GPUFilter(PixelFormat pfSrc, PixelFormat pfDest, bool bStandalone, 
        const string& sShaderID, unsigned numTextures, bool bMipmap)
    : m_PFSrc(pfSrc),
      m_PFDest(pfDest),
      m_bStandalone(bStandalone),
      m_NumTextures(numTextures),
      m_bMipmap(bMipmap),
      m_SrcSize(0,0),
      m_DestRect(0,0,0,0)
{
    createShader(sShaderID);
    m_pShader = avg::getShader(sShaderID);
    ObjectCounter::get()->incRef(&typeid(*this));
}

GPUFilter::~GPUFilter()
{
    ObjectCounter::get()->decRef(&typeid(*this));
}

BitmapPtr GPUFilter::apply(BitmapPtr pBmpSource)
{
    AVG_ASSERT(m_pSrcTex);
    AVG_ASSERT(!(m_pFBOs.empty()));
    m_pSrcMover->moveBmpToTexture(pBmpSource, *m_pSrcTex);
    apply(m_pSrcTex);
    BitmapPtr pFilteredBmp = m_pFBOs[0]->getImage();

    BitmapPtr pTmpBmp;
    if (pixelFormatIsBlueFirst(pFilteredBmp->getPixelFormat()) !=
            pixelFormatIsBlueFirst(pBmpSource->getPixelFormat()) &&
        pFilteredBmp->getBytesPerPixel() <= 4)
    {
        pTmpBmp = FilterFlipRGB().apply(pFilteredBmp);
    } else {
        pTmpBmp = pFilteredBmp;
    }

    BitmapPtr pDestBmp;
    if (pTmpBmp->getPixelFormat() != pBmpSource->getPixelFormat()) {
        pDestBmp = BitmapPtr(new Bitmap(m_DestRect.size(),
                pBmpSource->getPixelFormat()));
        pDestBmp->copyPixels(*pTmpBmp);
    } else {
        pDestBmp = pTmpBmp;
    }
    return pDestBmp;
}

void GPUFilter::apply(GLTexturePtr pSrcTex)
{
    m_pFBOs[0]->activate();
    applyOnGPU(pSrcTex);
    m_pFBOs[0]->copyToDestTexture();
}

GLTexturePtr GPUFilter::getDestTex(int i) const
{
    return m_pFBOs[i]->getTex();
}

BitmapPtr GPUFilter::getImage() const
{
    return m_pFBOs[0]->getImage();
}

FBOPtr GPUFilter::getFBO(int i)
{
    return m_pFBOs[i];
}

const IntRect& GPUFilter::getDestRect() const
{
    return m_DestRect;
}

const IntPoint& GPUFilter::getSrcSize() const
{
    return m_SrcSize;
}

FRect GPUFilter::getRelDestRect() const
{
    glm::vec2 srcSize(m_SrcSize);
    return FRect(m_DestRect.tl.x/srcSize.x, m_DestRect.tl.y/srcSize.y,
            m_DestRect.br.x/srcSize.x, m_DestRect.br.y/srcSize.y);
}

void GPUFilter::setDimensions(const IntPoint& srcSize)
{
    setDimensions(srcSize, IntRect(IntPoint(0,0), srcSize), GL_CLAMP_TO_EDGE);
}

void GPUFilter::setDimensions(const IntPoint& srcSize, const IntRect& destRect,
        unsigned texMode)
{
    bool bProjectionChanged = false;
    if (destRect != m_DestRect) {
        m_pFBOs.clear();
        for (unsigned i=0; i<m_NumTextures; ++i) {
            FBOPtr pFBO = FBOPtr(new FBO(destRect.size(), m_PFDest, 1, 1, false,
                    m_bMipmap));
            m_pFBOs.push_back(pFBO);
        }
        m_DestRect = destRect;
        bProjectionChanged = true;
    }
    if (m_bStandalone && srcSize != m_SrcSize) {
        m_pSrcTex = GLTexturePtr(new GLTexture(srcSize, m_PFSrc, false, 0, texMode, 
                texMode));
        m_pSrcMover = TextureMover::create(srcSize, m_PFSrc, GL_STREAM_DRAW);
        bProjectionChanged = true;
    }
    m_SrcSize = srcSize;
    if (bProjectionChanged) {
        m_pProjection = ImagingProjectionPtr(new ImagingProjection(srcSize, destRect));
    }
}
  
const OGLShaderPtr& GPUFilter::getShader() const
{
    return m_pShader;
}

void GPUFilter::draw(GLTexturePtr pTex)
{
    pTex->activate(GL_TEXTURE0);
    m_pProjection->draw(m_pShader);
}

void dumpKernel(int width, float* pKernel)
{
    cerr << "  Kernel width: " << width << endl;
    float sum = 0;
    for (int i = 0; i < width; ++i) {
        sum += pKernel[i];
        cerr << "  " << pKernel[i] << endl;
    }
    cerr << "Sum of coefficients: " << sum << endl;
}

int GPUFilter::getBlurKernelRadius(float stdDev) const
{
    return int(ceil(stdDev*3));
}

GLTexturePtr GPUFilter::calcBlurKernelTex(float stdDev, float opacity, bool bUseFloat)
        const
{
    AVG_ASSERT(opacity != -1);
    int kernelWidth;
    float* pKernel;
    if (stdDev == 0) {
        kernelWidth = 1;
        pKernel = new float[1];
        pKernel[0] = float(opacity);
    } else {
        float tempCoeffs[1024];
        int i=0;
        float coeff;
        do {
            coeff = float(exp(-i*i/(2*stdDev*stdDev))/sqrt(2*PI*stdDev*stdDev))
                    *float(opacity);
            tempCoeffs[i] = coeff;
            i++;
        } while (coeff > 0.003 && i < 1024);
        if (i > 1) {
            int kernelCenter = i - 2;
            kernelWidth = kernelCenter*2+1;
            pKernel = new float[kernelWidth];
            float sum = 0;
            for (int i = 0; i <= kernelCenter; ++i) {
                pKernel[kernelCenter+i] = tempCoeffs[i];
                sum += tempCoeffs[i];
                if (i != 0) {
                    pKernel[kernelCenter-i] = tempCoeffs[i];
                    sum += tempCoeffs[i];
                }
            }
            // Make sure the sum of coefficients is opacity despite the inaccuracies
            // introduced by using a kernel of finite size.
            for (int i = 0; i < kernelWidth; ++i) {
                pKernel[i] *= float(opacity)/sum;
            }
        } else {
            // Blur is so wide that all pixels would be black at 8-bit precision
            kernelWidth = 1;
            pKernel = new float[1];
            pKernel[0] = 0.;
        }
    }
//    dumpKernel(kernelWidth, pKernel);
    
    IntPoint size(kernelWidth, 1);
    PixelFormat pf;
    if (bUseFloat) {
        pf = R32G32B32A32F;
    } else {
        pf = I8;
    }
    GLTexturePtr pTex(new GLTexture(size, pf));
    TextureMoverPtr pKernelMover = TextureMover::create(size, pf, GL_STREAM_DRAW);
    BitmapPtr pBmp = pKernelMover->lock();
    void * pPixels = pBmp->getPixels();
    GLContext::checkError("GPUFilter::calcBlurKernelTex MapBuffer()");
    if (bUseFloat) {
        float * pCurFloat = (float*)pPixels;
        for (int i = 0; i < kernelWidth; ++i) {
            for (int j = 0; j < 4; ++j) {
                *pCurFloat = pKernel[i];
                ++pCurFloat;
            }
        }
    } else {
        unsigned char * pCurPixel = (unsigned char *)pPixels;
        for (int i = 0; i < kernelWidth; ++i) {
            *pCurPixel = (unsigned char)(pKernel[i]*255+0.5);
            ++pCurPixel;
        }
    }
    pKernelMover->unlock();
    pKernelMover->moveToTexture(*pTex);

    delete[] pKernel;
    return pTex;
}

}
