/* -*- Mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40; -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "GLTextureImage.h"
#include "GLContext.h"
#include "gfxContext.h"
#include "gfxPlatform.h"
#include "gfxUtils.h"
#include "gfx2DGlue.h"
#include "mozilla/gfx/2D.h"
#include "ScopedGLHelpers.h"
#include "GLUploadHelpers.h"
#include "GfxTexturesReporter.h"

#include "TextureImageEGL.h"

using namespace mozilla::gfx;

namespace mozilla {
namespace gl {

already_AddRefed<TextureImage>
CreateTextureImage(GLContext* gl,
                   const gfx::IntSize& aSize,
                   TextureImage::ContentType aContentType,
                   GLenum aWrapMode,
                   TextureImage::Flags aFlags,
                   TextureImage::ImageFormat aImageFormat)
{
    switch (gl->GetContextType()) {
        case GLContextType::EGL:
            return CreateTextureImageEGL(gl, aSize, aContentType, aWrapMode, aFlags, aImageFormat);
        default: {
            GLint maxTextureSize;
            gl->fGetIntegerv(LOCAL_GL_MAX_TEXTURE_SIZE, &maxTextureSize);
            if (aSize.width > maxTextureSize || aSize.height > maxTextureSize) {
              NS_ASSERTION(aWrapMode == LOCAL_GL_CLAMP_TO_EDGE, "Can't support wrapping with tiles!");
              return CreateTiledTextureImage(gl, aSize, aContentType, aFlags, aImageFormat);
            } else {
              return CreateBasicTextureImage(gl, aSize, aContentType, aWrapMode, aFlags);
            }
        }
    }
}


static already_AddRefed<TextureImage>
TileGenFunc(GLContext* gl,
            const IntSize& aSize,
            TextureImage::ContentType aContentType,
            TextureImage::Flags aFlags,
            TextureImage::ImageFormat aImageFormat)
{
    switch (gl->GetContextType()) {
        case GLContextType::EGL:
            return TileGenFuncEGL(gl, aSize, aContentType, aFlags, aImageFormat);
        default:
            return CreateBasicTextureImage(gl, aSize, aContentType,
                                           LOCAL_GL_CLAMP_TO_EDGE, aFlags);
    }
}

already_AddRefed<TextureImage>
TextureImage::Create(GLContext* gl,
                     const gfx::IntSize& size,
                     TextureImage::ContentType contentType,
                     GLenum wrapMode,
                     TextureImage::Flags flags)
{
    return CreateTextureImage(gl, size, contentType, wrapMode, flags);
}

bool
TextureImage::UpdateFromDataSource(gfx::DataSourceSurface* aSurface,
                                   const nsIntRegion* aDestRegion,
                                   const gfx::IntPoint* aSrcPoint)
{
    nsIntRegion destRegion = aDestRegion ? *aDestRegion
                                         : IntRect(0, 0,
                                                     aSurface->GetSize().width,
                                                     aSurface->GetSize().height);
    gfx::IntPoint srcPoint = aSrcPoint ? *aSrcPoint
                                       : gfx::IntPoint(0, 0);
    return DirectUpdate(aSurface, destRegion, srcPoint);
}

gfx::IntRect TextureImage::GetTileRect() {
    return gfx::IntRect(gfx::IntPoint(0,0), mSize);
}

gfx::IntRect TextureImage::GetSrcTileRect() {
    return GetTileRect();
}

void
TextureImage::UpdateUploadSize(size_t amount)
{
    if (mUploadSize > 0) {
        GfxTexturesReporter::UpdateAmount(GfxTexturesReporter::MemoryFreed, mUploadSize);
    }
    mUploadSize = amount;
    GfxTexturesReporter::UpdateAmount(GfxTexturesReporter::MemoryAllocated, mUploadSize);
}

BasicTextureImage::~BasicTextureImage()
{
    GLContext* ctx = mGLContext;
    if (ctx->IsDestroyed() || !ctx->IsOwningThreadCurrent()) {
        ctx = ctx->GetSharedContext();
    }

    // If we have a context, then we need to delete the texture;
    // if we don't have a context (either real or shared),
    // then they went away when the contex was deleted, because it
    // was the only one that had access to it.
    if (ctx && ctx->MakeCurrent()) {
        ctx->fDeleteTextures(1, &mTexture);
    }
}

void
BasicTextureImage::BindTexture(GLenum aTextureUnit)
{
    mGLContext->fActiveTexture(aTextureUnit);
    mGLContext->fBindTexture(LOCAL_GL_TEXTURE_2D, mTexture);
    mGLContext->fActiveTexture(LOCAL_GL_TEXTURE0);
}

bool
BasicTextureImage::DirectUpdate(gfx::DataSourceSurface* aSurf, const nsIntRegion& aRegion, const gfx::IntPoint& aFrom /* = gfx::IntPoint(0, 0) */)
{
    nsIntRegion region;
    if (mTextureState == Valid) {
        region = aRegion;
    } else {
        region = nsIntRegion(IntRect(0, 0, mSize.width, mSize.height));
    }
    bool needInit = mTextureState == Created;
    size_t uploadSize;

    mTextureFormat =
        UploadSurfaceToTexture(mGLContext,
                               aSurf,
                               region,
                               mTexture,
                               mSize,
                               &uploadSize,
                               needInit,
                               aFrom);
    if (mTextureFormat == SurfaceFormat::UNKNOWN) {
        return false;
    }

    if (uploadSize > 0) {
        UpdateUploadSize(uploadSize);
    }
    mTextureState = Valid;
    return true;
}

