// SPDX-License-Identifier: GPL-2.0-or-later
/** \file
 * LPE <offset> implementation
 */
/*
 * Authors:
 *   Maximilian Albert
 *   Jabiertxo Arraiza
 *
 * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl>
 * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com>
 * Copyright (C) Jabierto Arraiza 2015 <jabier.arraiza@marker.es>
 *
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include "lpe-offset.h"

#include <2geom/path-intersection.h>
#include <2geom/piecewise.h>
#include <2geom/svg-path-parser.h>

#include "inkscape.h"
#include "style.h"

#include "display/curve.h"
#include "helper/geom-pathstroke.h"
#include "helper/geom.h"
#include "live_effects/parameter/enum.h"
#include "object/sp-shape.h"
#include "path/path-boolop.h"
#include "path/path-util.h"
#include "svg/svg.h"
#include "ui/knot/knot-holder.h"
#include "ui/knot/knot-holder-entity.h"
#include "util/units.h"

// TODO due to internal breakage in glibmm headers, this must be last:
#include <glibmm/i18n.h>

namespace Inkscape {
namespace LivePathEffect {

namespace OfS {
    class KnotHolderEntityOffsetPoint : public LPEKnotHolderEntity {
    public:
        KnotHolderEntityOffsetPoint(LPEOffset * effect) : LPEKnotHolderEntity(effect) {}
        ~KnotHolderEntityOffsetPoint() override
        {
            LPEOffset *lpe = dynamic_cast<LPEOffset *>(_effect);
            if (lpe) {
                lpe->_knot_entity = nullptr;
            }
        }
        void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override;
        void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override;
        Geom::Point knot_get() const override;
    private:
    };
} // OfS


static const Util::EnumData<unsigned> JoinTypeData[] = {
    // clang-format off
    {JOIN_BEVEL,       N_("Beveled"),    "bevel"},
    {JOIN_ROUND,       N_("Rounded"),    "round"},
    {JOIN_MITER,       N_("Miter"),      "miter"},
    {JOIN_MITER_CLIP,  N_("Miter Clip"), "miter-clip"},
    {JOIN_EXTRAPOLATE, N_("Extrapolated arc"), "extrp_arc"},
    {JOIN_EXTRAPOLATE1, N_("Extrapolated arc Alt1"), "extrp_arc1"},
    {JOIN_EXTRAPOLATE2, N_("Extrapolated arc Alt2"), "extrp_arc2"},
    {JOIN_EXTRAPOLATE3, N_("Extrapolated arc Alt3"), "extrp_arc3"},
    // clang-format on
};

static const Util::EnumDataConverter<unsigned> JoinTypeConverter(JoinTypeData, sizeof(JoinTypeData)/sizeof(*JoinTypeData));


LPEOffset::LPEOffset(LivePathEffectObject *lpeobject) :
    Effect(lpeobject),
    unit(_("Unit"), _("Unit of measurement"), "unit", &wr, this, "mm"),
    offset(_("Offset:"), _("Offset"), "offset", &wr, this, 0.0),
    linejoin_type(_("Join:"), _("Determines the shape of the path's corners"),  "linejoin_type", JoinTypeConverter, &wr, this, JOIN_MITER),
    miter_limit(_("Miter limit:"), _("Maximum length of the miter join (in units of stroke width)"), "miter_limit", &wr, this, 4.0),
    attempt_force_join(_("Force miter"), _("Overrides the miter limit and forces a join."), "attempt_force_join", &wr, this, false),
    update_on_knot_move(_("Live update"), _("Update while moving handle"), "update_on_knot_move", &wr, this, true)
{
    show_orig_path = true;
    registerParameter(&linejoin_type);
    registerParameter(&unit);
    registerParameter(&offset);
    registerParameter(&miter_limit);
    registerParameter(&attempt_force_join);
    registerParameter(&update_on_knot_move);
    offset.param_set_increments(0.1, 0.1);
    offset.param_set_digits(6);
    offset_pt = Geom::Point(Geom::infinity(), Geom::infinity());
    _knot_entity = nullptr;
    _provides_knotholder_entities = true;
    apply_to_clippath_and_mask = true;
    prev_unit = unit.get_abbreviation();
    liveknot = false;
    fillrule = fill_nonZero;
}

LPEOffset::~LPEOffset() {
    modified_connection.disconnect();
};

bool LPEOffset::doOnOpen(SPLPEItem const *lpeitem)
{
    bool fixed = false;
    if (!is_load || is_applied) {
        return fixed;
    }
    legacytest_livarotonly = false;
    Glib::ustring version = lpeversion.param_getSVGValue();
    if (version < "1.2") {
        if (!SP_ACTIVE_DESKTOP) {
            legacytest_livarotonly = true;
        }
        lpeversion.param_setValue("1.2", true);
        fixed = true;
    }
    return fixed;
}

void
LPEOffset::doOnApply(SPLPEItem const* lpeitem)
{
    lpeversion.param_setValue("1.2", true);
}

void
LPEOffset::modified(SPObject *obj, guint flags)
{
    if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG && sp_lpe_item) {
        // Get the used fillrule
        SPCSSAttr *css;
        const gchar *val;
        css = sp_repr_css_attr (sp_lpe_item->getRepr() , "style");
        val = sp_repr_css_property (css, "fill-rule", nullptr);
        FillRuleFlatten fillrule_chan = fill_nonZero;
        if (val && strcmp (val, "evenodd") == 0)
        {
            fillrule_chan = fill_oddEven;
        }
        if (fillrule != fillrule_chan) {
            sp_lpe_item_update_patheffect (sp_lpe_item, true, true);
        }
    }
}

Geom::Point get_nearest_point(Geom::PathVector pathv, Geom::Point point)
{
    Geom::Point res = Geom::Point(Geom::infinity(), Geom::infinity());
    std::optional< Geom::PathVectorTime > pathvectortime = pathv.nearestTime(point);
    if (pathvectortime) {
        Geom::PathTime pathtime = pathvectortime->asPathTime();
        res = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + pathtime.t);
    }
    return res;
}

/* double get_separation(Geom::PathVector pathv, Geom::Point point, Geom::Point point_b)
{
    Geom::Point res = Geom::Point(Geom::infinity(), Geom::infinity());
    std::optional<Geom::PathVectorTime> pathvectortime = pathv.nearestTime(point);
    std::optional<Geom::PathVectorTime> pathvectortime_b = pathv.nearestTime(point_b);
    if (pathvectortime && pathvectortime_b) {
        Geom::PathTime pathtime = pathvectortime->asPathTime();
        Geom::PathTime pathtime_b = pathvectortime_b->asPathTime();
        if ((*pathvectortime).path_index == (*pathvectortime_b).path_index) {
            return std::abs((pathtime.curve_index + pathtime.t) - (pathtime_b.curve_index + pathtime_b.t));
        }
    }
    return -1;
} */

