/* 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 "WebGLContext.h"
#include "WebGLTexelConversions.h"

namespace mozilla {

using namespace WebGLTexelConversions;

namespace {

/** @class WebGLImageConverter
 *
 * This class is just a helper to implement WebGLContext::ConvertImage below.
 *
 * Design comments:
 *
 * WebGLContext::ConvertImage has to handle hundreds of format conversion paths.
 * It is important to minimize executable code size here. Instead of passing around
 * a large number of function parameters hundreds of times, we create a
 * WebGLImageConverter object once, storing these parameters, and then we call
 * the run() method on it.
 */
class WebGLImageConverter
{
    const size_t mWidth, mHeight;
    const void* const mSrcStart;
    void* const mDstStart;
    const ptrdiff_t mSrcStride, mDstStride;
    bool mAlreadyRun;
    bool mSuccess;

    /*
     * Returns sizeof(texel)/sizeof(type). The point is that we will iterate over
     * texels with typed pointers and this value will tell us by how much we need
     * to increment these pointers to advance to the next texel.
     */
    template<WebGLTexelFormat Format>
    static size_t NumElementsPerTexelForFormat() {
        switch (Format) {
            case WebGLTexelFormat::A8:
            case WebGLTexelFormat::A16F:
            case WebGLTexelFormat::A32F:
            case WebGLTexelFormat::R8:
            case WebGLTexelFormat::R16F:
            case WebGLTexelFormat::R32F:
            case WebGLTexelFormat::RGB565:
            case WebGLTexelFormat::RGB11F11F10F:
            case WebGLTexelFormat::RGBA4444:
            case WebGLTexelFormat::RGBA5551:
                return 1;
            case WebGLTexelFormat::RA8:
            case WebGLTexelFormat::RA16F:
            case WebGLTexelFormat::RA32F:
            case WebGLTexelFormat::RG8:
            case WebGLTexelFormat::RG16F:
            case WebGLTexelFormat::RG32F:
                return 2;
            case WebGLTexelFormat::RGB8:
            case WebGLTexelFormat::RGB16F:
            case WebGLTexelFormat::RGB32F:
                return 3;
            case WebGLTexelFormat::RGBA8:
            case WebGLTexelFormat::RGBA16F:
            case WebGLTexelFormat::RGBA32F:
            case WebGLTexelFormat::BGRX8:
            case WebGLTexelFormat::BGRA8:
                return 4;
            default:
                MOZ_ASSERT(false, "Unknown texel format. Coding mistake?");
                return 0;
        }
    }

