/*****************************************************************************
 *
 * This file is part of Mapnik (c++ mapping toolkit)
 *
 * Copyright (C) 2025 Artem Pavlenko
 *
 * This library 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 2.1 of the License, or (at your option) any later version.
 *
 * This library 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 this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *****************************************************************************/

#ifndef MAPNIK_IMAGE_FILTER_HPP
#define MAPNIK_IMAGE_FILTER_HPP

// mapnik
#include <mapnik/image_filter_types.hpp>
#include <mapnik/image_util.hpp>
#include <mapnik/util/hsl.hpp>
#include <mapnik/warning.hpp>
MAPNIK_DISABLE_WARNING_PUSH
#include <mapnik/warning_ignore.hpp>
#if BOOST_VERSION >= 106900
#include <boost/gil.hpp>
#else
#include <boost/gil/gil_all.hpp>
#endif
MAPNIK_DISABLE_WARNING_POP

#include <mapnik/warning.hpp>
MAPNIK_DISABLE_WARNING_PUSH
#include <mapnik/warning_ignore_agg.hpp>
#include "agg_basics.h"
#include "agg_rendering_buffer.h"
#include "agg_color_rgba.h"
#include "agg_pixfmt_rgba.h"
#include "agg_scanline_u.h"
#include "agg_blur.h"
#include "agg_gradient_lut.h"
MAPNIK_DISABLE_WARNING_POP

// stl
#include <cmath>

#if BOOST_VERSION >= 106800
namespace boost {
namespace gil {
using bits32f = boost::gil::float32_t;
}
} // namespace boost
#endif

// 8-bit YUV
// Y = ( (  66 * R + 129 * G +  25 * B + 128) >> 8) +  16
// U = ( ( -38 * R -  74 * G + 112 * B + 128) >> 8) + 128
// V = ( ( 112 * R -  94 * G -  18 * B + 128) >> 8) + 128

// bits_type x_gradient = (0.125f*c2 + 0.25f*c5 + 0.125f*c8)
//     - (0.125f*c0 + 0.25f*c3 + 0.125f*c6);
// bits_type y_gradient = (0.125f*c0 + 0.25f*c1 + 0.125f*c2)
//     - (0.125f*c6 + 0.25f*c7 + 0.125f*c8);

// c0 c1 c2
// c3 c4 c5
// c6 c7 c8

// sharpen
//   0 -1  0
//  -1  5 -1
//   0 -1  0
// bits_type out_value = -c1 - c3 + 5.0*c4 - c5 - c7;

// edge detect
//  0  1  0
//  1 -4  1
//  0  1  0
// bits_type out_value = c1 + c3 - 4.0*c4 + c5 + c7;

//
// if (out_value < 0) out_value = 0;
// if (out_value > 255) out_value = 255;

// emboss
// -2 -1  0
// -1  1  1
//  0  1  2

// bits_type out_value = -2*c0 - c1 - c3 + c4 + c5 + c7 +  2*c8;

// blur
// float out_value = (0.1f*c0 + 0.1f*c1 + 0.1f*c2 +
//                   0.1f*c3 + 0.1f*c4 + 0.1f*c5 +
//                  0.1f*c6 + 0.1f*c7 + 0.1f*c8);

// float out_value  = std::sqrt(std::pow(x_gradient,2) + std::pow(y_gradient,2));
// float theta = std::atan2(x_gradient,y_gradient);
// if (out_value < 0.0) out_value = 0.0;
// if (out_value < 1.0) out_value = 1.0;

// float conv_matrix[]={1/3.0,1/3.0,1/3.0};

// float gaussian_1[]={0.00022923296f,0.0059770769f,0.060597949f,0.24173197f,0.38292751f,
//                     0.24173197f,0.060597949f,0.0059770769f,0.00022923296f};

// float gaussian_2[]={
//     0.00048869418f,0.0024031631f,0.0092463447f,
//    0.027839607f,0.065602221f,0.12099898f,0.17469721f,
//    0.19744757f,
//    0.17469721f,0.12099898f,0.065602221f,0.027839607f,
//    0.0092463447f,0.0024031631f,0.00048869418f
// };

//  kernel_1d_fixed<float,9> kernel(conv,4);

// color_converted_view<rgb8_pixel_t>(src_view);
// using view_t = kth_channel_view_type< 0, const rgba8_view_t>::type;

// view_t red = kth_channel_view<0>(const_view(src_view));

// kernel_1d_fixed<float,3> kernel(sharpen,0);
// convolve_rows_fixed<rgba32f_pixel_t>(src_view,kernel,src_view);
//  convolve_cols_fixed<rgba32f_pixel_t>(src_view,kernel,dst_view);

