/*
*  This file is part of G'MIC-Qt, a generic plug-in for raster graphics
*  editors, offering hundreds of filters thanks to the underlying G'MIC
*  image processing framework.
*
*  Copyright (C) 2020, 2021 Nicholas Hayes
*
*  Portions Copyright 2017 Sebastien Fourey
*
*  G'MIC-Qt is free software: you can redistribute it and/or modify
*  it under the terms of the GNU General Public License as published by
*  the Free Software Foundation, either version 3 of the License, or
*  (at your option) any later version.
*
*  G'MIC-Qt 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 General Public License for more details.
*
*  You should have received a copy of the GNU General Public License
*  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QApplication>
#include <QImage>
#include <QString>
#include <QVector>
#include <QDebug>
#include <QDataStream>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <qglobal.h>
#include <QMessageBox>
#include <QUUid>
#include <iostream>
#include <limits>
#include <memory>
#include <new>
#include <list>
#include "Common.h"
#include "Host/GmicQtHost.h"
#include "ImageTools.h"
#include "GmicQt.h"
#include "gmic.h"
#include <lcms2.h>
#include <QMainWindow>

struct Gmic8bfLayer
{
    int32_t width;
    int32_t height;
    bool visible;
    QString name;
    gmic_library::gmic_image<float> imageData;
};

namespace host_8bf
{
    QString outputDir;
    QVector<Gmic8bfLayer> layers;
    int32_t activeLayerIndex;
    bool grayScale;
    uint8_t bitsPerChannel;
    int32_t documentWidth;
    int32_t documentHeight;
    int32_t hostTileWidth;
    int32_t hostTileHeight;
    cmsContext lcmsContext;
    cmsHPROFILE imageProfile;
    cmsHPROFILE displayProfile;
    bool fetchedDisplayProfileFromQtWidget;
    cmsHTRANSFORM transform;
    cmsUInt32Number transformFormat;
}

namespace GmicQtHost
{
    const QString ApplicationName = QString("8bf Hosts");
    const char * const ApplicationShortname = GMIC_QT_XSTRINGIFY(GMIC_HOST);
    const bool DarkThemeIsDefault = true;
}

namespace
{
    QString ReadUTF8String(QDataStream& dataStream)
    {
        int32_t length = 0;

        dataStream >> length;

        if (length == 0)
        {
            return QString();
        }
        else
        {
            QByteArray utf8Bytes(length, '\0');

            dataStream.readRawData(utf8Bytes.data(), length);

            return QString::fromUtf8(utf8Bytes);
        }
    }

    enum class InputFileParseStatus
    {
        Ok,
        FileOpenError,
        BadFileSignature,
        UnknownFileVersion,
        InvalidArgument,
        OutOfMemory,
        EndOfFile,
        PlatformEndianMismatch,
        FileReadError,
        IccProfileError
    };

    InputFileParseStatus FillTileBuffer(
        QDataStream& dataStream,
        const size_t& requiredSize,
        char* buffer)
    {
        size_t totalBytesRead = 0;

        while (totalBytesRead < requiredSize)
        {
            int bytesToRead = static_cast<int>(std::min(requiredSize - totalBytesRead, static_cast<size_t>(INT_MAX)));

            int bytesRead = dataStream.readRawData(buffer + totalBytesRead, bytesToRead);

            if (bytesRead <= 0)
            {
                return InputFileParseStatus::EndOfFile;
            }

            totalBytesRead += bytesRead;
        }

        return InputFileParseStatus::Ok;
    }

    InputFileParseStatus CopyTileToGmicImage8Interleaved(
        const unsigned char* tileBuffer,
        size_t tileBufferStride,
        int left,
        int top,
        int right,
        int bottom,
        gmic_library::gmic_image<float>& out)
    {
        const int imageWidth = out.width();
        const int numberOfChannels = out.spectrum();

        if (numberOfChannels == 3)
        {
            float* rPlane = out.data(0, 0, 0, 0);
            float* gPlane = out.data(0, 0, 0, 1);
            float* bPlane = out.data(0, 0, 0, 2);

            for (int y = top; y < bottom; ++y)
            {
                const unsigned char* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstR = rPlane + planeStart;
                float* dstG = gPlane + planeStart;
                float* dstB = bPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstR++ = static_cast<float>(src[0]);
                    *dstG++ = static_cast<float>(src[1]);
                    *dstB++ = static_cast<float>(src[2]);
                    src += 3;
                }
            }
        }
        else if (numberOfChannels == 4)
        {
            float* rPlane = out.data(0, 0, 0, 0);
            float* gPlane = out.data(0, 0, 0, 1);
            float* bPlane = out.data(0, 0, 0, 2);
            float* aPlane = out.data(0, 0, 0, 3);

            for (int y = top; y < bottom; ++y)
            {
                const unsigned char* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstR = rPlane + planeStart;
                float* dstG = gPlane + planeStart;
                float* dstB = bPlane + planeStart;
                float* dstA = aPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstR++ = static_cast<float>(src[0]);
                    *dstG++ = static_cast<float>(src[1]);
                    *dstB++ = static_cast<float>(src[2]);
                    *dstA++ = static_cast<float>(src[3]);
                    src += 4;
                }
            }
        }
        else if (numberOfChannels == 2)
        {
            float* grayPlane = out.data(0, 0, 0, 0);
            float* alphaPlane = out.data(0, 0, 0, 1);

            for (int y = top; y < bottom; ++y)
            {
                const unsigned char* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstGray = grayPlane + planeStart;
                float* dstAlpha = alphaPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstGray++ = static_cast<float>(src[0]);
                    *dstAlpha++ = static_cast<float>(src[1]);
                    src += 2;
                }
            }
        }
        else if (numberOfChannels == 1)
        {
            float* grayPlane = out.data(0, 0, 0, 0);

            for (int y = top; y < bottom; ++y)
            {
                const unsigned char* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstGray = grayPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstGray++ = static_cast<float>(src[0]);
                    src++;
                }
            }
        }
        else
        {
            return InputFileParseStatus::InvalidArgument;
        }

        return InputFileParseStatus::Ok;
    }

    InputFileParseStatus CopyTileToGmicImage8Planar(
        const unsigned char* tileBuffer,
        size_t tileBufferStride,
        int left,
        int top,
        int right,
        int bottom,
        int channelIndex,
        gmic_library::gmic_image<float>& image)
    {
        const int imageWidth = image.width();

        float* plane = image.data(0, 0, 0, channelIndex);

        for (int y = top; y < bottom; ++y)
        {
            const unsigned char* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);
            float* dst = plane + (static_cast<size_t>(y) * static_cast<size_t>(imageWidth)) + left;

            for (int x = left; x < right; x++)
            {
                *dst++ = static_cast<float>(src[0]);

                src++;
            }
        }

        return InputFileParseStatus::Ok;
    }

    InputFileParseStatus ConvertGmic8bfInputToGmicImage8(
        QDataStream& dataStream,
        int32_t inTileWidth,
        int32_t inTileHeight,
        int32_t inNumberOfChannels,
        bool planar,
        gmic_library::gmic_image<float>& image)
    {
        int32_t maxTileStride = planar ? inTileWidth : inTileWidth * inNumberOfChannels;
        size_t tileBufferSize = static_cast<size_t>(maxTileStride) * inTileHeight;

        std::unique_ptr<char[]> tileBuffer(new (std::nothrow) char[tileBufferSize]);

        if (!tileBuffer)
        {
            return InputFileParseStatus::OutOfMemory;
        }

        int width = image.width();
        int height = image.height();

        if (planar)
        {
            for (int i = 0; i < inNumberOfChannels; i++)
            {
                for (int y = 0; y < height; y += inTileHeight)
                {
                    int top = y;
                    int bottom = std::min(y + inTileHeight, height);

                    size_t rowCount = static_cast<size_t>(bottom) - top;

                    for (int x = 0; x < width; x += inTileWidth)
                    {
                        int left = x;
                        int right = std::min(x + inTileWidth, width);

                        size_t tileBufferStride = static_cast<size_t>(right) - left;
                        size_t bytesToRead = tileBufferStride * rowCount;

                        InputFileParseStatus status = FillTileBuffer(dataStream, bytesToRead, tileBuffer.get());

                        if (status != InputFileParseStatus::Ok)
                        {
                            return status;
                        }

                        status = CopyTileToGmicImage8Planar(
                            reinterpret_cast<const unsigned char*>(tileBuffer.get()),
                            tileBufferStride,
                            left,
                            top,
                            right,
                            bottom,
                            i,
                            image);

                        if (status != InputFileParseStatus::Ok)
                        {
                            return status;
                        }
                    }
                }
            }
        }
        else
        {
            for (int y = 0; y < height; y += inTileHeight)
            {
                int top = y;
                int bottom = std::min(y + inTileHeight, height);

                size_t rowCount = static_cast<size_t>(bottom) - top;

                for (int x = 0; x < width; x += inTileWidth)
                {
                    int left = x;
                    int right = std::min(x + inTileWidth, width);

                    size_t columnCount = static_cast<size_t>(right) - left;
                    size_t tileBufferStride = columnCount * inNumberOfChannels;
                    size_t bytesToRead = tileBufferStride * rowCount;

                    InputFileParseStatus status = FillTileBuffer(dataStream, bytesToRead, tileBuffer.get());

                    if (status != InputFileParseStatus::Ok)
                    {
                        return status;
                    }

                    status = CopyTileToGmicImage8Interleaved(
                        reinterpret_cast<const unsigned char*>(tileBuffer.get()),
                        tileBufferStride,
                        left,
                        top,
                        right,
                        bottom,
                        image);

                    if (status != InputFileParseStatus::Ok)
                    {
                        return status;
                    }
                }
            }
        }

        return InputFileParseStatus::Ok;
    }

    InputFileParseStatus CopyTileToGmicImage16Interleaved(
        const unsigned short* tileBuffer,
        size_t tileBufferStride,
        int left,
        int top,
        int right,
        int bottom,
        gmic_library::gmic_image<float>& image,
        const QVector<float>& sixteenBitToEightBitLUT)
    {
        const int imageWidth = image.width();

        if (image.spectrum() == 3)
        {
            float* rPlane = image.data(0, 0, 0, 0);
            float* gPlane = image.data(0, 0, 0, 1);
            float* bPlane = image.data(0, 0, 0, 2);

            for (int y = top; y < bottom; ++y)
            {
                const unsigned short* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstR = rPlane + planeStart;
                float* dstG = gPlane + planeStart;
                float* dstB = bPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstR++ = sixteenBitToEightBitLUT[src[0]];
                    *dstG++ = sixteenBitToEightBitLUT[src[1]];
                    *dstB++ = sixteenBitToEightBitLUT[src[2]];
                    src += 3;
                }
            }
        }
        else if (image.spectrum() == 4)
        {
            float* rPlane = image.data(0, 0, 0, 0);
            float* gPlane = image.data(0, 0, 0, 1);
            float* bPlane = image.data(0, 0, 0, 2);
            float* aPlane = image.data(0, 0, 0, 3);

            for (int y = top; y < bottom; ++y)
            {
                const unsigned short* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstR = rPlane + planeStart;
                float* dstG = gPlane + planeStart;
                float* dstB = bPlane + planeStart;
                float* dstA = aPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstR++ = sixteenBitToEightBitLUT[src[0]];
                    *dstG++ = sixteenBitToEightBitLUT[src[1]];
                    *dstB++ = sixteenBitToEightBitLUT[src[2]];
                    *dstA++ = sixteenBitToEightBitLUT[src[3]];
                    src += 4;
                }
            }
        }
        else if (image.spectrum() == 2)
        {
            float* grayPlane = image.data(0, 0, 0, 0);
            float* alphaPlane = image.data(0, 0, 0, 1);

            for (int y = top; y < bottom; ++y)
            {
                const unsigned short* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstGray = grayPlane + planeStart;
                float* dstAlpha = alphaPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstGray++ = sixteenBitToEightBitLUT[src[0]];
                    *dstAlpha++ = sixteenBitToEightBitLUT[src[1]];
                    src += 2;
                }
            }
        }
        else if (image.spectrum() == 1)
        {
            float* grayPlane = image.data(0, 0, 0, 0);

            for (int y = top; y < bottom; ++y)
            {
                const unsigned short* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstGray = grayPlane + planeStart;
                for (int x = left; x < right; x++)
                {
                    *dstGray++ = sixteenBitToEightBitLUT[src[0]];
                    src++;
                }
            }
        }
        else
        {
            return InputFileParseStatus::InvalidArgument;
        }

        return InputFileParseStatus::Ok;
    }

    InputFileParseStatus CopyTileToGmicImage16Planar(
        const unsigned short* tileBuffer,
        size_t tileBufferStride,
        int left,
        int top,
        int right,
        int bottom,
        int channelIndex,
        gmic_library::gmic_image<float>& image,
        const QVector<float>& sixteenBitToEightBitLUT)
    {
        const int imageWidth = image.width();

        float* plane = image.data(0, 0, 0, channelIndex);

        for (int y = top; y < bottom; ++y)
        {
            const unsigned short* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);
            float* dst = plane + (static_cast<size_t>(y) * imageWidth) + left;

            for (int x = left; x < right; x++)
            {
                *dst++ = sixteenBitToEightBitLUT[src[0]];
                src++;
            }
        }

        return InputFileParseStatus::Ok;
    }

    InputFileParseStatus ConvertGmic8bfInputToGmicImage16(
        QDataStream& dataStream,
        int32_t inTileWidth,
        int32_t inTileHeight,
        int32_t inNumberOfChannels,
        bool planar,
        gmic_library::gmic_image<float>& image)
    {
        size_t maxTileStride = planar ? inTileWidth : static_cast<size_t>(inTileWidth) * inNumberOfChannels;
        size_t tileBufferSize = maxTileStride * inTileHeight * 2;

        std::unique_ptr<char[]> tileBuffer(new (std::nothrow) char[tileBufferSize]);

        if (!tileBuffer)
        {
            return InputFileParseStatus::OutOfMemory;
        }

        QVector<float> sixteenBitToEightBitLUT;
        sixteenBitToEightBitLUT.reserve(65536);

        for (int i = 0; i < sixteenBitToEightBitLUT.capacity(); i++)
        {
            // G'MIC expect the input image data to be a floating-point value in the range of [0, 255].
            // We use a lookup table to avoid having to repeatedly perform division on the same values.
            sixteenBitToEightBitLUT.push_back(static_cast<float>(i) / 257.0f);
        }

        int width = image.width();
        int height = image.height();

        if (planar)
        {
            for (int i = 0; i < inNumberOfChannels; i++)
            {
                for (int y = 0; y < height; y += inTileHeight)
                {
                    int top = y;
                    int bottom = std::min(y + inTileHeight, height);

                    size_t rowCount = static_cast<size_t>(bottom) - top;

                    for (int x = 0; x < width; x += inTileWidth)
                    {
                        int left = x;
                        int right = std::min(x + inTileWidth, width);

                        size_t tileBufferStride = static_cast<size_t>(right) - left;
                        size_t bytesToRead = tileBufferStride * rowCount * 2;

                        InputFileParseStatus status = FillTileBuffer(dataStream, bytesToRead, tileBuffer.get());

                        if (status != InputFileParseStatus::Ok)
                        {
                            return status;
                        }

                        status = CopyTileToGmicImage16Planar(
                            reinterpret_cast<const unsigned short*>(tileBuffer.get()),
                            tileBufferStride,
                            left,
                            top,
                            right,
                            bottom,
                            i,
                            image,
                            sixteenBitToEightBitLUT);

                        if (status != InputFileParseStatus::Ok)
                        {
                            return status;
                        }
                    }
                }
            }
        }
        else
        {
            for (int y = 0; y < height; y += inTileHeight)
            {
                int top = y;
                int bottom = std::min(y + inTileHeight, height);

                size_t rowCount = static_cast<size_t>(bottom) - top;

                for (int x = 0; x < width; x += inTileWidth)
                {
                    int left = x;
                    int right = std::min(x + inTileWidth, width);

                    size_t columnCount = static_cast<size_t>(right) - left;
                    size_t tileBufferStride = columnCount * inNumberOfChannels;
                    size_t bytesToRead = tileBufferStride * rowCount * 2;

                    InputFileParseStatus status = FillTileBuffer(dataStream, bytesToRead, tileBuffer.get());

                    if (status != InputFileParseStatus::Ok)
                    {
                        return status;
                    }

                    status = CopyTileToGmicImage16Interleaved(
                        reinterpret_cast<const unsigned short*>(tileBuffer.get()),
                        tileBufferStride,
                        left,
                        top,
                        right,
                        bottom,
                        image,
                        sixteenBitToEightBitLUT);

                    if (status != InputFileParseStatus::Ok)
                    {
                        return status;
                    }
                }
            }
        }

        return InputFileParseStatus::Ok;
    }

    InputFileParseStatus CopyTileToGmicImage32Interleaved(
        const float* tileBuffer,
        size_t tileBufferStride,
        int left,
        int top,
        int right,
        int bottom,
        gmic_library::gmic_image<float>& out)
    {
        const int imageWidth = out.width();
        const int numberOfChannels = out.spectrum();

        if (numberOfChannels == 3)
        {
            float* rPlane = out.data(0, 0, 0, 0);
            float* gPlane = out.data(0, 0, 0, 1);
            float* bPlane = out.data(0, 0, 0, 2);

            for (int y = top; y < bottom; ++y)
            {
                const float* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstR = rPlane + planeStart;
                float* dstG = gPlane + planeStart;
                float* dstB = bPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstR++ = src[0];
                    *dstG++ = src[1];
                    *dstB++ = src[2];
                    src += 3;
                }
            }
        }
        else if (numberOfChannels == 4)
        {
            float* rPlane = out.data(0, 0, 0, 0);
            float* gPlane = out.data(0, 0, 0, 1);
            float* bPlane = out.data(0, 0, 0, 2);
            float* aPlane = out.data(0, 0, 0, 3);

            for (int y = top; y < bottom; ++y)
            {
                const float* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstR = rPlane + planeStart;
                float* dstG = gPlane + planeStart;
                float* dstB = bPlane + planeStart;
                float* dstA = aPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstR++ = src[0];
                    *dstG++ = src[1];
                    *dstB++ = src[2];
                    *dstA++ = src[3];
                    src += 4;
                }
            }
        }
        else if (numberOfChannels == 2)
        {
            float* grayPlane = out.data(0, 0, 0, 0);
            float* alphaPlane = out.data(0, 0, 0, 1);

            for (int y = top; y < bottom; ++y)
            {
                const float* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstGray = grayPlane + planeStart;
                float* dstAlpha = alphaPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstGray++ = src[0];
                    *dstAlpha++ = src[1];
                    src += 2;
                }
            }
        }
        else if (numberOfChannels == 1)
        {
            float* grayPlane = out.data(0, 0, 0, 0);

            for (int y = top; y < bottom; ++y)
            {
                const float* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);

                const size_t planeStart = (static_cast<size_t>(y) * imageWidth) + left;

                float* dstGray = grayPlane + planeStart;

                for (int x = left; x < right; x++)
                {
                    *dstGray++ = src[0];
                    src++;
                }
            }
        }
        else
        {
            return InputFileParseStatus::InvalidArgument;
        }

        return InputFileParseStatus::Ok;
    }

    InputFileParseStatus CopyTileToGmicImage32Planar(
        const float* tileBuffer,
        size_t tileBufferStride,
        int left,
        int top,
        int right,
        int bottom,
        int channelIndex,
        gmic_library::gmic_image<float>& image)
    {
        const int imageWidth = image.width();

        float* plane = image.data(0, 0, 0, channelIndex);

        for (int y = top; y < bottom; ++y)
        {
            const float* src = tileBuffer + ((static_cast<size_t>(y) - top) * tileBufferStride);
            float* dst = plane + (static_cast<size_t>(y) * static_cast<size_t>(imageWidth)) + left;

            for (int x = left; x < right; x++)
            {
                *dst++ = *src++;
            }
        }

        return InputFileParseStatus::Ok;
    }

    InputFileParseStatus ConvertGmic8bfInputToGmicImage32(
        QDataStream& dataStream,
        int32_t inTileWidth,
        int32_t inTileHeight,
        int32_t inNumberOfChannels,
        bool planar,
        gmic_library::gmic_image<float>& image)
    {
        int32_t maxTileStride = planar ? inTileWidth : inTileWidth * inNumberOfChannels;
        size_t tileBufferSize = static_cast<size_t>(maxTileStride) * inTileHeight * 4;

        std::unique_ptr<char[]> tileBuffer(new (std::nothrow) char[tileBufferSize]);

        if (!tileBuffer)
        {
            return InputFileParseStatus::OutOfMemory;
        }

        int width = image.width();
        int height = image.height();

        if (planar)
        {
            for (int i = 0; i < inNumberOfChannels; i++)
            {
                for (int y = 0; y < height; y += inTileHeight)
                {
                    int top = y;
                    int bottom = std::min(y + inTileHeight, height);

                    size_t rowCount = static_cast<size_t>(bottom) - top;

                    for (int x = 0; x < width; x += inTileWidth)
                    {
                        int left = x;
                        int right = std::min(x + inTileWidth, width);

                        size_t tileBufferStride = static_cast<size_t>(right) - left;
                        size_t bytesToRead = tileBufferStride * rowCount * 4;

                        InputFileParseStatus status = FillTileBuffer(dataStream, bytesToRead, tileBuffer.get());

                        if (status != InputFileParseStatus::Ok)
                        {
                            return status;
                        }

                        status = CopyTileToGmicImage32Planar(
                            reinterpret_cast<const float*>(tileBuffer.get()),
                            tileBufferStride,
                            left,
                            top,
                            right,
                            bottom,
                            i,
                            image);

                        if (status != InputFileParseStatus::Ok)
                        {
                            return status;
                        }
                    }
                }
            }
        }
        else
        {
            for (int y = 0; y < height; y += inTileHeight)
            {
                int top = y;
                int bottom = std::min(y + inTileHeight, height);

                size_t rowCount = static_cast<size_t>(bottom) - top;

                for (int x = 0; x < width; x += inTileWidth)
                {
                    int left = x;
                    int right = std::min(x + inTileWidth, width);

                    size_t columnCount = static_cast<size_t>(right) - left;
                    size_t tileBufferStride = columnCount * inNumberOfChannels;
                    size_t bytesToRead = tileBufferStride * rowCount * 4;

                    InputFileParseStatus status = FillTileBuffer(dataStream, bytesToRead, tileBuffer.get());

                    if (status != InputFileParseStatus::Ok)
                    {
                        return status;
                    }

                    status = CopyTileToGmicImage32Interleaved(
                        reinterpret_cast<const float*>(tileBuffer.get()),
                        tileBufferStride,
                        left,
                        top,
                        right,
                        bottom,
                        image);

                    if (status != InputFileParseStatus::Ok)
                    {
                        return status;
                    }
                }
            }
        }

        // Convert the image from [0, 1] t0 [0, 255].
        image *= 255;

        return InputFileParseStatus::Ok;
    }

    InputFileParseStatus ReadGmic8bfInput(const QString& path, gmic_library::gmic_image<float>& image, bool isActiveLayer)
    {
        QFile file(path);

        if (!file.open(QIODevice::ReadOnly))
        {
            return InputFileParseStatus::FileOpenError;
        }

        QDataStream dataStream(&file);

        char signature[4] = {};

        dataStream.readRawData(signature, 4);

        if (strncmp(signature, "G8IM", 4) != 0)
        {
            return InputFileParseStatus::BadFileSignature;
        }

        char endian[4] = {};

        dataStream.readRawData(endian, 4);

#if Q_BYTE_ORDER == Q_BIG_ENDIAN
        if (strncmp(endian, "BEDN", 4) == 0)
        {
            dataStream.setByteOrder(QDataStream::BigEndian);
        }
#elif Q_BYTE_ORDER == Q_LITTLE_ENDIAN
        if (strncmp(endian, "LEDN", 4) == 0)
        {
            dataStream.setByteOrder(QDataStream::LittleEndian);
        }
#else
#error "Unknown endianness on this platform."
#endif
        else
        {
            return InputFileParseStatus::PlatformEndianMismatch;
        }

        int32_t fileVersion = 0;

        dataStream >> fileVersion;

        if (fileVersion != 1)
        {
            return InputFileParseStatus::UnknownFileVersion;
        }

        int32_t width = 0;

        dataStream >> width;

        int32_t height = 0;

        dataStream >> height;

        int32_t numberOfChannels = 0;

        dataStream >> numberOfChannels;

        int32_t bitDepth = 0;

        dataStream >> bitDepth;

        int32_t flags = 0;

        dataStream >> flags;

        bool planar = false;

        planar = (flags & 1) != 0;

        int32_t inTileWidth = 0;

        dataStream >> inTileWidth;

        int32_t inTileHeight = 0;

        dataStream >> inTileHeight;

        if (isActiveLayer)
        {
            host_8bf::documentWidth = width;
            host_8bf::documentHeight = height;
            host_8bf::hostTileWidth = inTileWidth;
            host_8bf::hostTileHeight = inTileHeight;
        }

        image.assign(width, height, 1, numberOfChannels);

        InputFileParseStatus status = InputFileParseStatus::Ok;

        switch (bitDepth)
        {
        case 8:
            status = ConvertGmic8bfInputToGmicImage8(dataStream, inTileWidth, inTileHeight, numberOfChannels, planar, image);
            break;
        case 16:
            status = ConvertGmic8bfInputToGmicImage16(dataStream, inTileWidth, inTileHeight, numberOfChannels, planar, image);
            break;
        case 32:
            status = ConvertGmic8bfInputToGmicImage32(dataStream, inTileWidth, inTileHeight, numberOfChannels, planar, image);
            break;
        default:
            status = InputFileParseStatus::InvalidArgument;
            break;
        }

        return status;
    }

    InputFileParseStatus ReadColorProfile(const QString& path, cmsContext context, cmsHPROFILE* outProfile)
    {
        QFile file(path);

        if (!file.open(QIODevice::ReadOnly))
        {
            return InputFileParseStatus::FileOpenError;
        }

        QByteArray data = file.readAll();

        if (static_cast<qint64>(data.size()) != file.size())
        {
            return InputFileParseStatus::FileReadError;
        }

        *outProfile = cmsOpenProfileFromMemTHR(context, data.constData(), static_cast<cmsUInt32Number>(data.size()));

        return *outProfile != nullptr ? InputFileParseStatus::Ok : InputFileParseStatus::IccProfileError;
    }

    InputFileParseStatus ParseInputFileIndex(const QString& indexFilePath)
    {
        QFile file(indexFilePath);

        if (!file.open(QIODevice::ReadOnly))
        {
            return InputFileParseStatus::FileOpenError;
        }

        QDataStream dataStream(&file);

        char signature[4] = {};

        dataStream.readRawData(signature, 4);

        if (strncmp(signature, "G8LI", 4) != 0)
        {
            return InputFileParseStatus::BadFileSignature;
        }

        char endian[4] = {};

        dataStream.readRawData(endian, 4);

#if Q_BYTE_ORDER == Q_BIG_ENDIAN
        if (strncmp(endian, "BEDN", 4) == 0)
        {
            dataStream.setByteOrder(QDataStream::BigEndian);
        }
#elif Q_BYTE_ORDER == Q_LITTLE_ENDIAN
        if (strncmp(endian, "LEDN", 4) == 0)
        {
            dataStream.setByteOrder(QDataStream::LittleEndian);
        }
#else
#error "Unknown endianness on this platform."
#endif
        else
        {
            return InputFileParseStatus::PlatformEndianMismatch;
        }

        int32_t fileVersion = 0;

        dataStream >> fileVersion;

        if (fileVersion != 3)
        {
            return InputFileParseStatus::UnknownFileVersion;
        }

        int32_t layerCount = 0;

        dataStream >> layerCount;

        dataStream >> host_8bf::activeLayerIndex;

        dataStream >> host_8bf::bitsPerChannel;

        uint8_t grayScale;

        dataStream >> grayScale;

        host_8bf::grayScale = grayScale != 0;

        uint8_t haveIccProfiles;

        dataStream >> haveIccProfiles;

        // Skip the padding byte.
        dataStream.skipRawData(1);

        if (haveIccProfiles != 0)
        {
            QString imageColorProfile = ReadUTF8String(dataStream);
            QString displayColorProfile = ReadUTF8String(dataStream);

            host_8bf::lcmsContext = cmsCreateContext(nullptr, nullptr);

            if (host_8bf::lcmsContext == nullptr)
            {
                return InputFileParseStatus::IccProfileError;
            }

            InputFileParseStatus status = ReadColorProfile(imageColorProfile, host_8bf::lcmsContext, &host_8bf::imageProfile);

            if (status != InputFileParseStatus::Ok)
            {
                return status;
            }

            status = ReadColorProfile(displayColorProfile, host_8bf::lcmsContext, &host_8bf::displayProfile);

            if (status != InputFileParseStatus::Ok)
            {
                return status;
            }
        }

        host_8bf::layers.reserve(layerCount);

        for (int32_t i = 0; i < layerCount; i++)
        {
            int32_t layerWidth = 0;

            dataStream >> layerWidth;

            int32_t layerHeight = 0;

            dataStream >> layerHeight;

            int32_t layerVisible = 0;

            dataStream >> layerVisible;

            QString layerName = ReadUTF8String(dataStream);

            QString filePath = ReadUTF8String(dataStream);

            gmic_library::gmic_image<float> image;

            InputFileParseStatus status = ReadGmic8bfInput(filePath, image, i == host_8bf::activeLayerIndex);

            if (status != InputFileParseStatus::Ok)
            {
                return status;
            }

            Gmic8bfLayer layer{};
            layer.width = layerWidth;
            layer.height = layerHeight;
            layer.visible = layerVisible != 0;
            layer.name = layerName;
            layer.imageData = image;

            host_8bf::layers.push_back(layer);
        }

        if (layerCount > 1)
        {
            // The 8bf plug-in sends layers in bottom to top order, whereas the
            // G'MIC-Qt plug-in for GIMP sends layers in top to bottom order.
            // So we reverse the layer list to match the behavior of the G'MIC-Qt
            // plug-in for GIMP.

            host_8bf::activeLayerIndex = layerCount - (1 + host_8bf::activeLayerIndex);

            // Adapted from https://stackoverflow.com/a/20652805
            for(int k = 0, s = host_8bf::layers.size(), max = (s / 2); k < max; k++)
            {
                host_8bf::layers.swapItemsAt(k, s - (1 + k));
            }
        }

        return InputFileParseStatus::Ok;
    }

    QVector<Gmic8bfLayer> FilterLayersForInputMode(GmicQt::InputMode mode)
    {
        if (host_8bf::layers.size() == 1 || mode == GmicQt::InputMode::All)
        {
            return host_8bf::layers;
        }
        else
        {
            QVector<Gmic8bfLayer> filteredLayers;

            if (mode == GmicQt::InputMode::Active)
            {
                filteredLayers.push_back(host_8bf::layers[host_8bf::activeLayerIndex]);
            }
            else if (mode == GmicQt::InputMode::ActiveAndAbove)
            {
                const QVector<Gmic8bfLayer>& layers = host_8bf::layers;

                // This case is the opposite of the GIMP plug-in because the layer order has
                // been reversed to match the top to bottom order that the GIMP plug-in uses.
                if (host_8bf::activeLayerIndex > 0)
                {
                    filteredLayers.push_back(layers[host_8bf::activeLayerIndex - 1]);
                }
                filteredLayers.push_back(layers[host_8bf::activeLayerIndex]);
            }
            else if (mode == GmicQt::InputMode::ActiveAndBelow)
            {
                const QVector<Gmic8bfLayer>& layers = host_8bf::layers;

                // This case is the opposite of the GIMP plug-in because the layer order has
                // been reversed to match the top to bottom order that the GIMP plug-in uses.
                filteredLayers.push_back(layers[host_8bf::activeLayerIndex]);
                if (host_8bf::activeLayerIndex < (layers.size() - 1))
                {
                    filteredLayers.push_back(layers[host_8bf::activeLayerIndex + 1]);
                }
            }
            else if (mode == GmicQt::InputMode::AllVisible)
            {
                const QVector<Gmic8bfLayer>& layers = host_8bf::layers;

                for (int i = 0; i < layers.size(); i++)
                {
                    const Gmic8bfLayer& layer = layers[i];
                    if (layer.visible)
                    {
                        filteredLayers.push_back(layer);
                    }
                }
            }
            else if (mode == GmicQt::InputMode::AllInvisible)
            {
                const QVector<Gmic8bfLayer>& layers = host_8bf::layers;

                for (int i = 0; i < layers.size(); i++)
                {
                    const Gmic8bfLayer& layer = layers[i];
                    if (!layer.visible)
                    {
                        filteredLayers.push_back(layer);
                    }
                }
            }

            return filteredLayers;
        }
    }

    // The following method was copied from ImageConverter.cpp.

    inline unsigned char float2uchar_bounded(const float& in)
    {
        return (in < 0.0f) ? 0 : ((in > 255.0f) ? 255 : static_cast<unsigned char>(in));
    }

    inline unsigned short float2ushort_bounded(const float& in)
    {
        // Scale the value from [0, 255] to [0, 65535].
        const float fullRangeValue = in * 257.0f;

        return (fullRangeValue < 0.0f) ? 0 : ((fullRangeValue > 65535.0f) ? 65535 : static_cast<unsigned short>(fullRangeValue));
    }

    void WriteGmic8bfImageHeader(
        QDataStream& stream,
        int width,
        int height,
        int numberOfChannels,
        int bitsPerChannel,
        bool planar,
        int tileWidth,
        int tileHeight)
    {
        const int fileVersion = 1;
        const int flags = planar ? 1 : 0;

        stream.writeRawData("G8IM", 4);
#if Q_BYTE_ORDER == Q_BIG_ENDIAN
        stream.writeRawData("BEDN", 4);
        stream.setByteOrder(QDataStream::BigEndian);
#elif Q_BYTE_ORDER == Q_LITTLE_ENDIAN
        stream.writeRawData("LEDN", 4);
        stream.setByteOrder(QDataStream::LittleEndian);
#else
#error "Unknown endianness on this platform."
#endif
        stream << fileVersion;
        stream << width;
        stream << height;
        stream << numberOfChannels;
        stream << bitsPerChannel;
        stream << flags;
        stream << tileWidth;
        stream << tileHeight;
    }

    void WriteGmicOutputTile8Interleaved(
        QDataStream& dataStream,
        const gmic_library::gmic_image<float>& in,
        unsigned char* rowBuffer,
        int rowBufferLengthInBytes,
        int left,
        int top,
        int right,
        int bottom)
    {
        // The following code has been adapted from ImageConverter.cpp.

        if (in.spectrum() == 3)
        {
            const float* rPlane = in.data(0, 0, 0, 0);
            const float* gPlane = in.data(0, 0, 0, 1);
            const float* bPlane = in.data(0, 0, 0, 2);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* srcR = rPlane + planeStart;
                const float* srcG = gPlane + planeStart;
                const float* srcB = bPlane + planeStart;
                unsigned char* dst = rowBuffer;

                for (int x = left; x < right; ++x)
                {
                    dst[0] = float2uchar_bounded(*srcR++);
                    dst[1] = float2uchar_bounded(*srcG++);
                    dst[2] = float2uchar_bounded(*srcB++);

                    dst += 3;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
        else if (in.spectrum() == 4)
        {
            const float* rPlane = in.data(0, 0, 0, 0);
            const float* gPlane = in.data(0, 0, 0, 1);
            const float* bPlane = in.data(0, 0, 0, 2);
            const float* aPlane = in.data(0, 0, 0, 3);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* srcR = rPlane + planeStart;
                const float* srcG = gPlane + planeStart;
                const float* srcB = bPlane + planeStart;
                const float* srcA = aPlane + planeStart;

                unsigned char* dst = rowBuffer;

                for (int x = left; x < right; ++x)
                {
                    dst[0] = float2uchar_bounded(*srcR++);
                    dst[1] = float2uchar_bounded(*srcG++);
                    dst[2] = float2uchar_bounded(*srcB++);
                    dst[3] = float2uchar_bounded(*srcA++);

                    dst += 4;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
        else if (in.spectrum() == 2)
        {
            //
            // Gray + Alpha
            //
            const float* grayPlane = in.data(0, 0, 0, 0);
            const float* alphaPlane = in.data(0, 0, 0, 1);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* src = grayPlane + planeStart;
                const float* srcA = alphaPlane + planeStart;

                unsigned char* dst = rowBuffer;


                for (int x = left; x < right; ++x)
                {
                    dst[0] = float2uchar_bounded(*src++);
                    dst[1] = float2uchar_bounded(*srcA++);

                    dst += 2;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
        else
        {
            //
            // 8-bits Gray levels
            //
            const float* grayPlane = in.data(0, 0, 0, 0);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* src = grayPlane + planeStart;

                unsigned char* dst = rowBuffer;

                for (int x = left; x < right; ++x)
                {
                    dst[0] = float2uchar_bounded(*src++);

                    dst++;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
    }

    void WriteGmicOutputTile8Planar(
        QDataStream& dataStream,
        const gmic_library::gmic_image<float>& in,
        unsigned char* rowBuffer,
        int rowBufferLengthInBytes,
        int left,
        int top,
        int right,
        int bottom,
        int plane)
    {
        const float* srcPlane = in.data(0, 0, 0, plane);

        for (int y = top; y < bottom; ++y)
        {
            const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

            const float* src = srcPlane + planeStart;

            unsigned char* dst = rowBuffer;

            for (int x = left; x < right; ++x)
            {
                dst[0] = float2uchar_bounded(*src++);

                dst++;
            }

            dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
        }
    }

    void WriteGmicOutput8(
        const QString& outputFilePath,
        const gmic_library::gmic_image<float>& in,
        bool planar,
        int32_t tileWidth,
        int32_t tileHeight)
    {
        QFile file(outputFilePath);
        file.open(QFile::WriteOnly);
        QDataStream dataStream(&file);
        dataStream.setByteOrder(QDataStream::LittleEndian);

        const int width = in.width();
        const int height = in.height();
        const int numberOfChannels = in.spectrum();

        WriteGmic8bfImageHeader(dataStream, width, height, numberOfChannels, 8, planar, tileWidth, tileHeight);

        if (planar)
        {
            std::vector<unsigned char> rowBuffer(width);

            for (int i = 0; i < numberOfChannels; ++i)
            {
                for (int y = 0; y < height; y += tileHeight)
                {
                    int top = y;
                    int bottom = std::min(y + tileHeight, height);

                    for (int x = 0; x < width; x += tileWidth)
                    {
                        int left = x;
                        int right = std::min(x + tileWidth, width);

                        int rowBufferLengthInBytes = right - left;

                        WriteGmicOutputTile8Planar(
                            dataStream,
                            in,
                            rowBuffer.data(),
                            rowBufferLengthInBytes,
                            left,
                            top,
                            right,
                            bottom,
                            i);
                    }
                }
            }
        }
        else
        {
            std::vector<unsigned char> rowBuffer(static_cast<size_t>(width) * numberOfChannels);

            for (int y = 0; y < height; y += tileHeight)
            {
                int top = y;
                int bottom = std::min(y + tileHeight, height);

                for (int x = 0; x < width; x += tileWidth)
                {
                    int left = x;
                    int right = std::min(x + tileWidth, width);

                    int rowBufferLengthInBytes = (right - left) * numberOfChannels;

                    WriteGmicOutputTile8Interleaved(
                        dataStream,
                        in,
                        rowBuffer.data(),
                        rowBufferLengthInBytes,
                        left,
                        top,
                        right,
                        bottom);
                }
            }
        }
    }

    void WriteGmicOutputTile16Interleaved(
        QDataStream& dataStream,
        const gmic_library::gmic_image<float>& in,
        unsigned short* rowBuffer,
        int rowBufferLengthInBytes,
        int left,
        int top,
        int right,
        int bottom)
    {
        // The following code has been adapted from ImageConverter.cpp.

        if (in.spectrum() == 3)
        {
            const float* rPlane = in.data(0, 0, 0, 0);
            const float* gPlane = in.data(0, 0, 0, 1);
            const float* bPlane = in.data(0, 0, 0, 2);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* srcR = rPlane + planeStart;
                const float* srcG = gPlane + planeStart;
                const float* srcB = bPlane + planeStart;
                unsigned short* dst = rowBuffer;

                for (int x = left; x < right; ++x)
                {
                    dst[0] = float2ushort_bounded(*srcR++);
                    dst[1] = float2ushort_bounded(*srcG++);
                    dst[2] = float2ushort_bounded(*srcB++);

                    dst += 3;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
        else if (in.spectrum() == 4)
        {
            const float* rPlane = in.data(0, 0, 0, 0);
            const float* gPlane = in.data(0, 0, 0, 1);
            const float* bPlane = in.data(0, 0, 0, 2);
            const float* aPlane = in.data(0, 0, 0, 3);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* srcR = rPlane + planeStart;
                const float* srcG = gPlane + planeStart;
                const float* srcB = bPlane + planeStart;
                const float* srcA = aPlane + planeStart;

                unsigned short* dst = rowBuffer;

                for (int x = left; x < right; ++x)
                {
                    dst[0] = float2ushort_bounded(*srcR++);
                    dst[1] = float2ushort_bounded(*srcG++);
                    dst[2] = float2ushort_bounded(*srcB++);
                    dst[3] = float2ushort_bounded(*srcA++);

                    dst += 4;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
        else if (in.spectrum() == 2)
        {
            //
            // Gray + Alpha
            //
            const float* grayPlane = in.data(0, 0, 0, 0);
            const float* alphaPlane = in.data(0, 0, 0, 1);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* src = grayPlane + planeStart;
                const float* srcA = alphaPlane + planeStart;

                unsigned short* dst = rowBuffer;


                for (int x = left; x < right; ++x)
                {
                    dst[0] = float2ushort_bounded(*src++);
                    dst[1] = float2ushort_bounded(*srcA++);

                    dst += 2;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
        else
        {
            //
            // 16-bits Gray levels
            //
            const float* grayPlane = in.data(0, 0, 0, 0);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* src = grayPlane + planeStart;

                unsigned short* dst = rowBuffer;

                for (int x = left; x < right; ++x)
                {
                    dst[0] = float2ushort_bounded(*src++);

                    dst++;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
    }

    void WriteGmicOutputTile16Planar(
        QDataStream& dataStream,
        const gmic_library::gmic_image<float>& in,
        unsigned short* rowBuffer,
        int rowBufferLengthInBytes,
        int left,
        int top,
        int right,
        int bottom,
        int plane)
    {
        const float* srcPlane = in.data(0, 0, 0, plane);

        for (int y = top; y < bottom; ++y)
        {
            const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

            const float* src = srcPlane + planeStart;

            unsigned short* dst = rowBuffer;

            for (int x = left; x < right; ++x)
            {
                dst[0] = float2ushort_bounded(*src++);

                dst++;
            }

            dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
        }
    }

    void WriteGmicOutput16(
        const QString& outputFilePath,
        const gmic_library::gmic_image<float>& in,
        bool planar,
        int32_t tileWidth,
        int32_t tileHeight)
    {
        QFile file(outputFilePath);
        file.open(QFile::WriteOnly);
        QDataStream dataStream(&file);
        dataStream.setByteOrder(QDataStream::LittleEndian);

        const int width = in.width();
        const int height = in.height();
        const int numberOfChannels = in.spectrum();

        WriteGmic8bfImageHeader(dataStream, width, height, numberOfChannels, 16, planar, tileWidth, tileHeight);

        if (planar)
        {
            std::vector<unsigned short> rowBuffer(width);

            for (int i = 0; i < numberOfChannels; ++i)
            {
                for (int y = 0; y < height; y += tileHeight)
                {
                    int top = y;
                    int bottom = std::min(y + tileHeight, height);

                    for (int x = 0; x < width; x += tileWidth)
                    {
                        int left = x;
                        int right = std::min(x + tileWidth, width);

                        int columnCount = right - left;

                        int rowBufferLengthInBytes = columnCount * 2;

                        WriteGmicOutputTile16Planar(
                            dataStream,
                            in,
                            rowBuffer.data(),
                            rowBufferLengthInBytes,
                            left,
                            top,
                            right,
                            bottom,
                            i);
                    }
                }
            }
        }
        else
        {
            std::vector<unsigned short> rowBuffer(static_cast<size_t>(width) * numberOfChannels);

            for (int y = 0; y < height; y += tileHeight)
            {
                int top = y;
                int bottom = std::min(y + tileHeight, height);

                for (int x = 0; x < width; x += tileWidth)
                {
                    int left = x;
                    int right = std::min(x + tileWidth, width);

                    int rowBufferLengthInBytes = ((right - left) * numberOfChannels) * 2;

                    WriteGmicOutputTile16Interleaved(
                        dataStream,
                        in,
                        rowBuffer.data(),
                        rowBufferLengthInBytes,
                        left,
                        top,
                        right,
                        bottom);
                }
            }
        }
    }

    void WriteGmicOutputTile32Interleaved(
        QDataStream& dataStream,
        const gmic_library::gmic_image<float>& in,
        float* rowBuffer,
        int rowBufferLengthInBytes,
        int left,
        int top,
        int right,
        int bottom)
    {
        // The following code has been adapted from ImageConverter.cpp.

        if (in.spectrum() == 3)
        {
            const float* rPlane = in.data(0, 0, 0, 0);
            const float* gPlane = in.data(0, 0, 0, 1);
            const float* bPlane = in.data(0, 0, 0, 2);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* srcR = rPlane + planeStart;
                const float* srcG = gPlane + planeStart;
                const float* srcB = bPlane + planeStart;
                float* dst = rowBuffer;

                for (int x = left; x < right; ++x)
                {
                    dst[0] = *srcR++;
                    dst[1] = *srcG++;
                    dst[2] = *srcB++;

                    dst += 3;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
        else if (in.spectrum() == 4)
        {
            const float* rPlane = in.data(0, 0, 0, 0);
            const float* gPlane = in.data(0, 0, 0, 1);
            const float* bPlane = in.data(0, 0, 0, 2);
            const float* aPlane = in.data(0, 0, 0, 3);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* srcR = rPlane + planeStart;
                const float* srcG = gPlane + planeStart;
                const float* srcB = bPlane + planeStart;
                const float* srcA = aPlane + planeStart;

                float* dst = rowBuffer;

                for (int x = left; x < right; ++x)
                {
                    dst[0] = *srcR++;
                    dst[1] = *srcG++;
                    dst[2] = *srcB++;
                    dst[3] = *srcA++;

                    dst += 4;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
        else if (in.spectrum() == 2)
        {
            //
            // Gray + Alpha
            //
            const float* grayPlane = in.data(0, 0, 0, 0);
            const float* alphaPlane = in.data(0, 0, 0, 1);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* src = grayPlane + planeStart;
                const float* srcA = alphaPlane + planeStart;

                float* dst = rowBuffer;

                for (int x = left; x < right; ++x)
                {
                    dst[0] = *src++;
                    dst[1] = *srcA++;

                    dst += 2;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
        else
        {
            //
            // 16-bits Gray levels
            //
            const float* grayPlane = in.data(0, 0, 0, 0);

            for (int y = top; y < bottom; ++y)
            {
                const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

                const float* src = grayPlane + planeStart;

                float* dst = rowBuffer;

                for (int x = left; x < right; ++x)
                {
                    *dst++ = *src++;
                }

                dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
            }
        }
    }

    void WriteGmicOutputTile32Planar(
        QDataStream& dataStream,
        const gmic_library::gmic_image<float>& in,
        float* rowBuffer,
        int rowBufferLengthInBytes,
        int left,
        int top,
        int right,
        int bottom,
        int plane)
    {
        const float* srcPlane = in.data(0, 0, 0, plane);

        for (int y = top; y < bottom; ++y)
        {
            const size_t planeStart = (static_cast<size_t>(y) * in.width()) + left;

            const float* src = srcPlane + planeStart;

            float* dst = rowBuffer;

            for (int x = left; x < right; ++x)
            {
                *dst++ = *src++;
            }

            dataStream.writeRawData(reinterpret_cast<const char*>(rowBuffer), rowBufferLengthInBytes);
        }
    }

    void WriteGmicOutput32(
        const QString& outputFilePath,
        gmic_library::gmic_image<float>& in,
        bool planar,
        int32_t tileWidth,
        int32_t tileHeight)
    {
        // Convert the image from [0, 255] to [0, 1].
        in /= 255;

        QFile file(outputFilePath);
        file.open(QFile::WriteOnly);
        QDataStream dataStream(&file);
        dataStream.setByteOrder(QDataStream::LittleEndian);

        const int width = in.width();
        const int height = in.height();
        const int numberOfChannels = in.spectrum();

        WriteGmic8bfImageHeader(dataStream, width, height, numberOfChannels, 32, planar, tileWidth, tileHeight);

        if (planar)
        {
            std::vector<float> rowBuffer(width);

            for (int i = 0; i < numberOfChannels; ++i)
            {
                for (int y = 0; y < height; y += tileHeight)
                {
                    int top = y;
                    int bottom = std::min(y + tileHeight, height);

                    for (int x = 0; x < width; x += tileWidth)
                    {
                        int left = x;
                        int right = std::min(x + tileWidth, width);

                        int rowBufferLengthInBytes = (right - left) * 4;

                        WriteGmicOutputTile32Planar(
                            dataStream,
                            in,
                            rowBuffer.data(),
                            rowBufferLengthInBytes,
                            left,
                            top,
                            right,
                            bottom,
                            i);
                    }
                }
            }
        }
        else
        {
            std::vector<float> rowBuffer(static_cast<size_t>(width) * numberOfChannels);

            for (int y = 0; y < height; y += tileHeight)
            {
                int top = y;
                int bottom = std::min(y + tileHeight, height);

                for (int x = 0; x < width; x += tileWidth)
                {
                    int left = x;
                    int right = std::min(x + tileWidth, width);

                    int rowBufferLengthInBytes = ((right - left) * numberOfChannels) * 4;

                    WriteGmicOutputTile32Interleaved(
                        dataStream,
                        in,
                        rowBuffer.data(),
                        rowBufferLengthInBytes,
                        left,
                        top,
                        right,
                        bottom);
                }
            }
        }
    }

    void EmptyOutputFolder()
    {
        QDir dir(host_8bf::outputDir);
        dir.setFilter(QDir::NoDotAndDotDot | QDir::Files);
        foreach(QString dirFile, dir.entryList())
        {
            dir.remove(dirFile);
        }
    }

    GmicQt::InputMode ReadGmic8bfInputMode(QDataStream& dataStream)
    {
        GmicQt::InputMode mode = GmicQt::InputMode::Active;

        QString str = ReadUTF8String(dataStream);

        if (str == "All Layers")
        {
            mode = GmicQt::InputMode::All;
        }
        else if (str == "Active Layer and Below")
        {
            mode = GmicQt::InputMode::ActiveAndBelow;
        }
        else if (str == "Active Layer and Above")
        {
            mode = GmicQt::InputMode::ActiveAndAbove;
        }
        else if (str == "All Visible Layers")
        {
            mode = GmicQt::InputMode::AllVisible;
        }
        else if (str == "All Hidden Layers")
        {
            mode = GmicQt::InputMode::AllInvisible;
        }

        return mode;
    }

    bool ReadGmic8bfFilterParameters(const QString& path, GmicQt::RunParameters& parameters)
    {
        QFile file(path);

        if (file.open(QFile::ReadOnly))
        {
            QDataStream dataStream(&file);

            char signature[4] = {};

            dataStream.readRawData(signature, 4);

            if (strncmp(signature, "G8FP", 4) != 0)
            {
                return false;
            }

            char endian[4] = {};

            dataStream.readRawData(endian, 4);

#if Q_BYTE_ORDER == Q_BIG_ENDIAN
            if (strncmp(endian, "BEDN", 4) == 0)
            {
                dataStream.setByteOrder(QDataStream::BigEndian);
            }
#elif Q_BYTE_ORDER == Q_LITTLE_ENDIAN
            if (strncmp(endian, "LEDN", 4) == 0)
            {
                dataStream.setByteOrder(QDataStream::LittleEndian);
            }
#else
#error "Unknown endianness on this platform."
#endif
            else
            {
                return false;
            }

            int32_t fileVersion = 0;

            dataStream >> fileVersion;

            if (fileVersion != 1)
            {
                return false;
            }

            parameters.command = ReadUTF8String(dataStream).toStdString();
            parameters.filterPath = ReadUTF8String(dataStream).toStdString();
            parameters.inputMode = ReadGmic8bfInputMode(dataStream);
        }

        return true;
    }

    GmicQt::RunParameters GetFilterRunParameters(const QString& path)
    {
        GmicQt::RunParameters parameters = GmicQt::lastAppliedFilterRunParameters(GmicQt::ReturnedRunParametersFlag::AfterFilterExecution);

        if (!path.isEmpty())
        {
            ReadGmic8bfFilterParameters(path, parameters);
        }

        return parameters;
    }

    void WriteGmic8bfInputMode(QDataStream& dataStream, GmicQt::InputMode inputMode)
    {
        QString str;

        switch (inputMode)
        {
        case GmicQt::InputMode::All:
            str = "All Layers";
            break;
        case GmicQt::InputMode::ActiveAndBelow:
            str = "Active Layer and Below";
            break;
        case GmicQt::InputMode::ActiveAndAbove:
            str = "Active Layer and Above";
            break;
        case GmicQt::InputMode::AllVisible:
            str = "All Visible Layers";
            break;
        case GmicQt::InputMode::AllInvisible:
            str = "All Hidden Layers";
            break;
        case GmicQt::InputMode::Active:
        default:
            str = "Active Layer";
            break;
        }

        QByteArray utf8Bytes = str.toUtf8();

        dataStream << utf8Bytes.size();

        dataStream.writeRawData(utf8Bytes.constData(), utf8Bytes.size());
    }

    void WriteUtf8String(QDataStream& dataStream, const std::string& str)
    {
        if (str.size() <= INT_MAX)
        {
            const int stringLength = static_cast<int>(str.size());

            dataStream << stringLength;

            dataStream.writeRawData(str.c_str(), stringLength);
        }
    }

    void WriteGmic8bfFilterParameters(const QString& path, const GmicQt::RunParameters& parameters)
    {
        if (path.isEmpty())
        {
            return;
        }

        QFile file(path);

        if (file.open(QFile::WriteOnly))
        {
            QDataStream dataStream(&file);

            const int32_t fileVersion = 1;

            dataStream.writeRawData("G8FP", 4);
#if Q_BYTE_ORDER == Q_BIG_ENDIAN
            stream.writeRawData("BEDN", 4);
            dataStream.setByteOrder(QDataStream::BigEndian);
#elif Q_BYTE_ORDER == Q_LITTLE_ENDIAN
            dataStream.writeRawData("LEDN", 4);
            dataStream.setByteOrder(QDataStream::LittleEndian);
#else
#error "Unknown endianness on this platform."
#endif
            dataStream << fileVersion;

            WriteUtf8String(dataStream, parameters.command);
            WriteUtf8String(dataStream, parameters.filterPath);
            WriteGmic8bfInputMode(dataStream, parameters.inputMode);
            WriteUtf8String(dataStream, parameters.filterName());
        }
    }

    QWidget* visibleMainWindow()
    {
        for (QWidget* w : QApplication::topLevelWidgets()) {

            if ((dynamic_cast<QMainWindow*>(w) != nullptr) && (w->isVisible())) {
                return w;
            }
        }
        return nullptr;
    }

#ifdef Q_OS_WIN
    void FetchDisplayProfileFromWindowHandle(HWND hwnd)
    {
        HMONITOR hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);

        MONITORINFOEXW monitorInfo{};
        monitorInfo.cbSize = sizeof(monitorInfo);

        if (GetMonitorInfoW(hMonitor, &monitorInfo))
        {
            HDC hdc = CreateDCW(monitorInfo.szDevice, monitorInfo.szDevice, nullptr, nullptr);

            if (hdc != nullptr)
            {
                DWORD size = 0;

                GetICMProfileA(hdc, &size, nullptr);

                if (size > 0)
                {
                    CHAR* chars = new (std::nothrow) CHAR[size];

                    if (chars != nullptr && GetICMProfileA(hdc, &size, chars))
                    {
                        cmsHPROFILE profile = cmsOpenProfileFromFileTHR(host_8bf::lcmsContext, chars, "rb");

                        if (profile != nullptr)
                        {
                            if (host_8bf::displayProfile != nullptr)
                            {
                                cmsCloseProfile(host_8bf::displayProfile);
                            }

                            host_8bf::displayProfile = profile;
                        }
                    }

                    delete[] chars;
                }

                DeleteDC(hdc);
            }
        }
    }
#endif

    void FetchDisplayProfileFromQtWidget()
    {
#ifdef Q_OS_WIN
        QWidget* mainWindow = visibleMainWindow();

        if (mainWindow != nullptr)
        {
            WId mainWindowHandle = mainWindow->winId();

            if (mainWindowHandle != static_cast<quintptr>(0))
            {
                FetchDisplayProfileFromWindowHandle(reinterpret_cast<HWND>(mainWindowHandle));
            }
        }
#endif
    }
}

namespace GmicQtHost {

void getLayersExtent(int * width, int * height, GmicQt::InputMode mode)
{
    if (mode == GmicQt::InputMode::NoInput)
    {
        *width = 0;
        *height = 0;
        return;
    }

    if (host_8bf::layers.size() == 1)
    {
        const Gmic8bfLayer& layer = host_8bf::layers[0];

        *width = layer.width;
        *height = layer.height;
    }
    else
    {
        QVector<Gmic8bfLayer> filteredLayers = FilterLayersForInputMode(mode);

        for (int i = 0; i < filteredLayers.size(); i++)
        {
            Gmic8bfLayer& layer = filteredLayers[i];

            *width = std::max(*width, layer.width);
            *height = std::max(*height, layer.height);
        }
    }
}

void getCroppedImages(gmic_list<float> & images, gmic_list<char> & imageNames, double x, double y, double width, double height, GmicQt::InputMode mode)
{
    if (mode == GmicQt::InputMode::NoInput)
    {
        images.assign();
        imageNames.assign();
        return;
    }

    const bool entireImage = x < 0 && y < 0 && width < 0 && height < 0;
    if (entireImage)
    {
        x = 0.0;
        y = 0.0;
        width = 1.0;
        height = 1.0;
    }

    QVector<Gmic8bfLayer> filteredLayers = FilterLayersForInputMode(mode);

    const int layerCount = filteredLayers.size();

    images.assign(layerCount);
    imageNames.assign(layerCount);

    for (int i = 0; i < layerCount; ++i)
    {
        QByteArray layerNameBytes = filteredLayers[i].name.toUtf8();
        gmic_image<char>::string(layerNameBytes.constData()).move_to(imageNames[i]);
    }

    int maxWidth = 0;
    int maxHeight = 0;

    for (int i = 0; i < filteredLayers.size(); i++)
    {
        Gmic8bfLayer& layer = filteredLayers[i];

        maxWidth = std::max(maxWidth, layer.width);
        maxHeight = std::max(maxHeight, layer.height);
    }

    const int ix = entireImage ? 0 : static_cast<int>(std::floor(x * maxWidth));
    const int iy = entireImage ? 0 : static_cast<int>(std::floor(y * maxHeight));
    const int iw = entireImage ? maxWidth : std::min(maxWidth - ix, static_cast<int>(1 + std::ceil(width * maxWidth)));
    const int ih = entireImage ? maxHeight : std::min(maxHeight - iy, static_cast<int>(1 + std::ceil(height * maxHeight)));

    for (int i = 0; i < layerCount; i++)
    {
        if (entireImage)
        {
            images[i].assign(filteredLayers.at(i).imageData);
        }
        else
        {
            images[i].assign(filteredLayers.at(i).imageData.get_crop(ix, iy, ix + iw, iy + ih));
        }
    }
}

void outputImages(gmic_list<float> & images, const gmic_list<char> & imageNames, GmicQt::OutputMode /* mode */)
{
    unused(imageNames);

    if (images.size() > 0)
    {
        // Remove any files that may be present from the last time the user clicked Apply.
        EmptyOutputFolder();

        QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss");
        bool haveMultipleImages = images.size() > 1;

        for (size_t i = 0; i < images.size(); ++i)
        {
            QString outputPath;

            if (haveMultipleImages)
            {
                outputPath = QString("%1/%2-%3.g8i").arg(host_8bf::outputDir).arg(timestamp).arg(i);
            }
            else
            {
                outputPath = QString("%1/%2.g8i").arg(host_8bf::outputDir).arg(timestamp);
            }

            gmic_library::gmic_image<float>& in = images[i];

            const int width = in.width();
            const int height = in.height();
            bool planar = false;
            int tileWidth = width;
            int tileHeight = height;

            if (host_8bf::grayScale && (in.spectrum() == 3 || in.spectrum() == 4))
            {
                // Convert the RGB image to grayscale.
                GmicQt::calibrateImage(in, in.spectrum() == 4 ? 2 : 1, false);
            }

            if (i == 0)
            {
                // Replace the active layer image with the first output image.
                // This allows users to "layer" multiple effects using the G'MIC-Qt Apply button.
                //
                // Note that only the most recently applied effect will be used by the "Last Filter"
                // or "Repeat Filter" commands.

                Gmic8bfLayer& active = host_8bf::layers[host_8bf::activeLayerIndex];

                active.width = width;
                active.height = height;
                active.imageData.assign(in);

                // If the G'MIC output is a single image that matches the host document size it will be
                // copied to the active layer when G'MIC exits.
                if (images.size() == 1 && width == host_8bf::documentWidth && height == host_8bf::documentHeight)
                {
                    // The output will be written as a tiled planar image because that is the most
                    // efficient format for the host to read.
                    planar = true;
                    tileWidth = host_8bf::hostTileWidth;
                    tileHeight = host_8bf::hostTileHeight;
                }
            }


            switch (host_8bf::bitsPerChannel)
            {
            case 8:
                WriteGmicOutput8(outputPath, in, planar, tileWidth, tileHeight);
                break;
            case 16:
                WriteGmicOutput16(outputPath, in, planar, tileWidth, tileHeight);
                break;
            case 32:
                WriteGmicOutput32(outputPath, in, planar, tileWidth, tileHeight);
                break;
            }
        }
    }
}