void LPEOffset::transform_multiply(Geom::Affine const &postmul, bool /*set*/)
{
    refresh_widgets = true;
    if (!postmul.isTranslation()) {
        Geom::Affine current_affine = sp_item_transform_repr(sp_lpe_item);
        offset.param_transform_multiply(postmul * current_affine.inverse(), true);
    }
    offset_pt *= postmul;
}

Geom::Point LPEOffset::get_default_point(Geom::PathVector pathv)
{
    Geom::Point origin = Geom::Point(Geom::infinity(), Geom::infinity());
    Geom::OptRect bbox = pathv.boundsFast();
    if (bbox) {
        origin = Geom::Point((*bbox).midpoint()[Geom::X], (*bbox).top());
        origin = get_nearest_point(pathv, origin);
    }
    return origin;
}

double
LPEOffset::sp_get_offset(Geom::Point origin)
{
    double ret_offset = 0;
    int winding_value = mix_pathv_all.winding(origin);
    bool inset = false;
    if (winding_value % 2 != 0) {
        inset = true;
    }
    ret_offset = Geom::distance(origin, get_nearest_point(mix_pathv_all, origin));
    if (inset) {
        ret_offset *= -1;
    }
    return Inkscape::Util::Quantity::convert(ret_offset, "px", unit.get_abbreviation()) * this->scale;
}

void
LPEOffset::addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec)
{
    hp_vec.push_back(helper_path);
}

void
LPEOffset::doBeforeEffect (SPLPEItem const* lpeitem)
{
    auto obj = sp_lpe_item;
    if (is_load && obj) {
        modified_connection = obj->connectModified(sigc::mem_fun(*this, &LPEOffset::modified));
    }
    original_bbox(lpeitem);
    auto group = cast<SPGroup>(sp_lpe_item);
    if (group) {
        mix_pathv_all.clear();
    }
    this->scale = lpeitem->i2doc_affine().descrim();
    if (!is_load && prev_unit != unit.get_abbreviation()) {
        offset.param_set_undo(false);
        offset.param_set_value(Inkscape::Util::Quantity::convert(offset, prev_unit, unit.get_abbreviation()));
    } else {
        offset.param_set_undo(true);
    }
    prev_unit = unit.get_abbreviation();
}