namespace mapnik {
namespace filter {
namespace detail {

static float const blur_matrix[] = {0.1111f, 0.1111f, 0.1111f, 0.1111f, 0.1111f, 0.1111f, 0.1111f, 0.1111f, 0.1111f};
static float const emboss_matrix[] = {-2, -1, 0, -1, 1, 1, 0, 1, 2};
static float const sharpen_matrix[] = {0, -1, 0, -1, 5, -1, 0, -1, 0};
static float const edge_detect_matrix[] = {0, 1, 0, 1, -4, 1, 0, 1, 0};

} // namespace detail

using boost::gil::rgba8_image_t;
using boost::gil::rgba8_view_t;

template<typename Image>
boost::gil::rgba8_view_t rgba8_view(Image& img)
{
    using boost::gil::interleaved_view;
    using boost::gil::rgba8_pixel_t;
    return interleaved_view(img.width(),
                            img.height(),
                            reinterpret_cast<rgba8_pixel_t*>(img.bytes()),
                            img.width() * sizeof(rgba8_pixel_t));
}

template<typename Image>
struct double_buffer
{
    boost::gil::rgba8_image_t dst_buffer;
    boost::gil::rgba8_view_t dst_view;
    boost::gil::rgba8_view_t src_view;

    explicit double_buffer(Image& src)
        : dst_buffer(src.width(), src.height()),
          dst_view(view(dst_buffer)),
          src_view(rgba8_view(src))
    {}