void
BasicTextureImage::Resize(const gfx::IntSize& aSize)
{
    mGLContext->fBindTexture(LOCAL_GL_TEXTURE_2D, mTexture);

    // This matches the logic in UploadImageDataToTexture so that
    // we avoid mixing formats.
    GLenum format;
    GLenum type;
    if (mGLContext->GetPreferredARGB32Format() == LOCAL_GL_BGRA) {
        MOZ_ASSERT(!mGLContext->IsGLES());
        format = LOCAL_GL_BGRA;
        type = LOCAL_GL_UNSIGNED_INT_8_8_8_8_REV;
    } else {
        format = LOCAL_GL_RGBA;
        type = LOCAL_GL_UNSIGNED_BYTE;
    }

    mGLContext->fTexImage2D(LOCAL_GL_TEXTURE_2D,
                            0,
                            LOCAL_GL_RGBA,
                            aSize.width,
                            aSize.height,
                            0,
                            format,
                            type,
                            nullptr);

    mTextureState = Allocated;
    mSize = aSize;
}

gfx::IntSize TextureImage::GetSize() const {
  return mSize;
}

TextureImage::TextureImage(const gfx::IntSize& aSize,
             GLenum aWrapMode, ContentType aContentType,
             Flags aFlags)
    : mSize(aSize)
    , mWrapMode(aWrapMode)
    , mContentType(aContentType)
    , mTextureFormat(gfx::SurfaceFormat::UNKNOWN)
    , mSamplingFilter(SamplingFilter::GOOD)
    , mFlags(aFlags)
    , mUploadSize(0)
{}

BasicTextureImage::BasicTextureImage(GLuint aTexture,
                  const gfx::IntSize& aSize,
                  GLenum aWrapMode,
                  ContentType aContentType,
                  GLContext* aContext,
                  TextureImage::Flags aFlags)
  : TextureImage(aSize, aWrapMode, aContentType, aFlags)
  , mTexture(aTexture)
  , mTextureState(Created)
  , mGLContext(aContext)
{}

static bool
WantsSmallTiles(GLContext* gl)
{
    // We can't use small tiles on the SGX 540, because of races in texture upload.
    if (gl->WorkAroundDriverBugs() &&
        gl->Renderer() == GLRenderer::SGX540)
        return false;

    // We should use small tiles for good performance if we can't use
    // glTexSubImage2D() for some reason.
    if (!CanUploadSubTextures(gl))
        return true;

    // Don't use small tiles otherwise. (If we implement incremental texture upload,
    // then we will want to revisit this.)
    return false;
}

TiledTextureImage::TiledTextureImage(GLContext* aGL,
                                     gfx::IntSize aSize,
                                     TextureImage::ContentType aContentType,
                                     TextureImage::Flags aFlags,
                                     TextureImage::ImageFormat aImageFormat)
    : TextureImage(aSize, LOCAL_GL_CLAMP_TO_EDGE, aContentType, aFlags)
    , mCurrentImage(0)
    , mIterationCallback(nullptr)
    , mIterationCallbackData(nullptr)
    , mRows(0)
    , mColumns(0)
    , mGL(aGL)
    , mTextureState(Created)
    , mImageFormat(aImageFormat)
{
    if (!(aFlags & TextureImage::DisallowBigImage) && WantsSmallTiles(mGL)) {
      mTileSize = 256;
    } else {
      mGL->fGetIntegerv(LOCAL_GL_MAX_TEXTURE_SIZE, (GLint*) &mTileSize);
    }
    if (aSize.width != 0 && aSize.height != 0) {
        Resize(aSize);
    }
}

