//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#include <MaterialXRender/ImageHandler.h>

#include <MaterialXGenShader/Shader.h>
#include <MaterialXGenShader/Util.h>

#include <iostream>

MATERIALX_NAMESPACE_BEGIN

const string IMAGE_PROPERTY_SEPARATOR("_");
const string UADDRESS_MODE_SUFFIX(IMAGE_PROPERTY_SEPARATOR + "uaddressmode");
const string VADDRESS_MODE_SUFFIX(IMAGE_PROPERTY_SEPARATOR + "vaddressmode");
const string FILTER_TYPE_SUFFIX(IMAGE_PROPERTY_SEPARATOR + "filtertype");
const string DEFAULT_COLOR_SUFFIX(IMAGE_PROPERTY_SEPARATOR + "default");

const string ImageLoader::BMP_EXTENSION = "bmp";
const string ImageLoader::EXR_EXTENSION = "exr";
const string ImageLoader::GIF_EXTENSION = "gif";
const string ImageLoader::HDR_EXTENSION = "hdr";
const string ImageLoader::JPG_EXTENSION = "jpg";
const string ImageLoader::JPEG_EXTENSION = "jpeg";
const string ImageLoader::PIC_EXTENSION = "pic";
const string ImageLoader::PNG_EXTENSION = "png";
const string ImageLoader::PSD_EXTENSION = "psd";
const string ImageLoader::TGA_EXTENSION = "tga";
const string ImageLoader::TIF_EXTENSION = "tif";
const string ImageLoader::TIFF_EXTENSION = "tiff";
const string ImageLoader::TX_EXTENSION = "tx";
const string ImageLoader::TXT_EXTENSION = "txt";
const string ImageLoader::TXR_EXTENSION = "txr";

//
// ImageLoader methods
//

bool ImageLoader::saveImage(const FilePath&, ConstImagePtr, bool)
{
    return false;
}

ImagePtr ImageLoader::loadImage(const FilePath&)
{
    return nullptr;
}

//
// ImageHandler methods
//

ImageHandler::ImageHandler(ImageLoaderPtr imageLoader)
{
    addLoader(imageLoader);
    _zeroImage = createUniformImage(2, 2, 4, Image::BaseType::UINT8, Color4(0.0f));
}

void ImageHandler::addLoader(ImageLoaderPtr loader)
{
    if (loader)
    {
        const StringSet& extensions = loader->supportedExtensions();
        for (const auto& extension : extensions)
        {
            _imageLoaders[extension].push_back(loader);
        }
    }
}

StringSet ImageHandler::supportedExtensions()
{
    StringSet extensions;
    for (const auto& pair : _imageLoaders)
    {
        extensions.insert(pair.first);
    }
    return extensions;
}

bool ImageHandler::saveImage(const FilePath& filePath,
                             ConstImagePtr image,
                             bool verticalFlip)
{
    if (!image)
    {
        return false;
    }

    FilePath foundFilePath = _searchPath.find(filePath);
    if (foundFilePath.isEmpty())
    {
        return false;
    }

    string extension = foundFilePath.getExtension();
    for (ImageLoaderPtr loader : _imageLoaders[extension])
    {
        bool saved = false;
        try
        {
            saved = loader->saveImage(foundFilePath, image, verticalFlip);
        }
        catch (std::exception& e)
        {
            std::cerr << "Exception in image I/O library: " << e.what() << std::endl;
        }
        if (saved)
        {
            return true;
        }
    }
    return false;
}

ImagePtr ImageHandler::acquireImage(const FilePath& filePath, const Color4& defaultColor)
{
    // Resolve the input filepath.
    FilePath resolvedFilePath = filePath;
    if (_resolver)
    {
        resolvedFilePath = _resolver->resolve(resolvedFilePath, FILENAME_TYPE_STRING);
    }

    // Return a cached image if available.
    ImagePtr cachedImage = getCachedImage(resolvedFilePath);
    if (cachedImage)
    {
        return cachedImage;
    }

    // Load and cache the requested image.
    ImagePtr image = loadImage(_searchPath.find(resolvedFilePath));
    if (image)
    {
        cacheImage(resolvedFilePath, image);
        return image;
    }

    // No valid image was found, so generate a uniform texture with the given default color.
    // TODO: This step assumes that the missing image and its default color are in the same
    //       color space, which is not always the case.
    ImagePtr defaultImage = createUniformImage(1, 1, 4, Image::BaseType::UINT8, defaultColor);
    cacheImage(resolvedFilePath, defaultImage);
    return defaultImage;
}

bool ImageHandler::bindImage(ImagePtr, const ImageSamplingProperties&)
{
    return false;
}

bool ImageHandler::unbindImage(ImagePtr)
{
    return false;
}

