//    Copyright (C) 2019-2022 Jakub Melka
//
//    This file is part of PDF4QT.
//
//    PDF4QT is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Lesser General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    with the written consent of the copyright owner, any later version.
//
//    PDF4QT 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 Lesser General Public License for more details.
//
//    You should have received a copy of the GNU Lesser General Public License
//    along with PDF4QT.  If not, see <https://www.gnu.org/licenses/>.

#include "pdfblendfunction.h"
#include "pdfdbgheap.h"

#include <algorithm>

namespace pdf
{

constexpr const std::pair<const char*, BlendMode> BLEND_MODE_INFOS[] =
{
    { "Normal", BlendMode::Normal },
    { "Multiply", BlendMode::Multiply },
    { "Screen", BlendMode::Screen },
    { "Overlay", BlendMode::Overlay },
    { "Darken", BlendMode::Darken },
    { "Lighten", BlendMode::Lighten },
    { "ColorDodge", BlendMode::ColorDodge },
    { "ColorBurn", BlendMode::ColorBurn },
    { "HardLight", BlendMode::HardLight },
    { "SoftLight", BlendMode::SoftLight },
    { "Difference", BlendMode::Difference },
    { "Exclusion", BlendMode::Exclusion },
    { "Hue", BlendMode::Hue },
    { "Saturation", BlendMode::Saturation },
    { "Color", BlendMode::Color },
    { "Luminosity", BlendMode::Luminosity },
    { "Compatible", BlendMode::Compatible }
};

BlendMode PDFBlendModeInfo::getBlendMode(const QByteArray& name)
{
    for (const std::pair<const char*, BlendMode>& info : BLEND_MODE_INFOS)
    {
        if (info.first == name)
        {
            return info.second;
        }
    }

    return BlendMode::Invalid;
}

bool PDFBlendModeInfo::isSupportedByQt(BlendMode mode)
{
    switch (mode)
    {
       case BlendMode::Normal:
       case BlendMode::Multiply:
       case BlendMode::Screen:
       case BlendMode::Overlay:
       case BlendMode::Darken:
       case BlendMode::Lighten:
       case BlendMode::ColorDodge:
       case BlendMode::ColorBurn:
       case BlendMode::HardLight:
       case BlendMode::SoftLight:
       case BlendMode::Difference:
       case BlendMode::Exclusion:
       case BlendMode::Compatible:
            return true;

        default:
            return false;
    }
}

bool PDFBlendModeInfo::isSeparable(BlendMode mode)
{
    switch (mode)
    {
        case BlendMode::Normal:
        case BlendMode::Multiply:
        case BlendMode::Screen:
        case BlendMode::Overlay:
        case BlendMode::Darken:
        case BlendMode::Lighten:
        case BlendMode::ColorDodge:
        case BlendMode::ColorBurn:
        case BlendMode::HardLight:
        case BlendMode::SoftLight:
        case BlendMode::Difference:
        case BlendMode::Exclusion:
        case BlendMode::Compatible:
        case BlendMode::Overprint_SelectBackdrop:
        case BlendMode::Overprint_SelectNonZeroSourceOrBackdrop:
        case BlendMode::Overprint_SelectNonOneSourceOrBackdrop:
            return true;

        case BlendMode::Hue:
        case BlendMode::Saturation:
        case BlendMode::Color:
        case BlendMode::Luminosity:
            return false;

        default:
            Q_ASSERT(false);
            return false;
    }
}

bool PDFBlendModeInfo::isWhitePreserving(BlendMode mode)
{
    if (!isSeparable(mode))
    {
        return false;
    }

    if (mode == BlendMode::Difference || mode == BlendMode::Exclusion)
    {
        return false;
    }

    return true;
}

QPainter::CompositionMode PDFBlendModeInfo::getCompositionModeFromBlendMode(BlendMode mode)
{
    switch (mode)
    {
        case BlendMode::Normal:
            return QPainter::CompositionMode_SourceOver;
        case BlendMode::Multiply:
            return QPainter::CompositionMode_Multiply;
        case BlendMode::Screen:
            return QPainter::CompositionMode_Screen;
        case BlendMode::Overlay:
            return QPainter::CompositionMode_Overlay;
        case BlendMode::Darken:
            return QPainter::CompositionMode_Darken;
        case BlendMode::Lighten:
            return QPainter::CompositionMode_Lighten;
        case BlendMode::ColorDodge:
            return QPainter::CompositionMode_ColorDodge;
        case BlendMode::ColorBurn:
            return QPainter::CompositionMode_ColorBurn;
        case BlendMode::HardLight:
            return QPainter::CompositionMode_HardLight;
        case BlendMode::SoftLight:
            return QPainter::CompositionMode_SoftLight;
        case BlendMode::Difference:
            return QPainter::CompositionMode_Difference;
        case BlendMode::Exclusion:
            return QPainter::CompositionMode_Exclusion;
        case BlendMode::Compatible:
            return QPainter::CompositionMode_SourceOver;

        default:
            break;
    }

    return QPainter::CompositionMode_SourceOver;
}

QString PDFBlendModeInfo::getBlendModeName(BlendMode mode)
{
    for (const std::pair<const char*, BlendMode>& info : BLEND_MODE_INFOS)
    {
        if (info.second == mode)
        {
            return QString::fromLatin1(info.first);
        }
    }

    return "Unknown";
}

QString PDFBlendModeInfo::getBlendModeTranslatedName(BlendMode mode)
{
    switch (mode)
    {
        case BlendMode::Normal:
        case BlendMode::Compatible:
            return PDFTranslationContext::tr("Normal");
        case BlendMode::Multiply:
            return PDFTranslationContext::tr("Multiply");
        case BlendMode::Screen:
            return PDFTranslationContext::tr("Screen");
        case BlendMode::Overlay:
            return PDFTranslationContext::tr("Overlay");
        case BlendMode::Darken:
            return PDFTranslationContext::tr("Darken");
        case BlendMode::Lighten:
            return PDFTranslationContext::tr("Lighten");
        case BlendMode::ColorDodge:
            return PDFTranslationContext::tr("ColorDodge");
        case BlendMode::ColorBurn:
            return PDFTranslationContext::tr("ColorBurn");
        case BlendMode::HardLight:
            return PDFTranslationContext::tr("HardLight");
        case BlendMode::SoftLight:
            return PDFTranslationContext::tr("SoftLight");
        case BlendMode::Difference:
            return PDFTranslationContext::tr("Difference");
        case BlendMode::Exclusion:
            return PDFTranslationContext::tr("Exclusion");
        case BlendMode::Hue:
            return PDFTranslationContext::tr("Hue");
        case BlendMode::Saturation:
            return PDFTranslationContext::tr("Saturation");
        case BlendMode::Color:
            return PDFTranslationContext::tr("Color");
        case BlendMode::Luminosity:
            return PDFTranslationContext::tr("Luminosity");

        default:
            break;
    }

    return PDFTranslationContext::tr("Unknown");
}

std::vector<BlendMode> PDFBlendModeInfo::getBlendModes()
{
    return
    {
        BlendMode::Normal,
        BlendMode::Multiply,
        BlendMode::Screen,
        BlendMode::Overlay,
        BlendMode::Darken,
        BlendMode::Lighten,
        BlendMode::ColorDodge,
        BlendMode::ColorBurn,
        BlendMode::HardLight,
        BlendMode::SoftLight,
        BlendMode::Difference,
        BlendMode::Exclusion,
        BlendMode::Hue,
        BlendMode::Saturation,
        BlendMode::Color,
        BlendMode::Luminosity
    };
}

PDFColorComponent PDFBlendFunction::blend(BlendMode mode, PDFColorComponent Cb, PDFColorComponent Cs)
{
    switch (mode)
    {
        case BlendMode::Normal:
        case BlendMode::Compatible:
            return Cs;

        case BlendMode::Multiply:
            return Cb * Cs;

        case BlendMode::Screen:
            return Cb + Cs - Cb * Cs;

        case BlendMode::Overlay:
            return blend(BlendMode::HardLight, Cs, Cb);

        case BlendMode::Darken:
            return qMin(Cb, Cs);

        case BlendMode::Lighten:
            return qMax(Cb, Cs);

        case BlendMode::ColorDodge:
        {
            if (qFuzzyIsNull(Cb))
            {
                return 0.0f;
            }

            const PDFColorComponent CsInverted = 1.0f - Cs;
            if (Cb >= CsInverted)
            {
                return 1.0f;
            }

            return Cb / CsInverted;
        }

        case BlendMode::ColorBurn:
        {
            const PDFColorComponent CbInverted = 1.0f - Cb;
            if (qFuzzyIsNull(CbInverted))
            {
                return 1.0f;
            }

            if (CbInverted >= Cs)
            {
                return 0.0f;
            }

            return 1.0f - CbInverted / Cs;
        }

        case BlendMode::HardLight:
        {
            if (Cs <= 0.5f)
            {
                return blend(BlendMode::Multiply, Cb, 2.0f * Cs);
            }
            else
            {
                return blend(BlendMode::Screen, Cb, 2.0f * Cs - 1.0f);
            }
        }

        case BlendMode::SoftLight:
        {
            if (Cs <= 0.5f)
            {
                return Cb - (1.0f - 2.0f * Cs) * Cb * (1.0f - Cb);
            }
            else
            {
                PDFColorComponent D = 0.0f;
                if (Cb <= 0.25)
                {
                    D = ((16.0f * Cb - 12.0f) * Cb + 4.0f) * Cb;
                }
                else
                {
                    D = std::sqrt(Cb);
                }
                return Cb + (2.0f * Cs - 1.0f) * (D - Cb);
            }
        }

        case BlendMode::Difference:
            return qAbs(Cb - Cs);

        case BlendMode::Exclusion:
            return Cb + Cs - 2.0f * Cb * Cs;

        case BlendMode::Overprint_SelectBackdrop:
            return Cb;

        case BlendMode::Overprint_SelectNonZeroSourceOrBackdrop:
        {
            if (qFuzzyIsNull(Cs))
            {
                return Cb;
            }

            return Cs;
        }

        case BlendMode::Overprint_SelectNonOneSourceOrBackdrop:
        {
            if (qFuzzyIsNull(1.0f - Cs))
            {
                return Cb;
            }

            return Cs;
        }

        default:
        {
            Q_ASSERT(false);
            break;
        }
    }

    return Cs;
}

PDFRGB PDFBlendFunction::blend_Hue(PDFRGB Cb, PDFRGB Cs)
{
    return nonseparable_SetLum(nonseparable_SetSat(Cs, nonseparable_Sat(Cb)), nonseparable_Lum(Cb));
}

PDFRGB PDFBlendFunction::blend_Saturation(PDFRGB Cb, PDFRGB Cs)
{
    return nonseparable_SetLum(nonseparable_SetSat(Cb, nonseparable_Sat(Cs)), nonseparable_Lum(Cb));
}

PDFRGB PDFBlendFunction::blend_Color(PDFRGB Cb, PDFRGB Cs)
{
    return nonseparable_SetLum(Cs, nonseparable_Lum(Cb));
}

PDFRGB PDFBlendFunction::blend_Luminosity(PDFRGB Cb, PDFRGB Cs)
{
    return nonseparable_SetLum(Cb, nonseparable_Lum(Cs));
}

PDFGray PDFBlendFunction::blend_Hue(PDFGray Cb, PDFGray Cs)
{
    return nonseparable_rgb2gray(blend_Hue(nonseparable_gray2rgb(Cb), nonseparable_gray2rgb(Cs)));
}

PDFGray PDFBlendFunction::blend_Saturation(PDFGray Cb, PDFGray Cs)
{
    return nonseparable_rgb2gray(blend_Saturation(nonseparable_gray2rgb(Cb), nonseparable_gray2rgb(Cs)));
}

PDFGray PDFBlendFunction::blend_Color(PDFGray Cb, PDFGray Cs)
{
    return nonseparable_rgb2gray(blend_Color(nonseparable_gray2rgb(Cb), nonseparable_gray2rgb(Cs)));
}

PDFGray PDFBlendFunction::blend_Luminosity(PDFGray Cb, PDFGray Cs)
{
    return nonseparable_rgb2gray(blend_Luminosity(nonseparable_gray2rgb(Cb), nonseparable_gray2rgb(Cs)));
}

PDFCMYK PDFBlendFunction::blend_Hue(PDFCMYK Cb, PDFCMYK Cs)
{
    return nonseparable_rgb2cmyk(blend_Hue(nonseparable_cmyk2rgb(Cb), nonseparable_cmyk2rgb(Cs)), Cb[3]);
}

PDFCMYK PDFBlendFunction::blend_Saturation(PDFCMYK Cb, PDFCMYK Cs)
{
    return nonseparable_rgb2cmyk(blend_Saturation(nonseparable_cmyk2rgb(Cb), nonseparable_cmyk2rgb(Cs)), Cb[3]);
}

PDFCMYK PDFBlendFunction::blend_Color(PDFCMYK Cb, PDFCMYK Cs)
{
    return nonseparable_rgb2cmyk(blend_Color(nonseparable_cmyk2rgb(Cb), nonseparable_cmyk2rgb(Cs)), Cb[3]);
}

PDFCMYK PDFBlendFunction::blend_Luminosity(PDFCMYK Cb, PDFCMYK Cs)
{
    return nonseparable_rgb2cmyk(blend_Luminosity(nonseparable_cmyk2rgb(Cb), nonseparable_cmyk2rgb(Cs)), Cs[3]);
}

PDFGray PDFBlendFunction::blend_Nonseparable(BlendMode mode, PDFGray Cb, PDFGray Cs)
{
    switch (mode)
    {
        case BlendMode::Hue:
            return blend_Hue(Cb, Cs);

        case BlendMode::Saturation:
            return blend_Saturation(Cb, Cs);

        case BlendMode::Color:
            return blend_Color(Cb, Cs);

        case BlendMode::Luminosity:
            return blend_Luminosity(Cb, Cs);

        default:
            Q_ASSERT(false);
            break;
    }

    return Cs;
}

PDFRGB PDFBlendFunction::blend_Nonseparable(BlendMode mode, PDFRGB Cb, PDFRGB Cs)
{
    switch (mode)
    {
        case BlendMode::Hue:
            return blend_Hue(Cb, Cs);

        case BlendMode::Saturation:
            return blend_Saturation(Cb, Cs);

        case BlendMode::Color:
            return blend_Color(Cb, Cs);

        case BlendMode::Luminosity:
            return blend_Luminosity(Cb, Cs);

        default:
            Q_ASSERT(false);
            break;
    }

    return Cs;
}

PDFCMYK PDFBlendFunction::blend_Nonseparable(BlendMode mode, PDFCMYK Cb, PDFCMYK Cs)
{
    switch (mode)
    {
        case BlendMode::Hue:
            return blend_Hue(Cb, Cs);

        case BlendMode::Saturation:
            return blend_Saturation(Cb, Cs);

        case BlendMode::Color:
            return blend_Color(Cb, Cs);

        case BlendMode::Luminosity:
            return blend_Luminosity(Cb, Cs);

        default:
            Q_ASSERT(false);
            break;
    }

    return Cs;
}

PDFColorComponent PDFBlendFunction::getLuminosity(PDFGray gray)
{
    return nonseparable_Lum(nonseparable_gray2rgb(gray));
}

PDFColorComponent PDFBlendFunction::getLuminosity(PDFRGB rgb)
{
    return nonseparable_Lum(rgb);
}

PDFColorComponent PDFBlendFunction::getLuminosity(PDFCMYK cmyk)
{
    // This is according to chapter 11.5.3, deriving soft mask from a group luminosity
    return 1.0f - qMin(1.0f, 0.30f * cmyk[0] + 0.59f * cmyk[1] + 0.11f * cmyk[2] + cmyk[3]);
}

PDFRGB PDFBlendFunction::nonseparable_gray2rgb(PDFGray gray)
{
    return nonseparable_SetLum(PDFRGB{ 0.0f, 0.0f, 0.0f }, gray);
}

PDFGray PDFBlendFunction::nonseparable_rgb2gray(PDFRGB rgb)
{
    // Just convert to luminosity
    return nonseparable_Lum(rgb);
}

PDFRGB PDFBlendFunction::nonseparable_cmyk2rgb(PDFCMYK cmyk)
{
    return PDFRGB{ 1.0f - cmyk[0], 1.0f - cmyk[1], 1.0f - cmyk[2] };
}

PDFCMYK PDFBlendFunction::nonseparable_rgb2cmyk(PDFRGB rgb, PDFColorComponent K)
{
    return PDFCMYK{ 1.0f - rgb[0], 1.0f - rgb[1], 1.0f - rgb[2], K };
}

PDFColorComponent PDFBlendFunction::nonseparable_Lum(PDFRGB rgb)
{
    return 0.30 * rgb[0] + 0.59 * rgb[1] + 0.11 * rgb[2];
}

PDFColorComponent PDFBlendFunction::nonseparable_Sat(PDFRGB rgb)
{
    const PDFColorComponent min = *std::min_element(rgb.cbegin(), rgb.cend());
    const PDFColorComponent max = *std::max_element(rgb.cbegin(), rgb.cend());
    return max - min;
}

PDFRGB PDFBlendFunction::nonseparable_SetLum(PDFRGB C, PDFColorComponent l)
{
    const PDFColorComponent d = l - nonseparable_Lum(C);
    PDFRGB result = C;
    result[0] += d;
    result[1] += d;
    result[2] += d;
    return nonseparable_ClipColor(result);
}

PDFRGB PDFBlendFunction::nonseparable_SetSat(PDFRGB C, PDFColorComponent s)
{
    auto it_min = std::min_element(C.begin(), C.end());
    auto it_max = std::max_element(C.begin(), C.end());
    auto it_mid = C.end();
    for (auto it = C.begin(); it != C.end(); ++it)
    {
        if (it != it_min && it != it_max)
        {
            it_mid = it;
            break;
        }
    }
    Q_ASSERT(it_mid != C.end());

    PDFRGB result = C;
    if (*it_max > *it_min)
    {
        *it_mid = (*it_mid - *it_min) * s / (*it_max - *it_min);
        *it_max = s;
        result = C;
    }
    else
    {
        std::fill(result.begin(), result.end(), 0.0f);
    }

    return result;
}

PDFRGB PDFBlendFunction::nonseparable_ClipColor(PDFRGB C)
{
    PDFRGB result = C;
    const PDFColorComponent l = nonseparable_Lum(C);
    const PDFColorComponent n = *std::min_element(C.cbegin(), C.cend());
    const PDFColorComponent x = *std::max_element(C.cbegin(), C.cend());

    if (n < 0.0f)
    {
        const PDFColorComponent factor = 1.0f / (l - n);
        result[0] = l + (result[0] - l) * l * factor;
        result[1] = l + (result[1] - l) * l * factor;
        result[2] = l + (result[2] - l) * l * factor;
    }

    if (x > 1.0f)
    {
        const PDFColorComponent factor = 1.0f / (x - l);
        result[0] = l + (result[0] - l) * (1.0f - l) * factor;
        result[1] = l + (result[1] - l) * (1.0f - l) * factor;
        result[2] = l + (result[2] - l) * (1.0f - l) * factor;
    }

    return result;
}

}   // namespace pdf