TiledTextureImage::~TiledTextureImage()
{
}

bool
TiledTextureImage::DirectUpdate(gfx::DataSourceSurface* aSurf, const nsIntRegion& aRegion, const gfx::IntPoint& aFrom /* = gfx::IntPoint(0, 0) */)
{
    if (mSize.width == 0 || mSize.height == 0) {
        return true;
    }

    nsIntRegion region;

    if (mTextureState != Valid) {
        IntRect bounds = IntRect(0, 0, mSize.width, mSize.height);
        region = nsIntRegion(bounds);
    } else {
        region = aRegion;
    }

    bool result = true;
    int oldCurrentImage = mCurrentImage;
    BeginBigImageIteration();
    do {
        IntRect tileRect = GetSrcTileRect();
        int xPos = tileRect.x;
        int yPos = tileRect.y;

        nsIntRegion tileRegion;
        tileRegion.And(region, tileRect); // intersect with tile

        if (tileRegion.IsEmpty())
            continue;

        tileRegion.MoveBy(-xPos, -yPos); // translate into tile local space

        result &= mImages[mCurrentImage]->
          DirectUpdate(aSurf, tileRegion, aFrom + gfx::IntPoint(xPos, yPos));

        if (mCurrentImage == mImages.Length() - 1) {
            // We know we're done, but we still need to ensure that the callback
            // gets called (e.g. to update the uploaded region).
            NextTile();
            break;
        }
        // Override a callback cancelling iteration if the texture wasn't valid.
        // We need to force the update in that situation, or we may end up
        // showing invalid/out-of-date texture data.
    } while (NextTile() || (mTextureState != Valid));
    mCurrentImage = oldCurrentImage;

    mTextureFormat = mImages[0]->GetTextureFormat();
    mTextureState = Valid;
    return result;
}

void TiledTextureImage::BeginBigImageIteration()
{
    mCurrentImage = 0;
}

bool TiledTextureImage::NextTile()
{
    bool continueIteration = true;

    if (mIterationCallback)
        continueIteration = mIterationCallback(this, mCurrentImage,
                                               mIterationCallbackData);

    if (mCurrentImage + 1 < mImages.Length()) {
        mCurrentImage++;
        return continueIteration;
    }
    return false;
}

void TiledTextureImage::SetIterationCallback(BigImageIterationCallback aCallback,
                                             void* aCallbackData)
{
    mIterationCallback = aCallback;
    mIterationCallbackData = aCallbackData;
}

gfx::IntRect TiledTextureImage::GetTileRect()
{
    if (!GetTileCount()) {
        return gfx::IntRect();
    }
    gfx::IntRect rect = mImages[mCurrentImage]->GetTileRect();
    unsigned int xPos = (mCurrentImage % mColumns) * mTileSize;
    unsigned int yPos = (mCurrentImage / mColumns) * mTileSize;
    rect.MoveBy(xPos, yPos);
    return rect;
}

gfx::IntRect TiledTextureImage::GetSrcTileRect()
{
    gfx::IntRect rect = GetTileRect();
    const bool needsYFlip = mFlags & OriginBottomLeft;
    unsigned int srcY = needsYFlip ? mSize.height - rect.height - rect.y
                                   : rect.y;
    return gfx::IntRect(rect.x, srcY, rect.width, rect.height);
}

void
TiledTextureImage::BindTexture(GLenum aTextureUnit)
{
    if (!GetTileCount()) {
        return;
    }
    mImages[mCurrentImage]->BindTexture(aTextureUnit);
}

/*
 * Resize, trying to reuse tiles. The reuse strategy is to decide on reuse per
 * column. A tile on a column is reused if it hasn't changed size, otherwise it
 * is discarded/replaced. Extra tiles on a column are pruned after iterating
 * each column, and extra rows are pruned after iteration over the entire image
 * finishes.
 */