void applyColorProfile(gmic_library::gmic_image<gmic_pixel_type> & image)
{
    if (!image || image.spectrum() > 4)
    {
        return;
    }

    const bool performColorCorrection = host_8bf::lcmsContext != nullptr && host_8bf::imageProfile != nullptr && host_8bf::displayProfile != nullptr;

    if (host_8bf::grayScale)
    {
        if (image.spectrum() == 3 || image.spectrum() == 4)
        {
            // Convert the RGB image to gray scale.
            GmicQt::calibrateImage(image, image.spectrum() == 4 ? 2 : 1, false);
        }
    }
    else
    {
        if (performColorCorrection && (image.spectrum() == 1 || image.spectrum() == 2))
        {
            // Convert the gray scale image to RGB.
            // The color profile of a RGB image may not support gray scale image data.
            GmicQt::calibrateImage(image, image.spectrum() == 2 ? 4 : 3, false);
        }
    }

    if (!performColorCorrection)
    {
        return;
    }

    if (!host_8bf::fetchedDisplayProfileFromQtWidget)
    {
        host_8bf::fetchedDisplayProfileFromQtWidget = true;

        FetchDisplayProfileFromQtWidget();
    }

    gmic_library::gmic_image<gmic_pixel_type> corrected;
    image.get_permute_axes("cxyz").move_to(corrected) /= 255;

#ifndef TYPE_GRAYA_FLT
#define TYPE_GRAYA_FLT FLOAT_SH(1)|COLORSPACE_SH(PT_GRAY)|EXTRA_SH(1)|CHANNELS_SH(1)|BYTES_SH(4)
#endif

    cmsUInt32Number format = 0;
    cmsUInt32Number transformFlags = cmsFLAGS_BLACKPOINTCOMPENSATION;

    switch (image.spectrum())
    {
    case 1:
        format = TYPE_GRAY_FLT;
        break;
    case 2:
        format = TYPE_GRAYA_FLT;
        transformFlags |= cmsFLAGS_COPY_ALPHA;
        break;
    case 3:
        format = TYPE_RGB_FLT;
        break;
    case 4:
        format = TYPE_RGBA_FLT;
        transformFlags |= cmsFLAGS_COPY_ALPHA;
        break;
    }

    if (format != 0)
    {
        if (format != host_8bf::transformFormat)
        {
            host_8bf::transformFormat = format;

            if (host_8bf::transform != nullptr)
            {
                cmsDeleteTransform(host_8bf::transform);
            }

            host_8bf::transform = cmsCreateTransformTHR(
                host_8bf::lcmsContext,
                host_8bf::imageProfile,
                format,
                host_8bf::displayProfile,
                format,
                INTENT_RELATIVE_COLORIMETRIC,
                transformFlags);
        }

        if (host_8bf::transform != nullptr)
        {
            const cmsUInt64Number bytesPerLine64 = static_cast<cmsUInt64Number>(image.width()) * image.spectrum() * sizeof(gmic_pixel_type);

            if (bytesPerLine64 <= std::numeric_limits<cmsUInt32Number>::max())
            {
                const cmsUInt64Number bytesPerLine = static_cast<cmsUInt32Number>(bytesPerLine64);

                cmsDoTransformLineStride(
                    host_8bf::transform,
                    corrected.data(),
                    corrected.data(),
                    image.width(),
                    image.height(),
                    bytesPerLine,
                    bytesPerLine,
                    0,
                    0);
            }
        }
    }

    (corrected.permute_axes("yzcx") *= 255).cut(0, 255).move_to(image);
}