int offset_winding(Geom::PathVector pathvector, Geom::Path path)
{
    int wind = 0;
    Geom::Point p = path.initialPoint();
    for (auto i:pathvector) {
        if (i == path)  continue;
        if (!i.boundsFast().contains(p)) continue;
        wind += i.winding(p);
    }
    return wind;
}

void LPEOffset::doAfterEffect(SPLPEItem const * /*lpeitem*/, SPCurve *curve)
{
    if (offset_pt == Geom::Point(Geom::infinity(), Geom::infinity())) {
        if (_knot_entity) {
            _knot_entity->knot_get();
        }
    }
    if (is_load) {
        offset_pt = Geom::Point(Geom::infinity(), Geom::infinity());
    }
    if (_knot_entity && sp_lpe_item && !liveknot) {
        Geom::PathVector out;
        // we don do this on groups, editing is joining ito so no need to update knot
        auto shape = cast<SPShape>(sp_lpe_item);
        if (shape) {
            out = cast<SPShape>(sp_lpe_item)->curve()->get_pathvector();
            offset_pt = get_nearest_point(out, offset_pt);
            _knot_entity->knot_get();
        }
    }
}

// TODO: find a way to not remove wanted self intersections
// previously are some failed attempts

/* // Taken from Knot LPE duple code
static Geom::Path::size_type size_nondegenerate(Geom::Path const &path)
{
    Geom::Path::size_type retval = path.size_default();
    const Geom::Curve &closingline = path.back_closed();
    // the closing line segment is always of type
    // Geom::LineSegment.
    if (are_near(closingline.initialPoint(), closingline.finalPoint())) {
        // closingline.isDegenerate() did not work, because it only checks for
        // *exact* zero length, which goes wrong for relative coordinates and
        // rounding errors...
        // the closing line segment has zero-length. So stop before that one!
        retval = path.size_open();
    }
    return retval;
}
gint get_nearest_corner(Geom::OptRect bbox, Geom::Point point)
{
    if (bbox) {
        double distance_a = Geom::distance(point, (*bbox).corner(0));
        double distance_b = Geom::distance(point, (*bbox).corner(1));
        double distance_c = Geom::distance(point, (*bbox).corner(2));
        double distance_d = Geom::distance(point, (*bbox).corner(3));
        std::vector<double> distances{distance_a, distance_b, distance_c, distance_d};
        std::vector<double>::iterator mindistance = std::min_element(distances.begin(), distances.end());
        return std::distance(distances.begin(), mindistance);
    }
    return -1;
}

// This way not work with good selfintersections on consecutive curves
// and when there is nodes nearest to points
// I try different methods to cleanup without luky
// if anyone is interested there is a way to explore
// if in original path the section into the 2 nearest point dont have
// self intersections we can suppose this is a intersection to remove
// it works very well but in good selfintersections work only in one offset direction
bool consecutiveCurves(Geom::Path pathin, Geom::Point p) {
    Geom::Coord mindist = std::numeric_limits<Geom::Coord>::max();
    size_t pos;
    for (size_t i = 0; i < pathin.size_default(); ++i) {
        Geom::Curve const &c = pathin.at(i);
        double dist = Geom::distance(p, c.boundsFast());
        if (dist >= mindist) {
            continue;
        }
        Geom::Coord t = c.nearestTime(p);
        Geom::Coord d = Geom::distance(c.pointAt(t), p);
        if (d < mindist) {
            pos = i;
            mindist = d;
        }
    }
    size_t pos_b;
    mindist = std::numeric_limits<Geom::Coord>::max();
    for (size_t i = 0; i < pathin.size_default(); ++i) {
        Geom::Curve const &c = pathin.at(i);
        double dist = Geom::distance(p, c.boundsFast());
        if (dist >= mindist || pos == i) {
            continue;
        }
        Geom::Coord t = c.nearestTime(p);
        Geom::Coord d = Geom::distance(c.pointAt(t), p);
        if (d < mindist) {
            pos_b = i;
            mindist = d;
        }
    }
    if (Geom::are_near(Geom::distance(pos,pos_b), 1)) {
        return true;
    }
    return false;
}

Geom::Path removeIntersects(Geom::Path pathin, Geom::Path pathorig, size_t skipcross)
{
    Geom::Path out;
    Geom::Crossings crossings = Geom::self_crossings(pathin);
    size_t counter = 0;
    for (auto cross : crossings) {
        if (!Geom::are_near(cross.ta, cross.tb, 0.01)) {
            size_t sizepath = size_nondegenerate(pathin);
            double ta = cross.ta > cross.tb ? cross.tb : cross.ta;
            double tb = cross.ta > cross.tb ? cross.ta : cross.tb;
            if (!pathin.closed()) {
                counter++;
                if (skipcross >= counter) {
                    continue;
                }
                bool removeint = consecutiveCurves(pathorig, pathin.pointAt(ta));
                Geom::Path p0 = pathin;
                if (removeint) {
                    Geom::Path p1 = pathin.portion(ta, tb);
                    if ((*p1.boundsFast()).maxExtent() > 0.01) {
                        p0 = pathin.portion(0, ta);
                        if (!Geom::are_near(tb, sizepath, 0.01)) {
                            Geom::Path p2 = pathin.portion(tb, sizepath);
                            p0.setFinal(p2.initialPoint());
                            p0.append(p2);
                        }
                    } else {
                        skipcross++;
                    }
                } else {
                    skipcross++;
                }
                out = removeIntersects(p0, pathorig, skipcross);
                return out;
            }
        }
    }
    return pathin;
} */