void TiledTextureImage::Resize(const gfx::IntSize& aSize)
{
    if (mSize == aSize && mTextureState != Created) {
        return;
    }

    // calculate rows and columns, rounding up
    unsigned int columns = (aSize.width  + mTileSize - 1) / mTileSize;
    unsigned int rows = (aSize.height + mTileSize - 1) / mTileSize;

    // Iterate over old tile-store and insert/remove tiles as necessary
    int row;
    unsigned int i = 0;
    for (row = 0; row < (int)rows; row++) {
        // If we've gone beyond how many rows there were before, set mColumns to
        // zero so that we only create new tiles.
        if (row >= (int)mRows)
            mColumns = 0;

        // Similarly, if we're on the last row of old tiles and the height has
        // changed, discard all tiles in that row.
        // This will cause the pruning of columns not to work, but we don't need
        // to worry about that, as no more tiles will be reused past this point
        // anyway.
        if ((row == (int)mRows - 1) && (aSize.height != mSize.height))
            mColumns = 0;

        int col;
        for (col = 0; col < (int)columns; col++) {
            IntSize size( // use tilesize first, then the remainder
                    (col+1) * mTileSize > (unsigned int)aSize.width  ? aSize.width  % mTileSize : mTileSize,
                    (row+1) * mTileSize > (unsigned int)aSize.height ? aSize.height % mTileSize : mTileSize);

            bool replace = false;

            // Check if we can re-use old tiles.
            if (col < (int)mColumns) {
                // Reuse an existing tile. If the tile is an end-tile and the
                // width differs, replace it instead.
                if (mSize.width != aSize.width) {
                    if (col == (int)mColumns - 1) {
                        // Tile at the end of the old column, replace it with
                        // a new one.
                        replace = true;
                    } else if (col == (int)columns - 1) {
                        // Tile at the end of the new column, create a new one.
                    } else {
                        // Before the last column on both the old and new sizes,
                        // reuse existing tile.
                        i++;
                        continue;
                    }
                } else {
                    // Width hasn't changed, reuse existing tile.
                    i++;
                    continue;
                }
            }

            // Create a new tile.
            RefPtr<TextureImage> teximg =
                TileGenFunc(mGL, size, mContentType, mFlags, mImageFormat);
            if (replace)
                mImages.ReplaceElementAt(i, teximg);
            else
                mImages.InsertElementAt(i, teximg);
            i++;
        }

        // Prune any unused tiles on the end of the column.
        if (row < (int)mRows) {
            for (col = (int)mColumns - col; col > 0; col--) {
                mImages.RemoveElementAt(i);
            }
        }
    }

    // Prune any unused tiles at the end of the store.
    unsigned int length = mImages.Length();
    for (; i < length; i++)
      mImages.RemoveElementAt(mImages.Length()-1);

    // Reset tile-store properties.
    mRows = rows;
    mColumns = columns;
    mSize = aSize;
    mTextureState = Allocated;
    mCurrentImage = 0;
}

uint32_t TiledTextureImage::GetTileCount()
{
    return mImages.Length();
}

already_AddRefed<TextureImage>
CreateTiledTextureImage(GLContext* aGL,
                        const gfx::IntSize& aSize,
                        TextureImage::ContentType aContentType,
                        TextureImage::Flags aFlags,
                        TextureImage::ImageFormat aImageFormat)
{
  RefPtr<TextureImage> texImage = static_cast<TextureImage*>(
      new gl::TiledTextureImage(aGL, aSize, aContentType, aFlags, aImageFormat));
  return texImage.forget();
}


already_AddRefed<TextureImage>
CreateBasicTextureImage(GLContext* aGL,
                        const gfx::IntSize& aSize,
                        TextureImage::ContentType aContentType,
                        GLenum aWrapMode,
                        TextureImage::Flags aFlags)
{
    bool useNearestFilter = aFlags & TextureImage::UseNearestFilter;
    if (!aGL->MakeCurrent()) {
      return nullptr;
    }

    GLuint texture = 0;
    aGL->fGenTextures(1, &texture);

    ScopedBindTexture bind(aGL, texture);

    GLint texfilter = useNearestFilter ? LOCAL_GL_NEAREST : LOCAL_GL_LINEAR;
    aGL->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, texfilter);
    aGL->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, texfilter);
    aGL->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, aWrapMode);
    aGL->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, aWrapMode);

    RefPtr<BasicTextureImage> texImage =
        new BasicTextureImage(texture, aSize, aWrapMode, aContentType,
                              aGL, aFlags);
    return texImage.forget();
}

} // namespace gl
} // namespace mozilla