void showMessage(const char * message)
{
    unused(message);
}


} // GmicQtHost

#if defined(_MSC_VER) && defined(_DEBUG)
#include <sstream>

// Adapted from https://stackoverflow.com/a/20387632
bool launchDebugger()
{
    // Get System directory, typically c:\windows\system32
    std::wstring systemDir(MAX_PATH + 1, '\0');
    UINT nChars = GetSystemDirectoryW(&systemDir[0], static_cast<UINT>(systemDir.length()));
    if (nChars == 0) return false; // failed to get system directory
    systemDir.resize(nChars);

    // Get process ID and create the command line
    DWORD pid = GetCurrentProcessId();
    std::wostringstream s;
    s << systemDir << L"\\vsjitdebugger.exe -p " << pid;
    std::wstring cmdLine = s.str();

    // Start debugger process
    STARTUPINFOW si;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);

    PROCESS_INFORMATION pi;
    ZeroMemory(&pi, sizeof(pi));

    if (!CreateProcessW(NULL, &cmdLine[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) return false;

    // Close debugger process handles to eliminate resource leak
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    // Wait for the debugger to attach
    while (!IsDebuggerPresent()) Sleep(100);

    // Stop execution so the debugger can take over
    DebugBreak();
    return true;
}
#endif // defined(_MSC_VER) && defined(_DEBUG)

void DestroyLCMSData()
{
    if (host_8bf::imageProfile != nullptr)
    {
        cmsCloseProfile(host_8bf::imageProfile);
    }

    if (host_8bf::displayProfile != nullptr)
    {
        cmsCloseProfile(host_8bf::displayProfile);
    }

    if (host_8bf::transform != nullptr)
    {
        cmsDeleteTransform(host_8bf::transform);
    }

    if (host_8bf::lcmsContext != nullptr)
    {
        cmsDeleteContext(host_8bf::lcmsContext);
    }
}

int main(int argc, char *argv[])
{
#if defined(_MSC_VER) && defined(_DEBUG)
    launchDebugger();
#endif

    QString indexFilePath;
    QString parametersFilePath;
    bool useLastParameters = false;

    if (argc >= 3)
    {
        indexFilePath = argv[1];
        host_8bf::outputDir = argv[2];
        if (argc >= 4)
        {
            parametersFilePath = argv[3];

            if (argc == 5)
            {
                useLastParameters = strcmp(argv[4], "reapply") == 0;
            }
        }
    }
    else
    {
        return 1;
    }

    if (indexFilePath.isEmpty())
    {
        return 2;
    }

    if (host_8bf::outputDir.isEmpty())
    {
        return 3;
    }

    try
    {
        host_8bf::lcmsContext = nullptr;
        host_8bf::imageProfile = nullptr;
        host_8bf::displayProfile = nullptr;
        host_8bf::fetchedDisplayProfileFromQtWidget = false;
        host_8bf::transform = nullptr;
        host_8bf::transformFormat = 0;

        InputFileParseStatus status = ParseInputFileIndex(indexFilePath);

        // The return value 5 is skipped because it is already being used to
        // indicate that the user canceled the dialog.
        if (status != InputFileParseStatus::Ok)
        {
            DestroyLCMSData();

            switch (status)
            {
            case InputFileParseStatus::InvalidArgument:
               return 3;
            case InputFileParseStatus::FileOpenError:
                return 6;
            case InputFileParseStatus::BadFileSignature:
                return 7;
            case InputFileParseStatus::UnknownFileVersion:
                return 8;
            case InputFileParseStatus::OutOfMemory:
                return 9;
            case InputFileParseStatus::EndOfFile:
                return 10;
            case InputFileParseStatus::PlatformEndianMismatch:
                return 11;
            case InputFileParseStatus::FileReadError:
                return 12;
            case InputFileParseStatus::IccProfileError:
                return 13;
            default:
                return 4; // Unknown error
            }
        }
    }
    catch (const std::bad_alloc&)
    {
        DestroyLCMSData();
        return 9;
    }

    int exitCode = 0;
    std::list<GmicQt::InputMode> disabledInputModes;
    disabledInputModes.push_back(GmicQt::InputMode::NoInput);
    // disabledInputModes.push_back(GmicQt::InputMode::Active);
    // disabledInputModes.push_back(GmicQt::InputMode::All);
    // disabledInputModes.push_back(GmicQt::InputMode::ActiveAndBelow);
    // disabledInputModes.push_back(GmicQt::InputMode::ActiveAndAbove);
    // disabledInputModes.push_back(GmicQt::InputMode::AllVisible);
    // disabledInputModes.push_back(GmicQt::InputMode::AllInvisible);


    std::list<GmicQt::OutputMode> disabledOutputModes;
    // disabledOutputModes.push_back(GmicQt::OutputMode::InPlace);
    disabledOutputModes.push_back(GmicQt::OutputMode::NewImage);
    disabledOutputModes.push_back(GmicQt::OutputMode::NewLayers);
    disabledOutputModes.push_back(GmicQt::OutputMode::NewActiveLayers);
    bool dialogAccepted = true;

    GmicQt::RunParameters parameters = GetFilterRunParameters(parametersFilePath);

    if (useLastParameters)
    {
        exitCode = GmicQt::run(GmicQt::UserInterfaceMode::ProgressDialog,
                               parameters,
                               disabledInputModes,
                               disabledOutputModes,
                               &dialogAccepted);

        if (!dialogAccepted)
        {
            exitCode = 5;
        }
    }
    else
    {
        exitCode = GmicQt::run(GmicQt::UserInterfaceMode::Full,
                               parameters,
                               disabledInputModes,
                               disabledOutputModes,
                               &dialogAccepted);

        if (dialogAccepted)
        {
            GmicQt::RunParameters currentParameters = GmicQt::lastAppliedFilterRunParameters(GmicQt::ReturnedRunParametersFlag::AfterFilterExecution);

            WriteGmic8bfFilterParameters(parametersFilePath, currentParameters);
        }
        else
        {
            exitCode = 5;
        }
    }

    DestroyLCMSData();

    return exitCode;
}