    /*
     * This is the completely format-specific templatized conversion function,
     * that will be instantiated hundreds of times for all different combinations.
     * It is important to avoid generating useless code here. In particular, many
     * instantiations of this function template will never be called, so we try
     * to return immediately in these cases to allow the compiler to avoid generating
     * useless code.
     */
    template<WebGLTexelFormat SrcFormat,
             WebGLTexelFormat DstFormat,
             WebGLTexelPremultiplicationOp PremultiplicationOp>
    void run()
    {
        // check for never-called cases. We early-return to allow the compiler
        // to avoid generating this code. It would be tempting to abort() instead,
        // as returning early does leave the destination surface with uninitialized
        // data, but that would not allow the compiler to avoid generating this code.
        // So instead, we return early, so Success() will return false, and the caller
        // must check that and abort in that case. See WebGLContext::ConvertImage.

        if (SrcFormat == DstFormat &&
            PremultiplicationOp == WebGLTexelPremultiplicationOp::None)
        {
            // Should have used a fast exit path earlier, rather than entering this function.
            // we explicitly return here to allow the compiler to avoid generating this code
            return;
        }

        // Only textures uploaded from DOM elements or ImageData can allow DstFormat != SrcFormat.
        // DOM elements can only give BGRA8, BGRX8, A8, RGB565 formats. See DOMElementToImageSurface.
        // ImageData is always RGBA8. So all other SrcFormat will always satisfy DstFormat==SrcFormat,
        // so we can avoid compiling the code for all the unreachable paths.
        const bool CanSrcFormatComeFromDOMElementOrImageData
            = SrcFormat == WebGLTexelFormat::BGRA8 ||
              SrcFormat == WebGLTexelFormat::BGRX8 ||
              SrcFormat == WebGLTexelFormat::A8 ||
              SrcFormat == WebGLTexelFormat::RGB565 ||
              SrcFormat == WebGLTexelFormat::RGBA8;
        if (!CanSrcFormatComeFromDOMElementOrImageData &&
            SrcFormat != DstFormat)
        {
            return;
        }

        // Likewise, only textures uploaded from DOM elements or ImageData can possibly have to be unpremultiplied.
        if (!CanSrcFormatComeFromDOMElementOrImageData &&
            PremultiplicationOp == WebGLTexelPremultiplicationOp::Unpremultiply)
        {
            return;
        }

        // there is no point in premultiplication/unpremultiplication
        // in the following cases:
        //  - the source format has no alpha
        //  - the source format has no color
        //  - the destination format has no color
        if (!HasAlpha(SrcFormat) ||
            !HasColor(SrcFormat) ||
            !HasColor(DstFormat))
        {

            if (PremultiplicationOp != WebGLTexelPremultiplicationOp::None)
            {
                return;
            }
        }

        // end of early return cases.

        MOZ_ASSERT(!mAlreadyRun, "converter should be run only once!");
        mAlreadyRun = true;

        // gather some compile-time meta-data about the formats at hand.

        typedef
            typename DataTypeForFormat<SrcFormat>::Type
            SrcType;
        typedef
            typename DataTypeForFormat<DstFormat>::Type
            DstType;

        const WebGLTexelFormat IntermediateSrcFormat
            = IntermediateFormat<SrcFormat>::Value;
        const WebGLTexelFormat IntermediateDstFormat
            = IntermediateFormat<DstFormat>::Value;
        typedef
            typename DataTypeForFormat<IntermediateSrcFormat>::Type
            IntermediateSrcType;
        typedef
            typename DataTypeForFormat<IntermediateDstFormat>::Type
            IntermediateDstType;

        const size_t NumElementsPerSrcTexel = NumElementsPerTexelForFormat<SrcFormat>();
        const size_t NumElementsPerDstTexel = NumElementsPerTexelForFormat<DstFormat>();
        const size_t MaxElementsPerTexel = 4;
        MOZ_ASSERT(NumElementsPerSrcTexel <= MaxElementsPerTexel, "unhandled format");
        MOZ_ASSERT(NumElementsPerDstTexel <= MaxElementsPerTexel, "unhandled format");

        // we assume that the strides are multiples of the sizeof of respective types.
        // this assumption will allow us to iterate over src and dst images using typed
        // pointers, e.g. uint8_t* or uint16_t* or float*, instead of untyped pointers.
        // So this assumption allows us to write cleaner and safer code, but it might
        // not be true forever and if it eventually becomes wrong, we'll have to revert
        // to always iterating using uint8_t* pointers regardless of the types at hand.
        MOZ_ASSERT(mSrcStride % sizeof(SrcType) == 0 &&
                   mDstStride % sizeof(DstType) == 0,
                   "Unsupported: texture stride is not a multiple of sizeof(type)");
        const ptrdiff_t srcStrideInElements = mSrcStride / sizeof(SrcType);
        const ptrdiff_t dstStrideInElements = mDstStride / sizeof(DstType);

        const SrcType* srcRowStart = static_cast<const SrcType*>(mSrcStart);
        DstType* dstRowStart = static_cast<DstType*>(mDstStart);

        // the loop performing the texture format conversion
        for (size_t i = 0; i < mHeight; ++i) {
            const SrcType* srcRowEnd = srcRowStart + mWidth * NumElementsPerSrcTexel;
            const SrcType* srcPtr = srcRowStart;
            DstType* dstPtr = dstRowStart;
            while (srcPtr != srcRowEnd) {
                // convert a single texel. We proceed in 3 steps: unpack the source texel
                // so the corresponding interchange format (e.g. unpack RGB565 to RGBA8),
                // convert the resulting data type to the destination type (e.g. convert
                // from RGBA8 to RGBA32F), and finally pack the destination texel
                // (e.g. pack RGBA32F to RGB32F).
                IntermediateSrcType unpackedSrc[MaxElementsPerTexel];
                IntermediateDstType unpackedDst[MaxElementsPerTexel];

                // unpack a src texel to corresponding intermediate src format.
                // for example, unpack RGB565 to RGBA8
                unpack<SrcFormat>(srcPtr, unpackedSrc);
                // convert the data type to the destination type, if needed.
                // for example, convert RGBA8 to RGBA32F
                convertType(unpackedSrc, unpackedDst);
                // pack the destination texel.
                // for example, pack RGBA32F to RGB32F
                pack<DstFormat, PremultiplicationOp>(unpackedDst, dstPtr);

                srcPtr += NumElementsPerSrcTexel;
                dstPtr += NumElementsPerDstTexel;
            }
            srcRowStart += srcStrideInElements;
            dstRowStart += dstStrideInElements;
        }

        mSuccess = true;
        return;
    }

