#include "canvas.h"

#include <cmath>

namespace lunasvg {

static plutovg_matrix_t to_plutovg_matrix(const Transform& transform);
static plutovg_fill_rule_t to_plutovg_fill_rule(WindRule winding);
static plutovg_operator_t to_plutovg_operator(BlendMode mode);
static plutovg_line_cap_t to_plutovg_line_cap(LineCap cap);
static plutovg_line_join_t to_plutovg_line_join(LineJoin join);
static plutovg_spread_method_t to_plutovg_spread_methood(SpreadMethod spread);
static void to_plutovg_stops(plutovg_gradient_t* gradient, const GradientStops& stops);
static void to_plutovg_path(plutovg_t* pluto, const Path& path);

std::shared_ptr<Canvas> Canvas::create(unsigned char* data, unsigned int width, unsigned int height, unsigned int stride)
{
    return std::shared_ptr<Canvas>(new Canvas(data, static_cast<int>(width), static_cast<int>(height), static_cast<int>(stride)));
}

std::shared_ptr<Canvas> Canvas::create(double x, double y, double width, double height)
{
    if(width <= 0.0 || height <= 0.0)
        return std::shared_ptr<Canvas>(new Canvas(0, 0, 1, 1));

    auto l = static_cast<int>(floor(x));
    auto t = static_cast<int>(floor(y));
    auto r = static_cast<int>(ceil(x + width));
    auto b = static_cast<int>(ceil(y + height));
    return std::shared_ptr<Canvas>(new Canvas(l, t, r - l, b - t));
}

std::shared_ptr<Canvas> Canvas::create(const Rect& box)
{
    return create(box.x, box.y, box.w, box.h);
}

Canvas::Canvas(unsigned char* data, int width, int height, int stride)
{
    surface = plutovg_surface_create_for_data(data, width, height, stride);
    pluto = plutovg_create(surface);
    plutovg_matrix_init_identity(&translation);
    plutovg_rect_init(&rect, 0, 0, width, height);
}

Canvas::Canvas(int x, int y, int width, int height)
{
    surface = plutovg_surface_create(width, height);
    pluto = plutovg_create(surface);
    plutovg_matrix_init_translate(&translation, -x, -y);
    plutovg_rect_init(&rect, x, y, width, height);
}

Canvas::~Canvas()
{
    plutovg_surface_destroy(surface);
    plutovg_destroy(pluto);
}

void Canvas::setColor(const Color& color)
{
    plutovg_set_source_rgba(pluto, color.r, color.g, color.b, color.a);
}

void Canvas::setLinearGradient(double x1, double y1, double x2, double y2, const GradientStops& stops, SpreadMethod spread, const Transform& transform)
{
    auto gradient = plutovg_gradient_create_linear(x1, y1, x2, y2);
    auto matrix = to_plutovg_matrix(transform);
    to_plutovg_stops(gradient, stops);
    plutovg_gradient_set_spread(gradient, to_plutovg_spread_methood(spread));
    plutovg_gradient_set_matrix(gradient, &matrix);
    plutovg_set_source_gradient(pluto, gradient);
    plutovg_gradient_destroy(gradient);
}

void Canvas::setRadialGradient(double cx, double cy, double r, double fx, double fy, const GradientStops& stops, SpreadMethod spread, const Transform& transform)
{
    auto gradient = plutovg_gradient_create_radial(cx, cy, r, fx, fy, 0);
    auto matrix = to_plutovg_matrix(transform);
    to_plutovg_stops(gradient, stops);
    plutovg_gradient_set_spread(gradient, to_plutovg_spread_methood(spread));
    plutovg_gradient_set_matrix(gradient, &matrix);
    plutovg_set_source_gradient(pluto, gradient);
    plutovg_gradient_destroy(gradient);
}

void Canvas::setTexture(const Canvas* source, TextureType type, const Transform& transform)
{
    auto texture = plutovg_texture_create(source->surface);
    auto matrix = to_plutovg_matrix(transform);
    if(type == TextureType::Plain)
        plutovg_texture_set_type(texture, plutovg_texture_type_plain);
    else
        plutovg_texture_set_type(texture, plutovg_texture_type_tiled);

    plutovg_texture_set_matrix(texture, &matrix);
    plutovg_set_source_texture(pluto, texture);
    plutovg_texture_destroy(texture);
}

void Canvas::fill(const Path& path, const Transform& transform, WindRule winding, BlendMode mode, double opacity)
{
    auto matrix = to_plutovg_matrix(transform);
    plutovg_matrix_multiply(&matrix, &matrix, &translation);
    to_plutovg_path(pluto, path);
    plutovg_set_matrix(pluto, &matrix);
    plutovg_set_fill_rule(pluto, to_plutovg_fill_rule(winding));
    plutovg_set_opacity(pluto, opacity);
    plutovg_set_operator(pluto, to_plutovg_operator(mode));
    plutovg_fill(pluto);
}

void Canvas::stroke(const Path& path, const Transform& transform, double width, LineCap cap, LineJoin join, double miterlimit, const DashData& dash, BlendMode mode, double opacity)
{
    auto matrix = to_plutovg_matrix(transform);
    plutovg_matrix_multiply(&matrix, &matrix, &translation);
    to_plutovg_path(pluto, path);
    plutovg_set_matrix(pluto, &matrix);
    plutovg_set_line_width(pluto, width);
    plutovg_set_line_cap(pluto, to_plutovg_line_cap(cap));
    plutovg_set_line_join(pluto, to_plutovg_line_join(join));
    plutovg_set_miter_limit(pluto, miterlimit);
    plutovg_set_dash(pluto, dash.offset, dash.array.data(), static_cast<int>(dash.array.size()));
    plutovg_set_operator(pluto, to_plutovg_operator(mode));
    plutovg_set_opacity(pluto, opacity);
    plutovg_stroke(pluto);
}

void Canvas::blend(const Canvas* source, BlendMode mode, double opacity)
{
    plutovg_set_source_surface(pluto, source->surface, source->rect.x, source->rect.y);
    plutovg_set_operator(pluto, to_plutovg_operator(mode));
    plutovg_set_opacity(pluto, opacity);
    plutovg_set_matrix(pluto, &translation);
    plutovg_paint(pluto);
}

void Canvas::mask(const Rect& clip, const Transform& transform)
{
    auto matrix = to_plutovg_matrix(transform);
    auto path = plutovg_path_create();
    plutovg_path_add_rect(path, clip.x, clip.y, clip.w, clip.h);
    plutovg_path_transform(path, &matrix);
    plutovg_rect(pluto, rect.x, rect.y, rect.w, rect.h);
    plutovg_add_path(pluto, path);
    plutovg_path_destroy(path);

    plutovg_set_source_rgba(pluto, 0, 0, 0, 0);
    plutovg_set_fill_rule(pluto, plutovg_fill_rule_even_odd);
    plutovg_set_operator(pluto, plutovg_operator_src);
    plutovg_set_opacity(pluto, 0.0);
    plutovg_set_matrix(pluto, &translation);
    plutovg_fill(pluto);
}

void Canvas::clear(unsigned int value)
{
    auto r = (value >> 24) & 0xFF;
    auto g = (value >> 16) & 0xFF;
    auto b = (value >> 8) & 0xFF;
    auto a = (value >> 0) & 0xFF;

    plutovg_set_source_rgba(pluto, r / 255.0, g / 255.0, b / 255.0, a / 255.0);
    plutovg_set_opacity(pluto, 1.0);
    plutovg_set_operator(pluto, plutovg_operator_src);
    plutovg_paint(pluto);
}

void Canvas::rgba()
{
    auto width = plutovg_surface_get_width(surface);
    auto height = plutovg_surface_get_height(surface);
    auto stride = plutovg_surface_get_stride(surface);
    auto data = plutovg_surface_get_data(surface);
    for(int y = 0;y < height;y++)
    {
        auto pixels = reinterpret_cast<uint32_t*>(data + stride * y);
        for(int x = 0;x < width;x++)
        {
            auto pixel = pixels[x];
            auto a = (pixel >> 24) & 0xFF;
            if(a == 0)
                continue;

            auto r = (pixel >> 16) & 0xFF;
            auto g = (pixel >> 8) & 0xFF;
            auto b = (pixel >> 0) & 0xFF;
            if(a != 255)
            {
                r = (r * 255) / a;
                g = (g * 255) / a;
                b = (b * 255) / a;
            }

            pixels[x] = (a << 24) | (b << 16) | (g << 8) | r;
        }
    }
}

void Canvas::luminance()
{
    auto width = plutovg_surface_get_width(surface);
    auto height = plutovg_surface_get_height(surface);
    auto stride = plutovg_surface_get_stride(surface);
    auto data = plutovg_surface_get_data(surface);
    for(int y = 0;y < height;y++)
    {
        auto pixels = reinterpret_cast<uint32_t*>(data + stride * y);
        for(int x = 0;x < width;x++)
        {
            auto pixel = pixels[x];
            auto r = (pixel >> 16) & 0xFF;
            auto g = (pixel >> 8) & 0xFF;
            auto b = (pixel >> 0) & 0xFF;
            auto l = (2*r + 3*g + b) / 6;

            pixels[x] = l << 24;
        }
    }
}

unsigned int Canvas::width() const
{
    return plutovg_surface_get_width(surface);
}

unsigned int Canvas::height() const
{
    return plutovg_surface_get_height(surface);
}

unsigned int Canvas::stride() const
{
    return plutovg_surface_get_stride(surface);
}

unsigned char* Canvas::data() const
{
    return plutovg_surface_get_data(surface);
}

Rect Canvas::box() const
{
    return Rect(rect.x, rect.y, rect.w, rect.h);
}

plutovg_matrix_t to_plutovg_matrix(const Transform& transform)
{
    plutovg_matrix_t matrix;
    plutovg_matrix_init(&matrix, transform.m00, transform.m10, transform.m01, transform.m11, transform.m02, transform.m12);
    return matrix;
}

plutovg_fill_rule_t to_plutovg_fill_rule(WindRule winding)
{
    return winding == WindRule::EvenOdd ? plutovg_fill_rule_even_odd : plutovg_fill_rule_non_zero;
}

plutovg_operator_t to_plutovg_operator(BlendMode mode)
{
    return mode == BlendMode::Src ? plutovg_operator_src : mode == BlendMode::Src_Over ? plutovg_operator_src_over : mode == BlendMode::Dst_In ? plutovg_operator_dst_in : plutovg_operator_dst_out;
}

plutovg_line_cap_t to_plutovg_line_cap(LineCap cap)
{
    return cap == LineCap::Butt ? plutovg_line_cap_butt : cap == LineCap::Round ? plutovg_line_cap_round : plutovg_line_cap_square;
}

plutovg_line_join_t to_plutovg_line_join(LineJoin join)
{
    return join == LineJoin::Miter ? plutovg_line_join_miter : join == LineJoin::Round ? plutovg_line_join_round : plutovg_line_join_bevel;
}

static plutovg_spread_method_t to_plutovg_spread_methood(SpreadMethod spread)
{
    return spread == SpreadMethod::Pad ? plutovg_spread_method_pad : spread == SpreadMethod::Reflect ? plutovg_spread_method_reflect : plutovg_spread_method_repeat;
}

static void to_plutovg_stops(plutovg_gradient_t* gradient, const GradientStops& stops)
{
    for(const auto& stop : stops)
    {
        auto offset = std::get<0>(stop);
        auto& color = std::get<1>(stop);
        plutovg_gradient_add_stop_rgba(gradient, offset, color.r, color.g, color.b, color.a);
    }
}

void to_plutovg_path(plutovg_t* pluto, const Path& path)
{
    PathIterator it(path);
    std::array<Point, 3> p;
    while(!it.isDone())
    {
        switch(it.currentSegment(p)) {
        case PathCommand::MoveTo:
            plutovg_move_to(pluto, p[0].x, p[0].y);
            break;
        case PathCommand::LineTo:
            plutovg_line_to(pluto, p[0].x, p[0].y);
            break;
        case PathCommand::CubicTo:
            plutovg_cubic_to(pluto, p[0].x, p[0].y, p[1].x, p[1].y, p[2].x, p[2].y);
            break;
        case PathCommand::Close:
            plutovg_close_path(pluto);
            break;
        }

        it.next();
    }
}

} // namespace lunasvg
