// Copyright (c) 2009-2011 Ignacio Castano <castano@gmail.com>
// Copyright (c) 2008-2009 NVIDIA Corporation -- Ignacio Castano <icastano@nvidia.com>
// 
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

#include "Context.h"

#include "nvtt.h"

#include "InputOptions.h"
#include "CompressionOptions.h"
#include "OutputOptions.h"
#include "Surface.h"

#include "CompressorDX9.h"
#include "CompressorDX10.h"
#include "CompressorDX11.h"
#include "CompressorRGB.h"
#include "cuda/CudaUtils.h"
#include "cuda/CudaCompressorDXT.h"

#include "nvimage/DirectDrawSurface.h"
#include "nvimage/KtxFile.h"
#include "nvimage/ColorBlock.h"
#include "nvimage/BlockDXT.h"
#include "nvimage/Image.h"
#include "nvimage/FloatImage.h"
#include "nvimage/Filter.h"
#include "nvimage/Quantize.h"
#include "nvimage/NormalMap.h"
#include "nvimage/PixelFormat.h"
#include "nvimage/ColorSpace.h"

#include "nvcore/Memory.h"
#include "nvcore/Ptr.h"
#include "nvcore/Array.inl"

using namespace nv;
using namespace nvtt;

Compressor::Compressor() : m(*new Compressor::Private())
{
    // CUDA initialization.
    m.cudaSupported = cuda::isHardwarePresent();
    m.cudaEnabled = false;
    m.cuda = NULL;

    enableCudaAcceleration(m.cudaSupported);

    m.dispatcher = &m.defaultDispatcher;
}

Compressor::~Compressor()
{
    delete &m;
}


void Compressor::enableCudaAcceleration(bool enable)
{
    if (m.cudaSupported)
    {
        m.cudaEnabled = enable;
    }

    if (m.cudaEnabled && m.cuda == NULL)
    {
        m.cuda = new CudaContext();

        if (!m.cuda->isValid())
        {
            m.cudaEnabled = false;
            m.cuda = NULL;
        }
    }
}

bool Compressor::isCudaAccelerationEnabled() const
{
    return m.cudaEnabled;
}

void Compressor::setTaskDispatcher(TaskDispatcher * disp)
{
    if (disp == NULL) {
        m.dispatcher = &m.defaultDispatcher;
    }
    else {
        m.dispatcher = disp;
    }
}


// Input Options API.
bool Compressor::process(const InputOptions & inputOptions, const CompressionOptions & compressionOptions, const OutputOptions & outputOptions) const
{
    return m.compress(inputOptions.m, compressionOptions.m, outputOptions.m);
}

int Compressor::estimateSize(const InputOptions & inputOptions, const CompressionOptions & compressionOptions) const
{
    int w = inputOptions.m.width;
    int h = inputOptions.m.height;
    int d = inputOptions.m.depth;
    
    getTargetExtent(&w, &h, &d, inputOptions.m.maxExtent, inputOptions.m.roundMode, inputOptions.m.textureType);

    int mipmapCount = 1;
    if (inputOptions.m.generateMipmaps) {
        mipmapCount = countMipmaps(w, h, d);
        if (inputOptions.m.maxLevel > 0) mipmapCount = min(mipmapCount, inputOptions.m.maxLevel);
    }

    return inputOptions.m.faceCount * estimateSize(w, h, d, mipmapCount, compressionOptions);
}


// Surface API.
bool Compressor::outputHeader(const Surface & tex, int mipmapCount, const CompressionOptions & compressionOptions, const OutputOptions & outputOptions) const
{
    return m.outputHeader(tex.type(), tex.width(), tex.height(), tex.depth(), 1, mipmapCount, tex.isNormalMap(), compressionOptions.m, outputOptions.m);
}

bool Compressor::compress(const Surface & tex, int face, int mipmap, const CompressionOptions & compressionOptions, const OutputOptions & outputOptions) const
{
    return m.compress(tex, face, mipmap, compressionOptions.m, outputOptions.m);
}

int Compressor::estimateSize(const Surface & tex, int mipmapCount, const CompressionOptions & compressionOptions) const
{
    const int w = tex.width();
    const int h = tex.height();
    const int d = tex.depth();

    return estimateSize(w, h, d, mipmapCount, compressionOptions);
}

bool Compressor::outputHeader(const CubeSurface & cube, int mipmapCount, const CompressionOptions & compressionOptions, const OutputOptions & outputOptions) const
{
    return m.outputHeader(TextureType_Cube, cube.edgeLength(), cube.edgeLength(), 1, 1, mipmapCount, false, compressionOptions.m, outputOptions.m);
}

bool Compressor::compress(const CubeSurface & cube, int mipmap, const CompressionOptions & compressionOptions, const OutputOptions & outputOptions) const
{
    for (int i = 0; i < 6; i++) {
        if(!m.compress(cube.face(i), i, mipmap, compressionOptions.m, outputOptions.m)) {
            return false;
        }
    }
    return true;
}

int Compressor::estimateSize(const CubeSurface & cube, int mipmapCount, const CompressionOptions & compressionOptions) const
{
    return 6 * estimateSize(cube.edgeLength(), cube.edgeLength(), 1, mipmapCount, compressionOptions);
}