Geom::Path removeIntersects(Geom::Path pathin)
{
    // I have a pending ping to moazin for 1.2 to fix open paths offeset self intesections (Jabiertxof)
    // For 1.1 I comment the code because simply slow a lot or crash sometimes and never work really well
    /* Geom::Path out;
    Geom::Crossings crossings = Geom::self_crossings(pathin);
    static size_t maxiter = 0;
    if (!maxiter) {
        maxiter = crossings.size();
    }
    for (auto cross : crossings) {
        maxiter--;
        if (!maxiter) {
            return pathin;
        }
        if (!Geom::are_near(cross.ta, cross.tb, 0.01)) {
            size_t sizepath = size_nondegenerate(pathin);
            double ta = cross.ta > cross.tb ? cross.tb : cross.ta;
            double tb = cross.ta > cross.tb ? cross.ta : cross.tb;
            if (!pathin.closed()) {
                Geom::Path p0 = pathin;
                Geom::Path p1 = pathin.portion(ta, tb);
                p0 = pathin.portion(0, ta);
                if (!Geom::are_near(tb, sizepath, 0.01)) {
                    Geom::Path p2 = pathin.portion(tb, sizepath);
                    p0.setFinal(p2.initialPoint());
                    p0.append(p2);
                }
                out = removeIntersects(p0);
                return out;
            }
        }
    } */
    return pathin;
}

static Geom::PathVector
sp_simplify_pathvector(Geom::PathVector original_pathv, double threshold)
{
    auto pathliv = std::unique_ptr<Path>(Path_for_pathvector(original_pathv));
    pathliv->ConvertEvenLines(threshold);
    pathliv->Simplify(threshold);
    return pathliv->MakePathVector();
}

