// SPDX-License-Identifier: GPL-2.0-or-later
/**
 * A class to represent a Bezier path.
 */

/*
 * Author:
 *   Tavmjong Bah
 *
 * Copyright (C) 2020 Tavmjong Bah
 *
 * Rewrite of SPCanvasBPath
 *
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include "canvas-item-bpath.h"

#include <memory>  // unique_ptr
#include <utility> // std::move

#include "color.h" // SP_RGBA_x_F

#include "display/curve.h"
#include "display/cairo-utils.h"

#include "helper/geom.h"  // bounds_exact_transformed()

#include "ui/widget/canvas.h"

namespace Inkscape {

/**
 * Create an null control bpath.
 */
CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group)
    : CanvasItem(group)
{
    _name = "CanvasItemBpath:Null";
    _pickable = true; // For now, nobody gets events from this class!
}

/**
 * Create a control bpath. Curve is in document coordinates.
 */
CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group, SPCurve *curve, bool phantom_line)
    : CanvasItem(group)
    , _phantom_line(phantom_line)
{
    _name = "CanvasItemBpath";
    _pickable = true; // For now, everyone gets events from this class!
    if (curve) {
        _path = curve->get_pathvector();
    }

    request_update(); // Render immediately or temporary bpaths won't show.
}

/**
 * Create a control bpath. Path is in document coordinates.
 */
CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group, Geom::PathVector path, bool phantom_line)
    : CanvasItem(group)
    , _path(std::move(path))
    , _phantom_line(phantom_line)
{
    _name = "CanvasItemBpath";
    _pickable = true; // For now, everyone gets events from this class!
    request_update(); // Render immediately or temporary bpaths won't show.
}

/**
 * Set a control bpath. Curve is in document coordinates.
 */
void CanvasItemBpath::set_bpath(SPCurve *curve, bool phantom_line)
{
    if (curve) {   // No test to see if *curve == *_curve so we always do the swap.
        _path = curve->get_pathvector();
    } else {
        _path.clear();
    }

    _phantom_line = phantom_line;

    request_update();
}

/**
 * Set a control bpath. Path is in document coordinates.
 */
void CanvasItemBpath::set_bpath(Geom::PathVector const &path, bool phantom_line)
{
    _path = path;
    _phantom_line = phantom_line;

    request_update();
}

/**
 * Set the fill color and fill rule.
 */
void CanvasItemBpath::set_fill(guint rgba, SPWindRule fill_rule)
{
    if (_fill != rgba || _fill_rule != fill_rule) {
        _fill = rgba;
        _fill_rule = fill_rule;
        request_redraw();
    }
}

/**
 * Returns distance between point in canvas units and nearest point on bpath.
 */
double CanvasItemBpath::closest_distance_to(Geom::Point const &p)
{
    double d = Geom::infinity();

    // Convert p to document coordinates (quicker than converting path to canvas units).
    Geom::Point p_doc = p * _affine.inverse();
    _path.nearestTime(p_doc, &d);
    d *= _affine.descrim(); // Uniform scaling and rotation only.

    return d;
}

/**
 * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of bpath.
 */
bool CanvasItemBpath::contains(Geom::Point const &p, double tolerance)
{
    if (tolerance == 0) {
        tolerance = 1; // Need a minimum tolerance value or always returns false.
    }

    return closest_distance_to(p) < tolerance;
}

/**
 * Update and redraw control bpath.
 */
void CanvasItemBpath::update(Geom::Affine const &affine)
{
    if (_affine == affine && !_need_update) {
        // Nothing to do.
        return;
    }

    // Queue redraw of old area (erase previous content).
    request_redraw();

    // Get new bounds
    _affine = affine;

    _bounds = Geom::Rect(); // Reset bounds

    if (_path.empty()) return; // No path, no chocolate!

    Geom::OptRect bbox = bounds_exact_transformed(_path, _affine);

    if (bbox) {
        _bounds = *bbox;
        _bounds.expandBy(2);
    } else {
        _bounds = Geom::Rect();
    }

    // Queue redraw of new area
    request_redraw();

    _need_update = false;
}

/**
 * Render bpath to screen via Cairo.
 */
void CanvasItemBpath::render(Inkscape::CanvasItemBuffer *buf)
{
    if (!buf) {
        std::cerr << "CanvasItemBpath::Render: No buffer!" << std::endl;
         return;
    }

    if (!_visible) {
        // Hidden
        return;
    }

    if (_path.empty()) {
        return;
    }

    bool do_fill   = (_fill   & 0xff) != 0; // Not invisible.
    bool do_stroke = (_stroke & 0xff) != 0; // Not invisible.

    if (!do_fill && !do_stroke) {
        // Both fill and stroke invisible.
        return;
    }

    buf->cr->save();

    // Setup path
    buf->cr->set_tolerance(0.5);
    buf->cr->begin_new_path();

    feed_pathvector_to_cairo (buf->cr->cobj(), _path, _affine, buf->rect,
                              /* optimized_stroke = */ !do_fill, 1);

    // Do fill
    if (do_fill) {
        buf->cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill),
                                 SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill));
        buf->cr->set_fill_rule(_fill_rule == SP_WIND_RULE_EVENODD ?
                               Cairo::FILL_RULE_EVEN_ODD : Cairo::FILL_RULE_WINDING);
        buf->cr->fill_preserve();
    }

    // Do stroke
    if (do_stroke) {

        if (!_dashes.empty()) {
            buf->cr->set_dash(_dashes, 0.0); // 0.0 is offset
        }

        if (_phantom_line) {
            buf->cr->set_source_rgba(1.0, 1.0, 1.0, 0.25);
            buf->cr->set_line_width(2.0);
            buf->cr->stroke_preserve();
        }

        buf->cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke),
                                 SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke));
        buf->cr->set_line_width(1.0);
        buf->cr->stroke();

    } else {
        buf->cr->begin_new_path(); // Clears path
    }

    // Uncomment to show bounds
    // Geom::Rect bounds = _bounds;
    // bounds.expandBy(-1);
    // bounds -= buf->rect.min();
    // buf->cr->set_source_rgba(1.0, 0.0, 0.0, 1.0);
    // buf->cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height());
    // buf->cr->stroke();

    buf->cr->restore();
}

} // namespace Inkscape

/*
  Local Variables:
  mode:c++
  c-file-style:"stroustrup"
  c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
  indent-tabs-mode:nil
  fill-column:99
  End:
*/
// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