// Raw API.
bool Compressor::outputHeader(TextureType type, int w, int h, int d, int arraySize, int mipmapCount, bool isNormalMap, const CompressionOptions & compressionOptions, const OutputOptions & outputOptions) const
{
    return m.outputHeader(type, w, h, d, arraySize, mipmapCount, isNormalMap, compressionOptions.m, outputOptions.m);
}

bool Compressor::compress(int w, int h, int d, int face, int mipmap, const float * rgba, const CompressionOptions & compressionOptions, const OutputOptions & outputOptions) const
{
    return m.compress(AlphaMode_None, w, h, d, face, mipmap, rgba, compressionOptions.m, outputOptions.m);
}

int Compressor::estimateSize(int w, int h, int d, int mipmapCount, const CompressionOptions & compressionOptions) const
{
    const Format format = compressionOptions.m.format;

    const uint bitCount = compressionOptions.m.getBitCount();
    const uint pitchAlignment = compressionOptions.m.pitchAlignment;

    int size = 0;
    for (int m = 0; m < mipmapCount; m++)
    {
        size += computeImageSize(w, h, d, bitCount, pitchAlignment, format);

        // Compute extents of next mipmap:
        w = max(1, w / 2);
        h = max(1, h / 2);
        d = max(1, d / 2);
    }

    return size;
}