    ~double_buffer() { copy_pixels(dst_view, src_view); }
};

template<typename Src, typename Dst, typename Conv>
void process_channel_impl(Src const& src, Dst& dst, Conv const& k)
{
    using boost::gil::bits32f;

    bits32f out_value = k[0] * src[0] + k[1] * src[1] + k[2] * src[2] + k[3] * src[3] + k[4] * src[4] + k[5] * src[5] +
                        k[6] * src[6] + k[7] * src[7] + k[8] * src[8];
    if (out_value < 0)
        out_value = 0;
    if (out_value > 255)
        out_value = 255;
    dst = out_value;
}

template<typename Src, typename Dst, typename Conv>
void process_channel(Src const&, Dst&, Conv const&)
{}

template<typename Src, typename Dst>
void process_channel(Src const& src, Dst& dst, mapnik::filter::blur)
{
    process_channel_impl(src, dst, mapnik::filter::detail::blur_matrix);
}

template<typename Src, typename Dst>
void process_channel(Src const& src, Dst& dst, mapnik::filter::emboss)
{
    process_channel_impl(src, dst, mapnik::filter::detail::emboss_matrix);
}

template<typename Src, typename Dst>
void process_channel(Src const& src, Dst& dst, mapnik::filter::sharpen)
{
    process_channel_impl(src, dst, mapnik::filter::detail::sharpen_matrix);
}

template<typename Src, typename Dst>
void process_channel(Src const& src, Dst& dst, mapnik::filter::edge_detect)
{
    process_channel_impl(src, dst, mapnik::filter::detail::edge_detect_matrix);
}

template<typename Src, typename Dst>
void process_channel(Src const& src, Dst& dst, mapnik::filter::sobel)
{
    using boost::gil::bits32f;

    bits32f x_gradient = (src[2] + 2 * src[5] + src[8]) - (src[0] + 2 * src[3] + src[6]);

    bits32f y_gradient = (src[0] + 2 * src[1] + src[2]) - (src[6] + 2 * src[7] + src[8]);

    bits32f out_value = std::sqrt(std::pow(x_gradient, 2) + std::pow(y_gradient, 2));
    // bts32f theta = std::atan2(x_gradient,y_gradient);
    if (out_value < 0)
        out_value = 0;
    if (out_value > 255)
        out_value = 255;
    dst = out_value;
}

template<typename Src, typename Dst, typename Filter>
void apply_convolution_3x3(Src const& src_view, Dst& dst_view, Filter const& filter)
{
    using boost::gil::bits32f;
    using boost::gil::point2;

    // p0 p1 p2
    // p3 p4 p5
    // p6 p7 p8

    typename Src::xy_locator src_loc = src_view.xy_at(0, 0);
    typename Src::xy_locator::cached_location_t loc00 = src_loc.cache_location(-1, -1);
    typename Src::xy_locator::cached_location_t loc10 = src_loc.cache_location(0, -1);
    typename Src::xy_locator::cached_location_t loc20 = src_loc.cache_location(1, -1);
    typename Src::xy_locator::cached_location_t loc01 = src_loc.cache_location(-1, 0);
    typename Src::xy_locator::cached_location_t loc11 = src_loc.cache_location(0, 0);
    typename Src::xy_locator::cached_location_t loc21 = src_loc.cache_location(1, 0);
    typename Src::xy_locator::cached_location_t loc02 = src_loc.cache_location(-1, 1);
    typename Src::xy_locator::cached_location_t loc12 = src_loc.cache_location(0, 1);
    typename Src::xy_locator::cached_location_t loc22 = src_loc.cache_location(1, 1);

    typename Src::x_iterator dst_it = dst_view.row_begin(0);

    // top row
    for (std::ptrdiff_t x = 0; x < src_view.width(); ++x)
    {
        (*dst_it)[3] = src_loc[loc11][3]; // Dst.a = Src.a
        for (std::ptrdiff_t i = 0; i < 3; ++i)
        {
            bits32f p[9];

            p[4] = src_loc[loc11][i];
            p[7] = src_loc[loc12][i];

            if (x == 0)
            {
                p[3] = p[4];
                p[6] = p[7];
            }
            else
            {
                p[3] = src_loc[loc01][i];
                p[6] = src_loc[loc02][i];
            }

            if (x == (src_view.width()) - 1)
            {
                p[5] = p[4];
                p[8] = p[7];
            }
            else
            {
                p[5] = src_loc[loc21][i];
                p[8] = src_loc[loc22][i];
            }

            p[0] = p[6];
            p[1] = p[7];
            p[2] = p[8];

            process_channel(p, (*dst_it)[i], filter);
        }
        ++src_loc.x();
        ++dst_it;
    }
    // carrige-return
    src_loc += point2<std::ptrdiff_t>(-src_view.width(), 1);

    // 1... height-1 rows
    for (std::ptrdiff_t y = 1; y < src_view.height() - 1; ++y)
    {
        for (std::ptrdiff_t x = 0; x < src_view.width(); ++x)
        {
            (*dst_it)[3] = src_loc[loc11][3]; // Dst.a = Src.a
            for (std::ptrdiff_t i = 0; i < 3; ++i)
            {
                bits32f p[9];

                p[1] = src_loc[loc10][i];
                p[4] = src_loc[loc11][i];
                p[7] = src_loc[loc12][i];

                if (x == 0)
                {
                    p[0] = p[1];
                    p[3] = p[4];
                    p[6] = p[7];
                }
                else
                {
                    p[0] = src_loc[loc00][i];
                    p[3] = src_loc[loc01][i];
                    p[6] = src_loc[loc02][i];
                }

                if (x == (src_view.width()) - 1)
                {
                    p[2] = p[1];
                    p[5] = p[4];
                    p[8] = p[7];
                }
                else
                {
                    p[2] = src_loc[loc20][i];
                    p[5] = src_loc[loc21][i];
                    p[8] = src_loc[loc22][i];
                }
                process_channel(p, (*dst_it)[i], filter);
            }
            ++dst_it;
            ++src_loc.x();
        }
        // carrige-return
        src_loc += point2<std::ptrdiff_t>(-src_view.width(), 1);
    }

    // bottom row
    // src_loc = src_view.xy_at(0,src_view.height()-1);
    for (std::ptrdiff_t x = 0; x < src_view.width(); ++x)
    {
        (*dst_it)[3] = src_loc[loc11][3]; // Dst.a = Src.a
        for (std::ptrdiff_t i = 0; i < 3; ++i)
        {
            bits32f p[9];

            p[1] = src_loc[loc10][i];
            p[4] = src_loc[loc11][i];

            if (x == 0)
            {
                p[0] = p[1];
                p[3] = p[4];
            }
            else
            {
                p[0] = src_loc[loc00][i];
                p[3] = src_loc[loc01][i];
            }

            if (x == (src_view.width()) - 1)
            {
                p[2] = p[1];
                p[5] = p[4];
            }
            else
            {
                p[2] = src_loc[loc20][i];
                p[5] = src_loc[loc21][i];
            }

            p[6] = p[0];
            p[7] = p[1];
            p[8] = p[2];
            process_channel(p, (*dst_it)[i], filter);
        }
        ++src_loc.x();
        ++dst_it;
    }
}

template<typename Src, typename Filter>
void apply_filter(Src& src, Filter const& filter, double /*scale_factor*/)
{
    demultiply_alpha(src);
    double_buffer<Src> tb(src);
    apply_convolution_3x3(tb.src_view, tb.dst_view, filter);
}

template<typename Src>
void apply_filter(Src& src, agg_stack_blur const& op, double scale_factor)
{
    premultiply_alpha(src);
    agg::rendering_buffer buf(src.bytes(), src.width(), src.height(), src.row_size());
    agg::pixfmt_rgba32_pre pixf(buf);
    agg::stack_blur_rgba32(pixf, op.rx * scale_factor, op.ry * scale_factor);
}

inline double channel_delta(double source, double match)
{
    if (source > match)
        return (source - match) / (1.0 - match);
    if (source < match)
        return (match - source) / match;
    return (source - match);
}

inline uint8_t apply_alpha_shift(double source, double match, double alpha)
{
    source = (((source - match) / alpha) + match) * alpha;
    return static_cast<uint8_t>(std::floor((source * 255.0) + .5));
}

template<typename Src>
void apply_filter(Src& src, color_to_alpha const& op, double /*scale_factor*/)
{
    using namespace boost::gil;
    bool premultiplied = src.get_premultiplied();
    rgba8_view_t src_view = rgba8_view(src);
    double cr = static_cast<double>(op.color.red()) / 255.0;
    double cg = static_cast<double>(op.color.green()) / 255.0;
    double cb = static_cast<double>(op.color.blue()) / 255.0;
    for (std::ptrdiff_t y = 0; y < src_view.height(); ++y)
    {
        rgba8_view_t::x_iterator src_it = src_view.row_begin(static_cast<long>(y));
        for (std::ptrdiff_t x = 0; x < src_view.width(); ++x)
        {
            uint8_t& r = get_color(src_it[x], red_t());
            uint8_t& g = get_color(src_it[x], green_t());
            uint8_t& b = get_color(src_it[x], blue_t());
            uint8_t& a = get_color(src_it[x], alpha_t());
            double sr = static_cast<double>(r) / 255.0;
            double sg = static_cast<double>(g) / 255.0;
            double sb = static_cast<double>(b) / 255.0;
            double sa = static_cast<double>(a) / 255.0;
            // demultiply
            if (sa <= 0.0)
            {
                r = g = b = 0;
                continue;
            }
            else if (premultiplied)
            {
                sr /= sa;
                sg /= sa;
                sb /= sa;
            }
            // get that maximum color difference
            double xa = std::max(channel_delta(sr, cr), std::max(channel_delta(sg, cg), channel_delta(sb, cb)));
            if (xa > 0)
            {
                // apply difference to each channel, returning premultiplied
                // TODO - experiment with difference in hsl color space
                r = apply_alpha_shift(sr, cr, xa);
                g = apply_alpha_shift(sg, cg, xa);
                b = apply_alpha_shift(sb, cb, xa);
                // combine new alpha with original
                xa *= sa;
                a = static_cast<uint8_t>(std::floor((xa * 255.0) + .5));
                // all color values must be <= alpha
                if (r > a)
                    r = a;
                if (g > a)
                    g = a;
                if (b > a)
                    b = a;
            }
            else
            {
                r = g = b = a = 0;
            }
        }
    }
    // set as premultiplied
    set_premultiplied_alpha(src, true);
}

template<typename Src>
void apply_filter(Src& src, colorize_alpha const& op, double /*scale_factor*/)
{
    using namespace boost::gil;
    std::ptrdiff_t size = op.size();
    if (op.size() == 1)
    {
        // no interpolation if only one stop
        mapnik::filter::color_stop const& stop = op[0];
        mapnik::color const& c = stop.color;
        rgba8_view_t src_view = rgba8_view(src);
        for (std::ptrdiff_t y = 0; y < src_view.height(); ++y)
        {
            rgba8_view_t::x_iterator src_it = src_view.row_begin(static_cast<long>(y));
            for (std::ptrdiff_t x = 0; x < src_view.width(); ++x)
            {
                uint8_t& r = get_color(src_it[x], red_t());
                uint8_t& g = get_color(src_it[x], green_t());
                uint8_t& b = get_color(src_it[x], blue_t());
                uint8_t& a = get_color(src_it[x], alpha_t());
                if (a > 0)
                {
                    a = (c.alpha() * a + 255) >> 8;
                    r = (c.red() * a + 255) >> 8;
                    g = (c.green() * a + 255) >> 8;
                    b = (c.blue() * a + 255) >> 8;
                }
            }
        }
        // set as premultiplied
        set_premultiplied_alpha(src, true);
    }
    else if (size > 1)
    {
        // interpolate multiple stops
        agg::gradient_lut<agg::color_interpolator<agg::rgba8>> grad_lut;
        double step = 1.0 / (size - 1);
        double offset = 0.0;
        for (mapnik::filter::color_stop const& stop : op)
        {
            mapnik::color const& c = stop.color;
            double stop_offset = stop.offset;
            if (stop_offset == 0)
            {
                stop_offset = offset;
            }
            grad_lut.add_color(stop_offset,
                               agg::rgba(c.red() / 255.0, c.green() / 255.0, c.blue() / 255.0, c.alpha() / 255.0));
            offset += step;
        }
        if (grad_lut.build_lut())
        {
            rgba8_view_t src_view = rgba8_view(src);
            for (std::ptrdiff_t y = 0; y < src_view.height(); ++y)
            {
                rgba8_view_t::x_iterator src_it = src_view.row_begin(static_cast<long>(y));
                for (std::ptrdiff_t x = 0; x < src_view.width(); ++x)
                {
                    uint8_t& r = get_color(src_it[x], red_t());
                    uint8_t& g = get_color(src_it[x], green_t());
                    uint8_t& b = get_color(src_it[x], blue_t());
                    uint8_t& a = get_color(src_it[x], alpha_t());
                    if (a > 0)
                    {
                        agg::rgba8 c = grad_lut[a];
                        a = (c.a * a + 255) >> 8;
                        r = (c.r * a + 255) >> 8;
                        g = (c.g * a + 255) >> 8;
                        b = (c.b * a + 255) >> 8;
#if 0
                        // rainbow
                        r = 0;
                        g = 0;
                        b = 0;
                        if (a < 64)
                        {
                            g = a * 4;
                            b = 255;
                        }
                        else if (a >= 64 && a < 128)
                        {
                            g = 255;
                            b = 255 - ((a - 64) * 4);
                        }
                        else if (a >= 128 && a < 192)
                        {
                            r = (a - 128) * 4;
                            g = 255;
                        }
                        else // >= 192
                        {
                            r = 255;
                            g = 255 - ((a - 192) * 4);
                        }
                        r = (r * a + 255) >> 8;
                        g = (g * a + 255) >> 8;
                        b = (b * a + 255) >> 8;
#endif
                    }
                }
            }
        }
        // set as premultiplied
        set_premultiplied_alpha(src, true);
    }
}

template<typename Src>
void apply_filter(Src& src, scale_hsla const& transform, double /*scale_factor*/)
{
    using namespace boost::gil;
    bool tinting = !transform.is_identity();
    bool set_alpha = !transform.is_alpha_identity();
    // todo - filters be able to report if they
    // should be run to avoid overhead of temp buffer
    if (tinting || set_alpha)
    {
        bool premultiplied = src.get_premultiplied();
        rgba8_view_t src_view = rgba8_view(src);
        for (std::ptrdiff_t y = 0; y < src_view.height(); ++y)
        {
            rgba8_view_t::x_iterator src_it = src_view.row_begin(static_cast<long>(y));
            for (std::ptrdiff_t x = 0; x < src_view.width(); ++x)
            {
                uint8_t& r = get_color(src_it[x], red_t());
                uint8_t& g = get_color(src_it[x], green_t());
                uint8_t& b = get_color(src_it[x], blue_t());
                uint8_t& a = get_color(src_it[x], alpha_t());
                double r2 = static_cast<double>(r) / 255.0;
                double g2 = static_cast<double>(g) / 255.0;
                double b2 = static_cast<double>(b) / 255.0;
                double a2 = static_cast<double>(a) / 255.0;
                // demultiply
                if (a2 <= 0.0)
                {
                    r = g = b = 0;
                    continue;
                }
                else if (premultiplied)
                {
                    r2 /= a2;
                    g2 /= a2;
                    b2 /= a2;
                }

                if (set_alpha)
                {
                    a2 = transform.a0 + (a2 * (transform.a1 - transform.a0));
                    if (a2 <= 0)
                    {
                        r = g = b = a = 0;
                        continue;
                    }
                    else if (a2 > 1)
                    {
                        a2 = 1;
                        a = 255;
                    }
                    else
                    {
                        a = static_cast<uint8_t>(std::floor((a2 * 255.0) + .5));
                    }
                }
                if (tinting)
                {
                    double h;
                    double s;
                    double l;
                    rgb2hsl(r2, g2, b2, h, s, l);
                    double h2 = transform.h0 + (h * (transform.h1 - transform.h0));
                    double s2 = transform.s0 + (s * (transform.s1 - transform.s0));
                    double l2 = transform.l0 + (l * (transform.l1 - transform.l0));
                    if (h2 > 1)
                    {
                        h2 = 1;
                    }
                    else if (h2 < 0)
                    {
                        h2 = 0;
                    }
                    if (s2 > 1)
                    {
                        s2 = 1;
                    }
                    else if (s2 < 0)
                    {
                        s2 = 0;
                    }
                    if (l2 > 1)
                    {
                        l2 = 1;
                    }
                    else if (l2 < 0)
                    {
                        l2 = 0;
                    }
                    hsl2rgb(h2, s2, l2, r2, g2, b2);
                }
                // premultiply
                r2 *= a2;
                g2 *= a2;
                b2 *= a2;
                r = static_cast<uint8_t>(std::floor((r2 * 255.0) + .5));
                g = static_cast<uint8_t>(std::floor((g2 * 255.0) + .5));
                b = static_cast<uint8_t>(std::floor((b2 * 255.0) + .5));
                // all color values must be <= alpha
                if (r > a)
                    r = a;
                if (g > a)
                    g = a;
                if (b > a)
                    b = a;
            }
        }
        // set as premultiplied
        set_premultiplied_alpha(src, true);
    }
}

template<typename Src, typename ColorBlindFilter>
void apply_color_blind_filter(Src& src, ColorBlindFilter const& op)
{
    using namespace boost::gil;
    rgba8_view_t src_view = rgba8_view(src);
    bool premultiplied = src.get_premultiplied();

    static constexpr double gamma = 2.2;
    static constexpr double inv_gamma = 1.0 / gamma;

    for (std::ptrdiff_t y = 0; y < src_view.height(); ++y)
    {
        rgba8_view_t::x_iterator src_it = src_view.row_begin(static_cast<long>(y));
        for (std::ptrdiff_t x = 0; x < src_view.width(); ++x)
        {
            uint8_t& r = get_color(src_it[x], red_t());
            uint8_t& g = get_color(src_it[x], green_t());
            uint8_t& b = get_color(src_it[x], blue_t());
            uint8_t& a = get_color(src_it[x], alpha_t());
            // demultiply
            if (a == 0)
            {
                r = g = b = 0;
                continue;
            }
            else if (premultiplied)
            {
                std::uint32_t cr = (r * 255) / a;
                std::uint32_t cg = (g * 255) / a;
                std::uint32_t cb = (b * 255) / a;
                r = static_cast<uint8_t>((cr > 255) ? 255 : cr);
                g = static_cast<uint8_t>((cg > 255) ? 255 : cg);
                b = static_cast<uint8_t>((cb > 255) ? 255 : cb);
            }
            // Convert source color into XYZ color space
            double pow_r = std::pow(r, gamma);
            double pow_g = std::pow(g, gamma);
            double pow_b = std::pow(b, gamma);
            double X = (0.412424 * pow_r) + (0.357579 * pow_g) + (0.180464 * pow_b);
            double Y = (0.212656 * pow_r) + (0.715158 * pow_g) + (0.0721856 * pow_b);
            double Z = (0.0193324 * pow_r) + (0.119193 * pow_g) + (0.950444 * pow_b);
            // Convert XYZ into xyY Chromacity Coordinates (xy) and Luminance (Y)
            double chroma_x = X / (X + Y + Z);
            double chroma_y = Y / (X + Y + Z);
            // Generate the "Confusion Line" between the source color and the Confusion Point
            double m_div = chroma_x - op.x;
            if (std::abs(m_div) < (std::numeric_limits<double>::epsilon()))
                continue;
            double m = (chroma_y - op.y) / (chroma_x - op.x); // slope of Confusion Line
            double yint = chroma_y - chroma_x * m;            // y-intercept of confusion line (x-intercept = 0.0)
            // How far the xy coords deviate from the simulation
            double m_div2 = m - op.m;
            if (std::abs(m_div2) < (std::numeric_limits<double>::epsilon()))
                continue;
            double deviate_x = (op.yint - yint) / (m - op.m);
            double deviate_y = (m * deviate_x) + yint;
            // Compute the simulated color's XYZ coords
            X = deviate_x * Y / deviate_y;
            Z = (1.0 - (deviate_x + deviate_y)) * Y / deviate_y;
            // Neutral grey calculated from luminance (in D65)
            double neutral_X = 0.312713 * Y / 0.329016;
            double neutral_Z = 0.358271 * Y / 0.329016;
            // Difference between simulated color and neutral grey
            double diff_X = neutral_X - X;
            double diff_Z = neutral_Z - Z;
            // XYZ->RGB (sRGB:D65)
            double diff_r = diff_X * 3.2407100 + diff_Z * -0.4985710;
            double diff_g = diff_X * -0.9692580 + diff_Z * 0.0415557;
            double diff_b = diff_X * 0.0556352 + diff_Z * 1.0570700;
            // XYZ->RGB (sRGB:D65)
            double dr = X * 3.2407100 + Y * -1.537260 + Z * -0.4985710;
            double dg = X * -0.9692580 + Y * 1.875990 + Z * 0.0415557;
            double db = X * 0.0556352 + Y * -0.203996 + Z * 1.0570700;
            // Compensate simulated color towards a neutral fit in RGB space
            double fit_r = ((dr < 0.0 ? 0.0 : 1.0) - dr) / diff_r;
            double fit_g = ((dg < 0.0 ? 0.0 : 1.0) - dg) / diff_g;
            double fit_b = ((db < 0.0 ? 0.0 : 1.0) - db) / diff_b;
            double adjust =
              std::max((fit_r > 1.0 || fit_r < 0.0) ? 0.0 : fit_r, (fit_g > 1.0 || fit_g < 0.0) ? 0.0 : fit_g);
            adjust = std::max((fit_b > 1.0 || fit_b < 0.0) ? 0.0 : fit_b, adjust);
            // Shift proportional to the greatest shift
            dr += adjust * diff_r;
            dg += adjust * diff_g;
            db += adjust * diff_b;
            // Apply gamma correction
            dr = std::pow(dr, inv_gamma);
            dg = std::pow(dg, inv_gamma);
            db = std::pow(db, inv_gamma);
            // Clamp values
            if (dr < 0.0 || std::isnan(dr))
                dr = 0.0;
            if (dr > 255.0)
                dr = 255.0;
            if (dg < 0.0 || std::isnan(dg))
                dg = 0.0;
            if (dg > 255.0)
                dg = 255.0;
            if (db < 0.0 || std::isnan(db))
                db = 0.0;
            if (db > 255.0)
                db = 255.0;
            // premultiply
            r = (static_cast<uint8_t>(dr) * a + 255) >> 8;
            g = (static_cast<uint8_t>(dg) * a + 255) >> 8;
            b = (static_cast<uint8_t>(db) * a + 255) >> 8;
        }
    }
    // set as premultiplied
    set_premultiplied_alpha(src, true);
}

template<typename Src>
void apply_filter(Src& src, color_blind_protanope const& op, double /*scale_factor*/)
{
    apply_color_blind_filter(src, op);
}

template<typename Src>
void apply_filter(Src& src, color_blind_deuteranope const& op, double /*scale_factor*/)
{
    apply_color_blind_filter(src, op);
}

template<typename Src>
void apply_filter(Src& src, color_blind_tritanope const& op, double /*scale_factor*/)
{
    apply_color_blind_filter(src, op);
}

template<typename Src>
void apply_filter(Src& src, gray const& /*op*/, double /*scale_factor*/)
{
    premultiply_alpha(src);
    using namespace boost::gil;

    rgba8_view_t src_view = rgba8_view(src);

    for (std::ptrdiff_t y = 0; y < src_view.height(); ++y)
    {
        rgba8_view_t::x_iterator src_it = src_view.row_begin(static_cast<long>(y));
        for (std::ptrdiff_t x = 0; x < src_view.width(); ++x)
        {
            // formula taken from boost/gil/color_convert.hpp:rgb_to_luminance
            uint8_t& r = get_color(src_it[x], red_t());
            uint8_t& g = get_color(src_it[x], green_t());
            uint8_t& b = get_color(src_it[x], blue_t());
            uint8_t v = uint8_t((4915 * r + 9667 * g + 1802 * b + 8192) >> 14);
            r = g = b = v;
        }
    }
}

template<typename Src, typename Dst>
void x_gradient_impl(Src const& src_view, Dst const& dst_view)
{
    for (std::ptrdiff_t y = 0; y < src_view.height(); ++y)
    {
        typename Src::x_iterator src_it = src_view.row_begin(static_cast<long>(y));
        typename Dst::x_iterator dst_it = dst_view.row_begin(static_cast<long>(y));

        dst_it[0][0] = 128 + (src_it[0][0] - src_it[1][0]) / 2;
        dst_it[0][1] = 128 + (src_it[0][1] - src_it[1][1]) / 2;
        dst_it[0][2] = 128 + (src_it[0][2] - src_it[1][2]) / 2;

        dst_it[dst_view.width() - 1][0] =
          128 + (src_it[(src_view.width()) - 2][0] - src_it[(src_view.width()) - 1][0]) / 2;
        dst_it[dst_view.width() - 1][1] =
          128 + (src_it[(src_view.width()) - 2][1] - src_it[(src_view.width()) - 1][1]) / 2;
        dst_it[dst_view.width() - 1][2] =
          128 + (src_it[(src_view.width()) - 2][2] - src_it[(src_view.width()) - 1][2]) / 2;

        dst_it[0][3] = dst_it[(src_view.width()) - 1][3] = 255;

        for (std::ptrdiff_t x = 1; x < src_view.width() - 1; ++x)
        {
            dst_it[x][0] = 128 + (src_it[x - 1][0] - src_it[x + 1][0]) / 2;
            dst_it[x][1] = 128 + (src_it[x - 1][1] - src_it[x + 1][1]) / 2;
            dst_it[x][2] = 128 + (src_it[x - 1][2] - src_it[x + 1][2]) / 2;
            dst_it[x][3] = 255;
        }
    }
}

template<typename Src>
void apply_filter(Src& src, x_gradient const& /*op*/, double /*scale_factor*/)
{
    premultiply_alpha(src);
    double_buffer<Src> tb(src);
    x_gradient_impl(tb.src_view, tb.dst_view);
}

template<typename Src>
void apply_filter(Src& src, y_gradient const& /*op*/, double /*scale_factor*/)
{
    premultiply_alpha(src);
    double_buffer<Src> tb(src);
    x_gradient_impl(rotated90ccw_view(tb.src_view), rotated90ccw_view(tb.dst_view));
}

template<typename Src>
void apply_filter(Src& src, invert const& /*op*/, double /*scale_factor*/)
{
    premultiply_alpha(src);
    using namespace boost::gil;

    rgba8_view_t src_view = rgba8_view(src);

    for (std::ptrdiff_t y = 0; y < src_view.height(); ++y)
    {
        rgba8_view_t::x_iterator src_it = src_view.row_begin(static_cast<long>(y));
        for (std::ptrdiff_t x = 0; x < src_view.width(); ++x)
        {
            // we only work with premultiplied source,
            // thus all color values must be <= alpha
            uint8_t a = get_color(src_it[x], alpha_t());
            uint8_t& r = get_color(src_it[x], red_t());
            uint8_t& g = get_color(src_it[x], green_t());
            uint8_t& b = get_color(src_it[x], blue_t());
            r = a - r;
            g = a - g;
            b = a - b;
        }
    }
}

template<typename Src>
struct filter_visitor
{
    filter_visitor(Src& src, double scale_factor = 1.0)
        : src_(src),
          scale_factor_(scale_factor)
    {}