    template<WebGLTexelFormat SrcFormat,
             WebGLTexelFormat DstFormat>
    void run(WebGLTexelPremultiplicationOp premultiplicationOp)
    {
        #define WEBGLIMAGECONVERTER_CASE_PREMULTIPLICATIONOP(PremultiplicationOp) \
            case PremultiplicationOp: \
                return run<SrcFormat, DstFormat, PremultiplicationOp>();

        switch (premultiplicationOp) {
            WEBGLIMAGECONVERTER_CASE_PREMULTIPLICATIONOP(WebGLTexelPremultiplicationOp::None)
            WEBGLIMAGECONVERTER_CASE_PREMULTIPLICATIONOP(WebGLTexelPremultiplicationOp::Premultiply)
            WEBGLIMAGECONVERTER_CASE_PREMULTIPLICATIONOP(WebGLTexelPremultiplicationOp::Unpremultiply)
            default:
                MOZ_ASSERT(false, "unhandled case. Coding mistake?");
        }

        #undef WEBGLIMAGECONVERTER_CASE_PREMULTIPLICATIONOP
    }

    template<WebGLTexelFormat SrcFormat>
    void run(WebGLTexelFormat dstFormat,
             WebGLTexelPremultiplicationOp premultiplicationOp)
    {
        #define WEBGLIMAGECONVERTER_CASE_DSTFORMAT(DstFormat) \
            case DstFormat: \
                return run<SrcFormat, DstFormat>(premultiplicationOp);

        switch (dstFormat) {
        // 1-channel formats
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::A8)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::A16F)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::A32F)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::R8)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::R16F)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::R32F)
        // 2-channel formats
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RA8)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RA16F)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RA32F)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RG8)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RG16F)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RG32F)
        // 3-channel formats
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RGB565)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RGB8)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RGB11F11F10F)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RGB16F)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RGB32F)
        // 4-channel formats
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RGBA4444)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RGBA5551)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RGBA8)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RGBA16F)
        WEBGLIMAGECONVERTER_CASE_DSTFORMAT(WebGLTexelFormat::RGBA32F)

        default:
            MOZ_ASSERT(false, "unhandled case. Coding mistake?");
        }

        #undef WEBGLIMAGECONVERTER_CASE_DSTFORMAT
    }

public:

    void run(WebGLTexelFormat srcFormat,
             WebGLTexelFormat dstFormat,
             WebGLTexelPremultiplicationOp premultiplicationOp)
    {
        #define WEBGLIMAGECONVERTER_CASE_SRCFORMAT(SrcFormat) \
            case SrcFormat: \
                return run<SrcFormat>(dstFormat, premultiplicationOp);

        switch (srcFormat) {
        // 1-channel formats
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::A8)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::A16F)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::A32F)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::R8)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::R16F)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::R32F)
        // 2-channel formats
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RA8)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RA16F)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RA32F)
        // 3-channel formats
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RGB565)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RGB8)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RGB16F)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RGB32F)
        // 4-channel formats
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RGBA4444)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RGBA5551)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RGBA8)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RGBA16F)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::RGBA32F)
        // DOM element source formats
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::BGRX8)
        WEBGLIMAGECONVERTER_CASE_SRCFORMAT(WebGLTexelFormat::BGRA8)

        default:
            MOZ_ASSERT(false, "unhandled case. Coding mistake?");
        }

        #undef WEBGLIMAGECONVERTER_CASE_SRCFORMAT
    }

    WebGLImageConverter(size_t width, size_t height,
                        const void* srcStart, void* dstStart,
                        ptrdiff_t srcStride, ptrdiff_t dstStride)
        : mWidth(width), mHeight(height),
          mSrcStart(srcStart), mDstStart(dstStart),
          mSrcStride(srcStride), mDstStride(dstStride),
          mAlreadyRun(false), mSuccess(false)
    {}

    bool Success() const {
        return mSuccess;
    }
};

} // end anonymous namespace