bool Compressor::Private::compress(const InputOptions::Private & inputOptions, const CompressionOptions::Private & compressionOptions, const OutputOptions::Private & outputOptions) const
{
    // Make sure enums match.
    nvStaticCheck(FloatImage::WrapMode_Clamp == (FloatImage::WrapMode)WrapMode_Clamp);
    nvStaticCheck(FloatImage::WrapMode_Mirror == (FloatImage::WrapMode)WrapMode_Mirror);
    nvStaticCheck(FloatImage::WrapMode_Repeat == (FloatImage::WrapMode)WrapMode_Repeat);

    // Get output handler.
    if (!outputOptions.hasValidOutputHandler()) {
        outputOptions.error(Error_FileOpen);
        return false;
    }

    const int faceCount = inputOptions.faceCount;
    int width = inputOptions.width;
    int height = inputOptions.height;
    int depth = inputOptions.depth;
    int arraySize = inputOptions.textureType == TextureType_Array ? faceCount : 1;

    nv::getTargetExtent(&width, &height, &depth, inputOptions.maxExtent, inputOptions.roundMode, inputOptions.textureType);

    // If the extents have not changed, then we can use source images for all mipmaps.
    bool canUseSourceImages = (inputOptions.width == width && inputOptions.height == height && inputOptions.depth == depth);

    int mipmapCount = 1;
    if (inputOptions.generateMipmaps) {
        mipmapCount = countMipmaps(width, height, depth);
        if (inputOptions.maxLevel > 0) mipmapCount = min(mipmapCount, inputOptions.maxLevel);
    }

    if (!outputHeader(inputOptions.textureType, width, height, depth, arraySize, mipmapCount, inputOptions.isNormalMap, compressionOptions, outputOptions)) {
        return false;
    }


    if (outputOptions.container != Container_KTX)
    {
        nvtt::Surface img;
        img.setWrapMode(inputOptions.wrapMode);
        img.setAlphaMode(inputOptions.alphaMode);
        img.setNormalMap(inputOptions.isNormalMap);

        // Output each face from the largest mipmap to the smallest.
        for (int f = 0; f < faceCount; f++)
        {
            int w = width;
            int h = height;
            int d = depth;
            bool canUseSourceImagesForThisFace = canUseSourceImages;

            img.setImage(inputOptions.inputFormat, inputOptions.width, inputOptions.height, inputOptions.depth, inputOptions.images[f]);

            // To normal map.
            if (inputOptions.convertToNormalMap) {
                img.toGreyScale(inputOptions.heightFactors.x, inputOptions.heightFactors.y, inputOptions.heightFactors.z, inputOptions.heightFactors.w);
                img.toNormalMap(inputOptions.bumpFrequencyScale.x, inputOptions.bumpFrequencyScale.y, inputOptions.bumpFrequencyScale.z, inputOptions.bumpFrequencyScale.w);
            }

            // To linear space.
            if (!img.isNormalMap()) {
                img.toLinear(inputOptions.inputGamma);
            }

            // Resize input.
            img.resize(w, h, d, ResizeFilter_Box);

            nvtt::Surface tmp = img;
            if (!img.isNormalMap()) {
                tmp.toGamma(inputOptions.outputGamma);
            }

            quantize(tmp, compressionOptions);
            compress(tmp, f, 0, compressionOptions, outputOptions);

            for (int m = 1; m < mipmapCount; m++) {
                w = max(1, w/2);
                h = max(1, h/2);
                d = max(1, d/2);

                int idx = m * faceCount + f;

                bool useSourceImages = false;
                if (canUseSourceImagesForThisFace) {
                    if (inputOptions.images[idx] == NULL) { // One face is missing in this mipmap level.
                        canUseSourceImagesForThisFace = false; // If one level is missing, ignore the following source images.
                    }
                    else {
                        useSourceImages = true;
                    }
                }

                if (useSourceImages) {
                    img.setImage(inputOptions.inputFormat, w, h, d, inputOptions.images[idx]);

                    // For already generated mipmaps, we need to convert to linear.
                    if (!img.isNormalMap()) {
                        img.toLinear(inputOptions.inputGamma);
                    }
                }
                else {
                    if (inputOptions.mipmapFilter == MipmapFilter_Kaiser) {
                        float params[2] = { inputOptions.kaiserAlpha, inputOptions.kaiserStretch };
                        img.buildNextMipmap(MipmapFilter_Kaiser, inputOptions.kaiserWidth, params);
                    }
                    else {
                        img.buildNextMipmap(inputOptions.mipmapFilter);
                    }
                }
                nvDebugCheck(img.width() == w);
                nvDebugCheck(img.height() == h);
                nvDebugCheck(img.depth() == d);

                if (img.isNormalMap()) {
                    if (inputOptions.normalizeMipmaps) {
                        img.expandNormals();
                        img.normalizeNormalMap();
                        img.packNormals();
                    }
                    tmp = img;
                }
                else {
                    tmp = img;
                    tmp.toGamma(inputOptions.outputGamma);
                }

                quantize(tmp, compressionOptions);
                compress(tmp, f, m, compressionOptions, outputOptions);
            }
        }
    }
    else
    {
        // KTX files expect face mipmaps to be interleaved.
        Array<nvtt::Surface> images(faceCount);
        Array<bool> mipChainBroken(faceCount);

        int w = width;
        int h = height;
        int d = depth;

        // https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/#2.16
        uint imageSize = estimateSize(w, h, 1, 1, compressionOptions) * faceCount;
        outputOptions.writeData(&imageSize, sizeof(uint32));

        for (int f = 0; f < faceCount; f++)
        {
            nvtt::Surface s;
            s.setWrapMode(inputOptions.wrapMode);
            s.setAlphaMode(inputOptions.alphaMode);
            s.setNormalMap(inputOptions.isNormalMap);

            s.setImage(inputOptions.inputFormat, inputOptions.width, inputOptions.height, inputOptions.depth, inputOptions.images[f]);

            // To normal map.
            if (inputOptions.convertToNormalMap) {
                s.toGreyScale(inputOptions.heightFactors.x, inputOptions.heightFactors.y, inputOptions.heightFactors.z, inputOptions.heightFactors.w);
                s.toNormalMap(inputOptions.bumpFrequencyScale.x, inputOptions.bumpFrequencyScale.y, inputOptions.bumpFrequencyScale.z, inputOptions.bumpFrequencyScale.w);
            }

            // To linear space.
            if (!s.isNormalMap()) {
                s.toLinear(inputOptions.inputGamma);
            }

            // Resize input.
            s.resize(w, h, d, ResizeFilter_Box);

            nvtt::Surface tmp = s;
            if (!s.isNormalMap()) {
                tmp.toGamma(inputOptions.outputGamma);
            }

            quantize(tmp, compressionOptions);
            compress(tmp, f, 0, compressionOptions, outputOptions);

            images.push_back(s);
            mipChainBroken.push_back(false);
        }

        static const unsigned char padding[3] = {0, 0, 0};
        for (int m = 1; m < mipmapCount; m++)
        {
            w = max(1, w/2);
            h = max(1, h/2);
            d = max(1, d/2);

            // https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/#2.16
            imageSize = estimateSize(w, h, d, 1, compressionOptions) * faceCount;

            outputOptions.writeData(&imageSize, sizeof(uint32));

            nvtt::Surface tmp;

            for (int f = 0; f < faceCount; f++)
            {
                nvtt::Surface& img = images[f];
                int idx = m * faceCount + f;

                bool useSourceImages = false;
                if (!mipChainBroken[f]) {
                    if (inputOptions.images[idx] == NULL) { // One face is missing in this mipmap level.
                        mipChainBroken[f] = false; // If one level is missing, ignore the following source images.
                    }
                    else {
                        useSourceImages = true;
                    }
                }

                if (useSourceImages) {
                    img.setImage(inputOptions.inputFormat, w, h, d, inputOptions.images[idx]);

                    // For already generated mipmaps, we need to convert to linear.
                    if (!img.isNormalMap()) {
                        img.toLinear(inputOptions.inputGamma);
                    }
                }
                else {
                    if (inputOptions.mipmapFilter == MipmapFilter_Kaiser) {
                        float params[2] = { inputOptions.kaiserStretch, inputOptions.kaiserAlpha };
                        img.buildNextMipmap(MipmapFilter_Kaiser, inputOptions.kaiserWidth, params);
                    }
                    else {
                        img.buildNextMipmap(inputOptions.mipmapFilter);
                    }
                }
                nvDebugCheck(img.width() == w);
                nvDebugCheck(img.height() == h);
                nvDebugCheck(img.depth() == d);

                if (img.isNormalMap()) {
                    if (inputOptions.normalizeMipmaps) {
                        img.normalizeNormalMap();
                    }
                    tmp = img;
                }
                else {
                    tmp = img;
                    tmp.toGamma(inputOptions.outputGamma);
                }

                quantize(tmp, compressionOptions);
                compress(tmp, f, m, compressionOptions, outputOptions);

                //cube padding
                if (faceCount == 6 && arraySize == 1)
                {
                    //TODO calc offset for uncompressed images
                }
            }

            int mipPadding = 3 - ((imageSize + 3) % 4);
            if (mipPadding != 0) {
                outputOptions.writeData(&padding, mipPadding);
            }
        }
    }

    return true;
}