    template<typename T>
    void operator()(T const& filter) const
    {
        apply_filter(src_, filter, scale_factor_);
    }

    Src& src_;
    double scale_factor_;
};

struct filter_radius_visitor
{
    int& radius_;
    filter_radius_visitor(int& radius)
        : radius_(radius)
    {}
    template<typename T>
    void operator()(T const& /*filter*/) const
    {}

    void operator()(agg_stack_blur const& op) const
    {
        if (static_cast<int>(op.rx) > radius_)
            radius_ = static_cast<int>(op.rx);
        if (static_cast<int>(op.ry) > radius_)
            radius_ = static_cast<int>(op.ry);
    }
};

template<typename Src>
void filter_image(Src& src, std::string const& filter, double scale_factor = 1)
{
    std::vector<filter_type> filter_vector;
    if (!parse_image_filters(filter, filter_vector))
    {
        throw std::runtime_error("Failed to parse filter argument in filter_image: '" + filter + "'");
    }
    filter_visitor<Src> visitor(src, scale_factor);
    for (filter_type const& filter_tag : filter_vector)
    {
        util::apply_visitor(visitor, filter_tag);
    }
}

template<typename Src>
Src filter_image(Src const& src, std::string const& filter, double scale_factor = 1)
{
    std::vector<filter_type> filter_vector;
    if (!parse_image_filters(filter, filter_vector))
    {
        throw std::runtime_error("Failed to parse filter argument in filter_image: '" + filter + "'");
    }
    Src new_src(src);
    filter_visitor<Src> visitor(new_src, scale_factor);
    for (filter_type const& filter_tag : filter_vector)
    {
        util::apply_visitor(visitor, filter_tag);
    }
    return new_src;
}

} // namespace filter

} // namespace mapnik

#endif // MAPNIK_IMAGE_FILTER_HPP