bool
ConvertImage(size_t width, size_t height,
             const void* srcBegin, size_t srcStride, gl::OriginPos srcOrigin,
             WebGLTexelFormat srcFormat, bool srcPremultiplied,
             void* dstBegin, size_t dstStride, gl::OriginPos dstOrigin,
             WebGLTexelFormat dstFormat, bool dstPremultiplied,
             bool* const out_wasTrivial)
{
    *out_wasTrivial = true;

    if (srcFormat == WebGLTexelFormat::FormatNotSupportingAnyConversion ||
        dstFormat == WebGLTexelFormat::FormatNotSupportingAnyConversion)
    {
        return false;
    }

    if (!width || !height)
        return true;

    const bool shouldYFlip = (srcOrigin != dstOrigin);

    const bool canSkipPremult = (!HasAlpha(srcFormat) ||
                                 !HasColor(srcFormat) ||
                                 !HasColor(dstFormat));

    WebGLTexelPremultiplicationOp premultOp;
    if (canSkipPremult) {
        premultOp = WebGLTexelPremultiplicationOp::None;
    } else if (!srcPremultiplied && dstPremultiplied) {
        premultOp = WebGLTexelPremultiplicationOp::Premultiply;
    } else if (srcPremultiplied && !dstPremultiplied) {
        premultOp = WebGLTexelPremultiplicationOp::Unpremultiply;
    } else {
        premultOp = WebGLTexelPremultiplicationOp::None;
    }

    const uint8_t* srcItr = (const uint8_t*)srcBegin;
    const uint8_t* const srcEnd = srcItr + srcStride * height;
    uint8_t* dstItr = (uint8_t*)dstBegin;
    ptrdiff_t dstItrStride = dstStride;
    if (shouldYFlip) {
         dstItr = dstItr + dstStride * (height - 1);
         dstItrStride = -dstItrStride;
    }

    if (srcFormat == dstFormat && premultOp == WebGLTexelPremultiplicationOp::None) {
        // Fast exit path: we just have to memcpy all the rows.
        //
        // The case where absolutely nothing needs to be done is supposed to have
        // been handled earlier (in TexImage2D_base, etc).
        //
        // So the case we're handling here is when even though no format conversion is
        // needed, we still might have to flip vertically and/or to adjust to a different
        // stride.

        // We ignore canSkipPremult for this perf trap, since it's an avoidable perf cliff
        // under the WebGL API user's control.
        MOZ_ASSERT((srcPremultiplied != dstPremultiplied ||
                    shouldYFlip ||
                    srcStride != dstStride),
                   "Performance trap -- should handle this case earlier to avoid memcpy");

        const auto bytesPerPixel = TexelBytesForFormat(srcFormat);
        const size_t bytesPerRow = bytesPerPixel * width;

        while (srcItr != srcEnd) {
            memcpy(dstItr, srcItr, bytesPerRow);
            srcItr += srcStride;
            dstItr += dstItrStride;
        }
        return true;
    }

    *out_wasTrivial = false;

    WebGLImageConverter converter(width, height, srcItr, dstItr, srcStride, dstItrStride);
    converter.run(srcFormat, dstFormat, premultOp);

    if (!converter.Success()) {
        // the dst image may be left uninitialized, so we better not try to
        // continue even in release builds. This should never happen anyway,
        // and would be a bug in our code.
        NS_RUNTIMEABORT("programming mistake in WebGL texture conversions");
    }

    return true;
}

} // end namespace mozilla