bool Compressor::Private::compress(const Surface & tex, int face, int mipmap, const CompressionOptions::Private & compressionOptions, const OutputOptions::Private & outputOptions) const
{
    if (!compress(tex.alphaMode(), tex.width(), tex.height(), tex.depth(), face, mipmap, tex.data(), compressionOptions, outputOptions)) {
        return false;
    }

    return true;
}

bool Compressor::Private::compress(AlphaMode alphaMode, int w, int h, int d, int face, int mipmap, const float * rgba, const CompressionOptions::Private & compressionOptions, const OutputOptions::Private & outputOptions) const
{
    int size = computeImageSize(w, h, d, compressionOptions.getBitCount(), compressionOptions.pitchAlignment, compressionOptions.format);
    outputOptions.beginImage(size, w, h, d, face, mipmap);

    // Decide what compressor to use.
    AutoPtr<CompressorInterface> compressor;
#if defined HAVE_CUDA
    if (cudaEnabled && w * h >= 512)
    {
        compressor = chooseGpuCompressor(compressionOptions);
    }
#endif
    if (compressor == NULL)
    {
        compressor = chooseCpuCompressor(compressionOptions);
    }

    if (compressor == NULL)
    {
        outputOptions.error(Error_UnsupportedFeature);
    }
    else
    {
        compressor->compress(alphaMode, w, h, d, rgba, dispatcher, compressionOptions, outputOptions);
    }

    outputOptions.endImage();

    return true;
}


void Compressor::Private::quantize(Surface & img, const CompressionOptions::Private & compressionOptions) const
{
    if (compressionOptions.enableColorDithering) {
        if (compressionOptions.format >= Format_BC1 && compressionOptions.format <= Format_BC3) {
            img.quantize(0, 5, true, true);
            img.quantize(1, 6, true, true);
            img.quantize(2, 5, true, true);
        }
        else if (compressionOptions.format == Format_RGB) {
            img.quantize(0, compressionOptions.rsize, true, true);
            img.quantize(1, compressionOptions.gsize, true, true);
            img.quantize(2, compressionOptions.bsize, true, true);
        }
    }
    if (compressionOptions.enableAlphaDithering) {
        if (compressionOptions.format == Format_RGB) {
            img.quantize(3, compressionOptions.asize, true, true);
        }
    }
    else if (compressionOptions.binaryAlpha) {
        img.binarize(3, float(compressionOptions.alphaThreshold)/255.0f, compressionOptions.enableAlphaDithering);
    }
}