Geom::PathVector 
LPEOffset::doEffect_path(Geom::PathVector const & path_in)
{
    Geom::PathVector ret_closed;
    Geom::PathVector ret_open;
    SPItem *item = current_shape;
    SPDocument *document = getSPDoc();
    if (!item || !document) {
        return path_in;
    }
    // Get the used fillrule
    SPCSSAttr *css;
    const gchar *val;
    css = sp_repr_css_attr (item->getRepr() , "style");
    val = sp_repr_css_property (css, "fill-rule", nullptr);

    fillrule = fill_nonZero;
    if (val && strcmp (val, "evenodd") == 0)
    {
        fillrule = fill_oddEven;
    }

    double tolerance = -1;
    if (liveknot) {
        tolerance = 3;
    }
    // Get the doc units offset
    double to_offset = Inkscape::Util::Quantity::convert(offset, unit.get_abbreviation(), "px") / this->scale;
    Geom::PathVector open_pathv;
    Geom::PathVector closed_pathv;
    Geom::PathVector mix_pathv;
    Geom::PathVector mix_pathv_workon;
    Geom::PathVector orig_pathv = pathv_to_linear_and_cubic_beziers(path_in);
    helper_path = orig_pathv;
    // Store separated open/closed paths
    Geom::PathVector splitter;
    for (auto &i : orig_pathv) {
        // this improve offset in near closed paths
        if (Geom::are_near(i.initialPoint(), i.finalPoint())) {
            i.close(true);
        }
        if (i.closed()) {
            closed_pathv.push_back(i);
        } else {
            open_pathv.push_back(i);
        }
    }
    sp_flatten(closed_pathv, fillrule);

    // we flatten using original fill rule
    mix_pathv = open_pathv;
    for (auto path : closed_pathv) {
        mix_pathv.push_back(path);
    }
    auto group = cast<SPGroup>(sp_lpe_item);
    // Calculate the original pathvector used outside this function
    // to calculate the offset
    if (group) {
        mix_pathv_all.insert(mix_pathv_all.begin(), mix_pathv.begin(), mix_pathv.end());
    } else {
        mix_pathv_all = mix_pathv;
    }
    if (to_offset < 0) {
        Geom::OptRect bbox = mix_pathv.boundsFast();
        if (bbox) {
            (*bbox).expandBy(to_offset / 2.0);
            if ((*bbox).hasZeroArea()) {
                Geom::PathVector empty;
                return empty;
            }
        }
    }
    for (auto pathin : closed_pathv) {
        // Geom::OptRect bbox = pathin.boundsFast();
        // if (pbbox && (*pbbox).minExtent() > to_offset) {
        mix_pathv_workon.push_back(pathin);
        //}
    }
    mix_pathv_workon.insert(mix_pathv_workon.begin(), open_pathv.begin(), open_pathv.end());

    if (offset == 0.0) {
        if (is_load && offset_pt == Geom::Point(Geom::infinity(), Geom::infinity())) {
            offset_pt = get_default_point(path_in);
            if (_knot_entity) {
                _knot_entity->knot_get();
            }
        }
        return path_in;
    }
    Geom::OptRect bbox = closed_pathv.boundsFast();
    double bboxsize = 0;
    if (bbox) {
        bboxsize = (*bbox).maxExtent();
    }
    LineJoinType join = static_cast<LineJoinType>(linejoin_type.get_value());
    Geom::PathVector ret_closed_tmp;
    if (to_offset > 0) {
        for (auto &i : mix_pathv_workon) {
            Geom::Path tmp = half_outline(
                i, to_offset, (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), join, tolerance);
            if (tmp.closed()) {
                Geom::OptRect pbbox = tmp.boundsFast();
                if (pbbox && (*pbbox).minExtent() > to_offset) {
                    ret_closed_tmp.push_back(tmp);
                }
            } else {
                Geom::Path tmp_b = half_outline(i.reversed(), to_offset,
                                                (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit),
                                                join, tolerance);
                Geom::PathVector switch_pv_a(tmp);
                Geom::PathVector switch_pv_b(tmp_b);
                double distance_b = Geom::distance(offset_pt, get_nearest_point(switch_pv_a, offset_pt));
                double distance_a = Geom::distance(offset_pt, get_nearest_point(switch_pv_b, offset_pt));
                if (distance_b < distance_a) {
                    ret_open.push_back(removeIntersects(tmp));
                } else {
                    ret_open.push_back(removeIntersects(tmp_b));
                }
            }
        }
        sp_flatten(ret_closed_tmp, fill_nonZero);
        for (auto path : ret_closed_tmp) {
            ret_closed.push_back(path);
        }
    } else if (to_offset < 0) {
        for (auto &i : mix_pathv_workon) {
            double gap = 0.01;
            if (legacytest_livarotonly) {
                gap = 0;
            }
            Geom::Path tmp =
                half_outline(i.reversed(), std::abs(to_offset + gap),
                             (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), join, tolerance);
            // not remember why i instead tmp, afete 1.1 release we can switch to tmp to tests
            if (i.closed()) {
                Geom::PathVector out(tmp);
                for (auto path : out) {
                    ret_closed.push_back(path);
                }
            } else {
                Geom::Path tmp_b = half_outline(i, std::abs(to_offset),
                                                (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit),
                                                join, tolerance);
                Geom::PathVector switch_pv_a(tmp);
                Geom::PathVector switch_pv_b(tmp_b);
                double distance_b = Geom::distance(offset_pt, get_nearest_point(switch_pv_a, offset_pt));
                double distance_a = Geom::distance(offset_pt, get_nearest_point(switch_pv_b, offset_pt));
                if (distance_b < distance_a) {
                    ret_open.push_back(removeIntersects(tmp));
                } else {
                    ret_open.push_back(removeIntersects(tmp_b));
                }
            }
        }
        if (!ret_closed.empty()) {
            Geom::PathVector outline;
            for (const auto &i : mix_pathv_workon) {
                if (i.closed()) {
                    Geom::PathVector tmp = Inkscape::outline(i, std::abs(to_offset * 2), 4.0, join,
                                                             static_cast<LineCapType>(BUTT_FLAT), tolerance);
                    outline.insert(outline.begin(), tmp.begin(), tmp.end());
                }
            }
            sp_flatten(outline, fill_nonZero);
            double size = Geom::L2(Geom::bounds_fast(ret_closed)->dimensions());
            size /= sp_lpe_item->i2doc_affine().descrim();
            ret_closed = sp_pathvector_boolop(outline, ret_closed, bool_op_diff, fill_nonZero, fill_nonZero, legacytest_livarotonly);
            if (!liveknot && legacytest_livarotonly) {
                ret_closed = sp_simplify_pathvector(ret_closed, 0.0003 * size);
            }
        }
    }
    ret_closed.insert(ret_closed.begin(), ret_open.begin(), ret_open.end());
    return ret_closed;
}