void ImageHandler::unbindImages()
{
    for (const auto& iter : _imageCache)
    {
        unbindImage(iter.second);
    }
}

bool ImageHandler::createRenderResources(ImagePtr, bool, bool)
{
    return false;
}

void ImageHandler::releaseRenderResources(ImagePtr)
{
}

ImageVec ImageHandler::getReferencedImages(ConstDocumentPtr doc)
{
    ImageVec imageVec;
    for (ElementPtr elem : doc->traverseTree())
    {
        if (elem->getActiveSourceUri() != doc->getSourceUri())
        {
            continue;
        }

        InputPtr input = elem->asA<Input>();
        if (input && input->getType() == FILENAME_TYPE_STRING)
        {
            ImagePtr image = acquireImage(input->getResolvedValueString());
            if (image)
            {
                imageVec.push_back(image);
            }
        }
    }
    return imageVec;
}

ImagePtr ImageHandler::loadImage(const FilePath& filePath)
{
    string extension = stringToLower(filePath.getExtension());
    for (ImageLoaderPtr loader : _imageLoaders[extension])
    {
        ImagePtr image;
        try
        {
            image = loader->loadImage(filePath);
        }
        catch (std::exception& e)
        {
            std::cerr << "Exception in image I/O library: " << e.what() << std::endl;
        }
        if (image)
        {
            return image;
        }
    }

    if (!filePath.isEmpty())
    {
        if (!filePath.exists())
        {
            std::cerr << string("Image file not found: ") + filePath.asString() << std::endl;
        }
        else if (!_imageLoaders.count(extension))
        {
            std::cerr << string("Unsupported image extension: ") + filePath.asString() << std::endl;
        }
        else
        {
            std::cerr << string("Image loader failed to parse image: ") + filePath.asString() << std::endl;
        }
    }

    return nullptr;
}

void ImageHandler::cacheImage(const string& filePath, ImagePtr image)
{
    _imageCache[filePath] = image;
}

ImagePtr ImageHandler::getCachedImage(const FilePath& filePath)
{
    if (_imageCache.count(filePath))
    {
        return _imageCache[filePath];
    }
    if (!filePath.isAbsolute())
    {
        for (const FilePath& path : _searchPath)
        {
            FilePath combined = path / filePath;
            if (_imageCache.count(combined))
            {
                return _imageCache[combined];
            }
        }
    }
    return nullptr;
}

//
// ImageSamplingProperties methods
//

void ImageSamplingProperties::setProperties(const string& fileNameUniform,
                                            const VariableBlock& uniformBlock)
{
    const int INVALID_MAPPED_INT_VALUE = -1; // Any value < 0 is not considered to be invalid

    // Get the additional texture parameters based on image uniform name
    // excluding the trailing "_file" postfix string
    string root = fileNameUniform;
    size_t pos = root.find_last_of(IMAGE_PROPERTY_SEPARATOR);
    if (pos != string::npos)
    {
        root = root.substr(0, pos);
    }

    const ShaderPort* port = uniformBlock.find(root + UADDRESS_MODE_SUFFIX);
    ValuePtr intValue = port ? port->getValue() : nullptr;
    uaddressMode = ImageSamplingProperties::AddressMode(intValue && intValue->isA<int>() ? intValue->asA<int>() : INVALID_MAPPED_INT_VALUE);

    port = uniformBlock.find(root + VADDRESS_MODE_SUFFIX);
    intValue = port ? port->getValue() : nullptr;
    vaddressMode = ImageSamplingProperties::AddressMode(intValue && intValue->isA<int>() ? intValue->asA<int>() : INVALID_MAPPED_INT_VALUE);

    port = uniformBlock.find(root + FILTER_TYPE_SUFFIX);
    intValue = port ? port->getValue() : nullptr;
    filterType = ImageSamplingProperties::FilterType(intValue && intValue->isA<int>() ? intValue->asA<int>() : INVALID_MAPPED_INT_VALUE);

    port = uniformBlock.find(root + DEFAULT_COLOR_SUFFIX);
    if (!port)
    {
        port = uniformBlock.find(root + DEFAULT_COLOR_SUFFIX + "_cm_in");
    }
    ValuePtr colorValue = port ? port->getValue() : nullptr;
    if (colorValue)
    {
        mapValueToColor(colorValue, defaultColor);
    }
}

bool ImageSamplingProperties::operator==(const ImageSamplingProperties& r) const
{
    return (enableMipmaps == r.enableMipmaps &&
            uaddressMode == r.uaddressMode &&
            vaddressMode == r.vaddressMode &&
            filterType == r.filterType &&
            defaultColor == r.defaultColor);
}

MATERIALX_NAMESPACE_END