bool Compressor::Private::outputHeader(nvtt::TextureType textureType, int w, int h, int d, int arraySize, int mipmapCount, bool isNormalMap, const CompressionOptions::Private & compressionOptions, const OutputOptions::Private & outputOptions) const
{
    if (w <= 0 || h <= 0 || d <= 0 || arraySize <= 0 || mipmapCount <= 0)
    {
        outputOptions.error(Error_InvalidInput);
        return false;
    }

    if (!outputOptions.outputHeader)
    {
        return true;
    }

    // Output DDS header.
    if (outputOptions.container == Container_DDS || outputOptions.container == Container_DDS10)
    {
        DDSHeader header;

        header.setUserVersion(outputOptions.version);

        if (textureType == TextureType_2D) {
            nvCheck(arraySize == 1);
            header.setTexture2D();
        }
        else if (textureType == TextureType_Cube) {
            nvCheck(arraySize == 1);
            header.setTextureCube();
        }
        else if (textureType == TextureType_3D) {
            nvCheck(arraySize == 1);
            header.setTexture3D();
            header.setDepth(d);
        }
        else if (textureType == TextureType_Array) {
            header.setTextureArray(arraySize);
        }

        header.setWidth(w);
        header.setHeight(h);
        header.setMipmapCount(mipmapCount);

        bool supported = true;

        if (outputOptions.container == Container_DDS10)
        {
            if (compressionOptions.format == Format_RGBA)
            {
                const uint bitcount = compressionOptions.getBitCount();

                if (compressionOptions.pixelType == PixelType_Float) {
                    if (compressionOptions.rsize == 16 && compressionOptions.gsize == 16 && compressionOptions.bsize == 16 && compressionOptions.asize == 16) {
                        header.setDX10Format(DXGI_FORMAT_R16G16B16A16_FLOAT);
                    }
                    else if (compressionOptions.rsize == 11 && compressionOptions.gsize == 11 && compressionOptions.bsize == 10 && compressionOptions.asize == 0) {
                        header.setDX10Format(DXGI_FORMAT_R11G11B10_FLOAT);
                    }
                    else {
                        supported = false;
                    }
                }
                else {
                    if (bitcount == 16 && compressionOptions.rsize == 16) {
                        header.setDX10Format(DXGI_FORMAT_R16_UNORM);
                    }
                    else {
                        uint format = findDXGIFormat(compressionOptions.bitcount,
                                                     compressionOptions.rmask,
                                                     compressionOptions.gmask,
                                                     compressionOptions.bmask,
                                                     compressionOptions.amask);

                        if (format != DXGI_FORMAT_UNKNOWN) {
                            header.setDX10Format(format);
                        }
                        else {
                            supported = false;
                        }
                    }
                }
            }
            else
            {
                if (compressionOptions.format == Format_DXT1 || compressionOptions.format == Format_DXT1a || compressionOptions.format == Format_DXT1n) {
                    header.setDX10Format(outputOptions.srgb ? DXGI_FORMAT_BC1_UNORM_SRGB : DXGI_FORMAT_BC1_UNORM);
                    if (compressionOptions.format == Format_DXT1a) header.setHasAlphaFlag(true);
                    if (isNormalMap) header.setNormalFlag(true);
                }
                else if (compressionOptions.format == Format_DXT3) {
                    header.setDX10Format(outputOptions.srgb ? DXGI_FORMAT_BC2_UNORM_SRGB : DXGI_FORMAT_BC2_UNORM);
                }
                else if (compressionOptions.format == Format_DXT5 || compressionOptions.format == Format_BC3_RGBM) {
                    header.setDX10Format(outputOptions.srgb ? DXGI_FORMAT_BC3_UNORM_SRGB : DXGI_FORMAT_BC3_UNORM);
                }
                else if (compressionOptions.format == Format_DXT5n) {
                    header.setDX10Format(DXGI_FORMAT_BC3_UNORM);
                    if (isNormalMap) header.setNormalFlag(true);
                }
                else if (compressionOptions.format == Format_BC4) {
                    header.setDX10Format(DXGI_FORMAT_BC4_UNORM); // DXGI_FORMAT_BC4_SNORM ?
                }
                else if (compressionOptions.format == Format_BC5 /*|| compressionOptions.format == Format_BC5_Luma*/) {
                    header.setDX10Format(DXGI_FORMAT_BC5_UNORM); // DXGI_FORMAT_BC5_SNORM ?
                    if (isNormalMap) header.setNormalFlag(true);
                }
                else if (compressionOptions.format == Format_BC6) {
                    if (compressionOptions.pixelType == PixelType_Float) header.setDX10Format(DXGI_FORMAT_BC6H_SF16);
                    /*if (compressionOptions.pixelType == PixelType_UnsignedFloat)*/ header.setDX10Format(DXGI_FORMAT_BC6H_UF16); // By default we assume unsigned.
                }
                else if (compressionOptions.format == Format_BC7) {
                    header.setDX10Format(outputOptions.srgb ? DXGI_FORMAT_BC7_UNORM_SRGB : DXGI_FORMAT_BC7_UNORM);
                    if (isNormalMap) header.setNormalFlag(true);
                }
                else if (compressionOptions.format == Format_CTX1) {
                    supported = false;
                }
                else {
                    supported = false;
                }
            }
        }
        else
        {
            if (compressionOptions.format == Format_RGBA)
            {
                // Get output bit count.
                header.setPitch(computeBytePitch(w, compressionOptions.getBitCount(), compressionOptions.pitchAlignment));

                if (compressionOptions.pixelType == PixelType_Float)
                {
                    if (compressionOptions.rsize == 16 && compressionOptions.gsize == 0 && compressionOptions.bsize == 0 && compressionOptions.asize == 0)
                    {
                        header.setFormatCode(111); // D3DFMT_R16F
                    }
                    else if (compressionOptions.rsize == 16 && compressionOptions.gsize == 16 && compressionOptions.bsize == 0 && compressionOptions.asize == 0)
                    {
                        header.setFormatCode(112); // D3DFMT_G16R16F
                    }
                    else if (compressionOptions.rsize == 16 && compressionOptions.gsize == 16 && compressionOptions.bsize == 16 && compressionOptions.asize == 16)
                    {
                        header.setFormatCode(113); // D3DFMT_A16B16G16R16F
                    }
                    else if (compressionOptions.rsize == 32 && compressionOptions.gsize == 0 && compressionOptions.bsize == 0 && compressionOptions.asize == 0)
                    {
                        header.setFormatCode(114); // D3DFMT_R32F
                    }
                    else if (compressionOptions.rsize == 32 && compressionOptions.gsize == 32 && compressionOptions.bsize == 0 && compressionOptions.asize == 0)
                    {
                        header.setFormatCode(115); // D3DFMT_G32R32F
                    }
                    else if (compressionOptions.rsize == 32 && compressionOptions.gsize == 32 && compressionOptions.bsize == 32 && compressionOptions.asize == 32)
                    {
                        header.setFormatCode(116); // D3DFMT_A32B32G32R32F
                    }
                    else
                    {
                        supported = false;
                    }
                }
                else // Fixed point
                {
                    const uint bitcount = compressionOptions.getBitCount();

                    if (compressionOptions.bitcount != 0)
                    {
                        // Masks already computed.
                        header.setPixelFormat(compressionOptions.bitcount, compressionOptions.rmask, compressionOptions.gmask, compressionOptions.bmask, compressionOptions.amask);
                    }
                    else if (bitcount <= 32)
                    {
                        // Compute pixel format masks.
                        const uint ashift = 0;
                        const uint bshift = ashift + compressionOptions.asize;
                        const uint gshift = bshift + compressionOptions.bsize;
                        const uint rshift = gshift + compressionOptions.gsize;

                        const uint rmask = ((1 << compressionOptions.rsize) - 1) << rshift;
                        const uint gmask = ((1 << compressionOptions.gsize) - 1) << gshift;
                        const uint bmask = ((1 << compressionOptions.bsize) - 1) << bshift;
                        const uint amask = ((1 << compressionOptions.asize) - 1) << ashift;

                        header.setPixelFormat(bitcount, rmask, gmask, bmask, amask);
                    }
                    else
                    {
                        supported = false;
                    }
                }
            }
            else
            {
                header.setLinearSize(computeImageSize(w, h, d, compressionOptions.bitcount, compressionOptions.pitchAlignment, compressionOptions.format));

                if (compressionOptions.format == Format_DXT1 || compressionOptions.format == Format_DXT1a || compressionOptions.format == Format_DXT1n) {
                    header.setFourCC('D', 'X', 'T', '1');
                    if (isNormalMap) header.setNormalFlag(true);
                }
                else if (compressionOptions.format == Format_DXT3) {
                    header.setFourCC('D', 'X', 'T', '3');
                }
                else if (compressionOptions.format == Format_DXT5 || compressionOptions.format == Format_BC3_RGBM) {
                    header.setFourCC('D', 'X', 'T', '5');
                }
                else if (compressionOptions.format == Format_DXT5n) {
                    header.setFourCC('D', 'X', 'T', '5');
                    if (isNormalMap) {
                        header.setNormalFlag(true);
                        header.setSwizzleCode('A', '2', 'D', '5');
                        //header.setSwizzleCode('x', 'G', 'x', 'R');
                    }
                }
                else if (compressionOptions.format == Format_BC4) {
                    header.setFourCC('A', 'T', 'I', '1');
                }
                else if (compressionOptions.format == Format_BC5 /*|| compressionOptions.format == Format_BC5_Luma*/) {
                    header.setFourCC('A', 'T', 'I', '2');
                    if (isNormalMap) {
                        header.setNormalFlag(true);
                        header.setSwizzleCode('A', '2', 'X', 'Y');
                    }
                }
                else if (compressionOptions.format == Format_BC6) {
                    header.setFourCC('Z', 'O', 'H', ' ');               // This is not supported by D3DX. Always use DX10 header with BC6-7 formats.
                    supported = false;
                }
                else if (compressionOptions.format == Format_BC7) {
                    header.setFourCC('Z', 'O', 'L', 'A');               // This is not supported by D3DX. Always use DX10 header with BC6-7 formats.
                    if (isNormalMap) header.setNormalFlag(true);
                    supported = false;
                }
                else if (compressionOptions.format == Format_CTX1) {
                    header.setFourCC('C', 'T', 'X', '1');
                    if (isNormalMap) header.setNormalFlag(true);
                }
                else {
                    supported = false;
                }
            }

            if (outputOptions.srgb) header.setSrgbFlag(true);
        }

        if (!supported)
        {
            // This container does not support the requested format.
            outputOptions.error(Error_UnsupportedOutputFormat);
            return false;
        }

        uint headerSize = 128;
        if (header.hasDX10Header())
        {
            nvStaticCheck(sizeof(DDSHeader) == 128 + 20);
            headerSize = 128 + 20;
        }

        // Swap bytes if necessary.
        header.swapBytes();

        bool writeSucceed = outputOptions.writeData(&header, headerSize);
        if (!writeSucceed)
        {
            outputOptions.error(Error_FileWrite);
        }

        return writeSucceed;
    }
    else if (outputOptions.container == Container_KTX) 
    {
        KtxHeader header;
        // TODO cube arrays
        if (textureType == TextureType_2D) {
            nvCheck(arraySize == 1);
            header.numberOfArrayElements = 0;
            header.numberOfFaces = 1;
            header.pixelDepth = 0;
        }
        else if (textureType == TextureType_Cube) {
            nvCheck(arraySize == 1);
            header.numberOfArrayElements = 0;
            header.numberOfFaces = 6;
            header.pixelDepth = 0;
        }
        else if (textureType == TextureType_3D) {
            nvCheck(arraySize == 1);
            header.numberOfArrayElements = 0;
            header.numberOfFaces = 1;
            header.pixelDepth = d;
        }
        else if (textureType == TextureType_Array) {
            header.numberOfArrayElements = arraySize;
            header.numberOfFaces = 1;
            header.pixelDepth = 0; // Is it?
        }

        header.pixelWidth = w;
        header.pixelHeight = h;
        header.numberOfMipmapLevels = mipmapCount;

        bool supported = true;

        // TODO non-compressed formats
        if (compressionOptions.format == Format_RGBA)
        {
            //header.glType = ?;
            //header.glTypeSize = ?;
            //header.glFormat = ?;
        }
        else
        {
            header.glType = 0;
            header.glTypeSize = 1;
            header.glFormat = 0;
            
            if (compressionOptions.format == Format_DXT1) {
                header.glInternalFormat = outputOptions.srgb ? KTX_INTERNAL_COMPRESSED_SRGB_S3TC_DXT1 : KTX_INTERNAL_COMPRESSED_RGB_S3TC_DXT1;
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RGB;
            }
            else if (compressionOptions.format == Format_DXT1a) {
                header.glInternalFormat = outputOptions.srgb ? KTX_INTERNAL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1 : KTX_INTERNAL_COMPRESSED_RGBA_S3TC_DXT1;
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RGBA;
            }
            else if (compressionOptions.format == Format_DXT3) {
                header.glInternalFormat = outputOptions.srgb ? KTX_INTERNAL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3 : KTX_INTERNAL_COMPRESSED_RGBA_S3TC_DXT3;
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RGBA;
            }
            else if (compressionOptions.format == Format_DXT5 || compressionOptions.format == Format_BC3_RGBM) {
                header.glInternalFormat = outputOptions.srgb ? KTX_INTERNAL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5 : KTX_INTERNAL_COMPRESSED_RGBA_S3TC_DXT5;
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RGBA;
            }
            else if (compressionOptions.format == Format_BC4) {
                header.glInternalFormat = KTX_INTERNAL_COMPRESSED_RED_RGTC1; // KTX_INTERNAL_COMPRESSED_SIGNED_RED_RGTC1 ?
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RED;
            }
            else if (compressionOptions.format == Format_BC5) {
                header.glInternalFormat = KTX_INTERNAL_COMPRESSED_RG_RGTC2; // KTX_INTERNAL_COMPRESSED_SIGNED_RG_RGTC2 ?
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RG;
            }
            else if (compressionOptions.format == Format_BC6) {
                if (compressionOptions.pixelType == PixelType_Float) header.glInternalFormat = KTX_INTERNAL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT;
                else /*if (compressionOptions.pixelType == PixelType_UnsignedFloat)*/ header.glInternalFormat = KTX_INTERNAL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT; // By default we assume unsigned.
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RGB;
            }
            else if (compressionOptions.format == Format_BC7) {
                header.glInternalFormat = outputOptions.srgb ? KTX_INTERNAL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM : KTX_INTERNAL_COMPRESSED_RGBA_BPTC_UNORM;
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RGBA;
            }
            else if (compressionOptions.format == Format_ETC1) {
                header.glInternalFormat = outputOptions.srgb ? KTX_INTERNAL_COMPRESSED_SRGB_ETC1 : KTX_INTERNAL_COMPRESSED_RGB_ETC1;
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RGB;
            }
            else if (compressionOptions.format == Format_ETC2_R) {
                header.glInternalFormat = KTX_INTERNAL_COMPRESSED_RED_EAC;
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RED;
            }
            else if (compressionOptions.format == Format_ETC2_RG) {
                header.glInternalFormat = KTX_INTERNAL_COMPRESSED_RG_EAC;
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RG;
            }
            else if (compressionOptions.format == Format_ETC2_RGB) {
                header.glInternalFormat = outputOptions.srgb ? KTX_INTERNAL_COMPRESSED_SRGB_ETC2 : KTX_INTERNAL_COMPRESSED_RGB_ETC2;
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RGB;
            }
            else if (compressionOptions.format == Format_ETC2_RGBA) {
                header.glInternalFormat = outputOptions.srgb ? KTX_INTERNAL_COMPRESSED_SRGB_ALPHA_ETC2_EAC : KTX_INTERNAL_COMPRESSED_RGBA_ETC2_EAC;
                header.glBaseInternalFormat = KTX_BASE_INTERNAL_RGBA;
            }
            else {
                supported = false;
            }

            //TODO compressionOptions.format == Format_DXT1n, Format_DXT5n ? There seems to be no way to indicate a normal map using ktx. Maybe via key value data?
        }
        
        if (!supported)
        {
            // This container does not support the requested format.
            outputOptions.error(Error_UnsupportedOutputFormat);
            return false;
        }

        const uint headerSize = 64;
        nvStaticCheck(sizeof(KtxHeader) == 64);

        bool writeSucceed = outputOptions.writeData(&header, headerSize);
        if (!writeSucceed)
        {
            outputOptions.error(Error_FileWrite);
        }

        return writeSucceed;
    }

    return true;
}