void LPEOffset::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item)
{
    _knot_entity = new OfS::KnotHolderEntityOffsetPoint(this);
    _knot_entity->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE,
                         "LPEOffset", _("Offset point"));
    _knot_entity->knot->setMode(Inkscape::CANVAS_ITEM_CTRL_MODE_COLOR);
    _knot_entity->knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
    _knot_entity->knot->setFill(0xFF6600FF, 0x4BA1C7FF, 0xCF1410FF, 0xFF6600FF);
    _knot_entity->knot->setStroke(0x000000FF, 0x000000FF, 0x000000FF, 0x000000FF);
    _knot_entity->knot->updateCtrl();
    offset_pt = Geom::Point(Geom::infinity(), Geom::infinity());
    knotholder->add(_knot_entity);
}

namespace OfS {

void KnotHolderEntityOffsetPoint::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state)
{
    using namespace Geom;
    LPEOffset* lpe = dynamic_cast<LPEOffset *>(_effect);
    Geom::Point s = snap_knot_position(p, state);
    double offset = lpe->sp_get_offset(s);
    lpe->offset_pt = s;
    if (lpe->update_on_knot_move) {
        lpe->liveknot = true;
        lpe->offset.param_set_value(offset);
        sp_lpe_item_update_patheffect (cast<SPLPEItem>(item), false, false);
    } else {
        lpe->liveknot = false;
    }
}

void KnotHolderEntityOffsetPoint::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state)
{
    LPEOffset *lpe = dynamic_cast<LPEOffset *>(_effect);
    lpe->liveknot = false;
    using namespace Geom;
    Geom::Point s = lpe->offset_pt;
    double offset = lpe->sp_get_offset(s);
    lpe->offset.param_set_value(offset);
    lpe->makeUndoDone(_("Move handle"));
}

Geom::Point KnotHolderEntityOffsetPoint::knot_get() const
{
    LPEOffset *lpe = dynamic_cast<LPEOffset *>(_effect);
    if (!lpe) {
        return Geom::Point();
    }
    if (!lpe->update_on_knot_move) {
        return lpe->offset_pt;
    }
    Geom::Point nearest = lpe->offset_pt;
    if (nearest == Geom::Point(Geom::infinity(), Geom::infinity())) {
        Geom::PathVector out;
        auto group = cast<SPGroup>(item);
        auto shape = cast<SPShape>(item);
        if (group) {
            std::vector<SPItem *> item_list = group->item_list();
            for (auto child : item_list) {
                auto subchild = cast<SPShape>(child);
                if (subchild) {
                    Geom::PathVector tmp = subchild->curve()->get_pathvector();
                    out.insert(out.begin(), tmp.begin(), tmp.end());
                    sp_flatten(out, fill_oddEven);
                }
            }
        } else if (shape) {
            SPCurve const *c = shape->curve();
            if (c) {
                out = c->get_pathvector();
            }
        }
        if (!out.empty()) {
            nearest = lpe->get_default_point(out);
        }
    }
    lpe->offset_pt = nearest;
    return lpe->offset_pt;
}

} // namespace OfS
} //namespace LivePathEffect
} /* 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 :