CompressorInterface * Compressor::Private::chooseCpuCompressor(const CompressionOptions::Private & compressionOptions) const
{
    if (compressionOptions.format == Format_RGB)
    {
        return new PixelFormatConverter;
    }
    else if (compressionOptions.format == Format_DXT1)
    {
#if defined(HAVE_ATITC)
        if (compressionOptions.externalCompressor == "ati") return new AtiCompressorDXT1;
        else
#endif

#if defined(HAVE_SQUISH)
        if (compressionOptions.externalCompressor == "squish") return new SquishCompressorDXT1;
        else
#endif

#if defined(HAVE_D3DX)
        if (compressionOptions.externalCompressor == "d3dx") return new D3DXCompressorDXT1;
        else
#endif

#if defined(HAVE_D3DX)
        if (compressionOptions.externalCompressor == "stb") return new StbCompressorDXT1;
        else
#endif

        if (compressionOptions.quality == Quality_Fastest)
        {
            return new FastCompressorDXT1;
        }

        return new CompressorDXT1;
    }
    else if (compressionOptions.format == Format_DXT1a)
    {
        if (compressionOptions.quality == Quality_Fastest)
        {
            return new FastCompressorDXT1a;
        }

        return new CompressorDXT1a;
    }
    else if (compressionOptions.format == Format_DXT1n)
    {
        // Not supported.
    }
    else if (compressionOptions.format == Format_DXT3)
    {
        if (compressionOptions.quality == Quality_Fastest)
        {
            return new FastCompressorDXT3;
        }

        return new CompressorDXT3;
    }
    else if (compressionOptions.format == Format_DXT5)
    {
#if defined(HAVE_ATITC)
        if (compressionOptions.externalCompressor == "ati") return new AtiCompressorDXT5;
        else
#endif

        if (compressionOptions.quality == Quality_Fastest)
        {
            return new FastCompressorDXT5;
        }

        return new CompressorDXT5;
    }
    else if (compressionOptions.format == Format_DXT5n)
    {
        if (compressionOptions.quality == Quality_Fastest)
        {
            return new FastCompressorDXT5n;
        }

        return new CompressorDXT5n;
    }
    else if (compressionOptions.format == Format_BC4)
    {
        if (compressionOptions.quality == Quality_Fastest || compressionOptions.quality == Quality_Normal)
        {
            return new FastCompressorBC4;
        }

        return new ProductionCompressorBC4;
    }
    else if (compressionOptions.format == Format_BC5)
    {
        if (compressionOptions.quality == Quality_Fastest || compressionOptions.quality == Quality_Normal)
        {
            return new FastCompressorBC5;
        }

        return new ProductionCompressorBC5;
    }
    else if (compressionOptions.format == Format_CTX1)
    {
        // Not supported.
    }
    else if (compressionOptions.format == Format_BC6)
    {
        return new CompressorBC6;
    }
    else if (compressionOptions.format == Format_BC7)
    {
        return new CompressorBC7;
    }
    else if (compressionOptions.format == Format_BC3_RGBM)
    {
        return new CompressorBC3_RGBM;
    }
    else if (compressionOptions.format >= Format_ETC1 && compressionOptions.format <= Format_ETC2_RGB_A1)
    {
#if defined(HAVE_RGETC)
        if (compressionOptions.format == Format_ETC1 && compressionOptions.externalCompressor == "rg_etc") return new RgEtcCompressor;
#endif
#if defined(HAVE_ETCLIB)
        if (compressionOptions.externalCompressor == "etclib") return new EtcLibCompressor;
#endif
        if (compressionOptions.format == Format_ETC1) return new CompressorETC1;
        else if (compressionOptions.format == Format_ETC2_R) return new CompressorETC2_R;
        //else if (compressionOptions.format == Format_ETC2_RG) return new CompressorETC2_RG;
        else if (compressionOptions.format == Format_ETC2_RGB) return new CompressorETC2_RGB;
        else if (compressionOptions.format == Format_ETC2_RGBA) return new CompressorETC2_RGBA;
    }
    else if (compressionOptions.format == Format_ETC2_RGBM)
    {
        return new CompressorETC2_RGBM;
    }
    else if (compressionOptions.format >= Format_PVR_2BPP_RGB && compressionOptions.format <= Format_PVR_4BPP_RGBA)
    {
#if defined(HAVE_PVRTEXTOOL)
        return new CompressorPVR;
#endif
    }
    return NULL;
}


CompressorInterface * Compressor::Private::chooseGpuCompressor(const CompressionOptions::Private & compressionOptions) const
{
    nvDebugCheck(cudaSupported);

    if (compressionOptions.quality == Quality_Fastest)
    {
        // Do not use CUDA compressors in fastest quality mode.
        return NULL;
    }

#if defined HAVE_CUDA
    if (compressionOptions.format == Format_DXT1)
    {
        return new CudaCompressorDXT1(*cuda);
    }
    else if (compressionOptions.format == Format_DXT1a)
    {
        //#pragma NV_MESSAGE("TODO: Implement CUDA DXT1a compressor.")
    }
    else if (compressionOptions.format == Format_DXT1n)
    {
        // Not supported.
    }
    else if (compressionOptions.format == Format_DXT3)
    {
        //return new CudaCompressorDXT3(*cuda);
    }
    else if (compressionOptions.format == Format_DXT5)
    {
        //return new CudaCompressorDXT5(*cuda);
    }
    else if (compressionOptions.format == Format_DXT5n)
    {
        // @@ Return CUDA compressor.
    }
    else if (compressionOptions.format == Format_BC4)
    {
        // Not supported.
    }
    else if (compressionOptions.format == Format_BC5)
    {
        // Not supported.
    }
    else if (compressionOptions.format == Format_CTX1)
    {
        // @@ Return CUDA compressor.
    }
    else if (compressionOptions.format == Format_BC6)
    {
        // Not supported.
    }
    else if (compressionOptions.format == Format_BC7)
    {
        // Not supported.
    }
#endif // defined HAVE_CUDA

    return NULL;
}

int Compressor::Private::estimateSize(int w, int h, int d, int mipmapCount, const CompressionOptions::Private & compressionOptions) const
{
    const Format format = compressionOptions.format;

    const uint bitCount = compressionOptions.bitcount;
    const uint pitchAlignment = compressionOptions.pitchAlignment;

    int size = 0;
    for (int m = 0; m < mipmapCount; m++)
    {
        size += computeImageSize(w, h, d, bitCount, pitchAlignment, format);

        // Compute extents of next mipmap:
        w = max(1, w / 2);
        h = max(1, h / 2);
        d = max(1, d / 2);
    }

    return size;
}
