// SPDX-License-Identifier: GPL-2.0-or-later
/** @file
 * Marker edit mode - onCanvas marker editing of marker orientation, position, scale
 *//*
 * Authors:
 * see git history
 * Rachana Podaralla <rpodaralla3@gatech.edu>
 *
 * Copyright (C) 2018 Authors
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include "display/curve.h"

#include "desktop.h"
#include "document.h"
#include "style.h"
#include "message-context.h"
#include "selection.h"

#include "object/sp-path.h"
#include "object/sp-shape.h"
#include "object/sp-marker.h"

#include "ui/shape-editor.h"
#include "ui/tool/event-utils.h"
#include "ui/tool/multi-path-manipulator.h"
#include "ui/tool/path-manipulator.h"
#include "ui/tools/marker-tool.h"


namespace Inkscape {
namespace UI {
namespace Tools {

MarkerTool::MarkerTool(SPDesktop *desktop)
    : ToolBase(desktop, "/tools/marker", "select.svg")
{
    Inkscape::Selection *selection = desktop->getSelection();

    this->sel_changed_connection.disconnect();
    this->sel_changed_connection = selection->connectChanged(
        sigc::mem_fun(*this, &MarkerTool::selection_changed)
    );
    this->selection_changed(selection);

    Inkscape::Preferences *prefs = Inkscape::Preferences::get();
    if (prefs->getBool("/tools/marker/selcue")) this->enableSelectionCue();
    if (prefs->getBool("/tools/marker/gradientdrag")) this->enableGrDrag();
}

MarkerTool::~MarkerTool()
{
    ungrabCanvasEvents();

    this->message_context->clear();
    this->_shape_editors.clear();

    this->enableGrDrag(false);
    this->sel_changed_connection.disconnect();
}

/*
- cycles through all the selected items to see if any have a marker in the right location (based on enterMarkerMode)
- if a matching item is found, loads the corresponding marker on the shape into the shape-editor and exits the loop
- forces user to only edit one marker at a time
*/
void MarkerTool::selection_changed(Inkscape::Selection *selection) {
    using namespace Inkscape::UI;

    g_assert(_desktop != nullptr);

    SPDocument *doc = _desktop->getDocument();
    g_assert(doc != nullptr);

    auto selected_items = selection->items();
    this->_shape_editors.clear();

    for(auto i = selected_items.begin(); i != selected_items.end(); ++i){
        SPItem *item = *i;

        if(item) {
            auto shape = cast<SPShape>(item);

            if(shape && shape->hasMarkers() && (editMarkerMode != -1)) {
                SPObject *obj = shape->_marker[editMarkerMode];

                if(obj) {

                    auto sp_marker = cast<SPMarker>(obj);
                    g_assert(sp_marker != nullptr);

                    sp_validate_marker(sp_marker, doc);

                    ShapeRecord sr;
                    switch(editMarkerMode) {
                        case SP_MARKER_LOC_START:
                            sr  = get_marker_transform(shape, item, sp_marker, SP_MARKER_LOC_START);
                            break;

                        case SP_MARKER_LOC_MID:
                            sr  = get_marker_transform(shape, item, sp_marker, SP_MARKER_LOC_MID);
                            break;

                        case SP_MARKER_LOC_END:
                            sr  = get_marker_transform(shape, item, sp_marker, SP_MARKER_LOC_END);
                            break;

                        default:
                            break;
                    }

                    auto si = std::make_unique<ShapeEditor>(_desktop, sr.edit_transform, sr.edit_rotation, editMarkerMode);
                    si->set_item(cast<SPItem>(sr.object));

                    this->_shape_editors.insert({item, std::move(si)});
                    break;                     
                }
            }
        }
    }
}

// handles selection of new items
bool MarkerTool::root_handler(GdkEvent* event) {
    g_assert(_desktop != nullptr);

    Inkscape::Selection *selection = _desktop->getSelection();
    gint ret = false;
    
    switch (event->type) {
        case GDK_BUTTON_PRESS:
            if (event->button.button == 1) {

                Geom::Point const button_w(event->button.x, event->button.y);  
                this->item_to_select = sp_event_context_find_item (_desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE);

                grabCanvasEvents();
                ret = true;
            }
            break;
        case GDK_BUTTON_RELEASE:
            if (event->button.button == 1) {

                if (this->item_to_select) {
                    // unselect all items, except for newly selected item
                    selection->set(this->item_to_select);
                } else {
                    // clicked into empty space, deselect any selected items
                    selection->clear();
                }

                this->item_to_select = nullptr;
                ungrabCanvasEvents();
                ret = true;
            }
            break;
        default:
            break;
    }

    return (!ret? ToolBase::root_handler(event): ret);
}

/* 
- this function uses similar logic that exists in sp_shape_update_marker_view
- however, the tangent angle needs to be saved here and parent_item->i2dt_affine() needs to also be accounted for in the right places
- calculate where the shape-editor knotholders need to go based on the reference shape
*/
ShapeRecord MarkerTool::get_marker_transform(SPShape* shape, SPItem *parent_item, SPMarker *sp_marker, SPMarkerLoc marker_type)
{

    // scale marker transform with parent stroke width
    SPStyle *style = shape->style;
    SPDocument *doc = _desktop->getDocument();
    Geom::Scale scale = doc->getDocumentScale();

    if(sp_marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) {
        scale *= Geom::Scale(style->stroke_width.computed);
    }

    Geom::PathVector const &pathv = shape->curve()->get_pathvector();
    Geom::Affine ret = Geom::identity(); //edit_transform
    double angle = 0.0; // edit_rotation - tangent angle used for auto orientation
    Geom::Point p;
    
    if(marker_type == SP_MARKER_LOC_START) {

        Geom::Curve const &c = pathv.begin()->front();
        p = c.pointAt(0);
        ret = Geom::Translate(p * parent_item->i2doc_affine());

        if (!c.isDegenerate()) {
            Geom::Point tang = c.unitTangentAt(0);
            angle = Geom::atan2(tang);
            ret = Geom::Rotate(angle) * ret;
        }

    } else if(marker_type == SP_MARKER_LOC_MID) {
        /* 
        - a shape can have multiple mid markers - only one is needed
        - once a valid mid marker is found, save edit_transfom and edit_rotation and break out of loop
        */
        for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) {

            // mid marker start position
            if (path_it != pathv.begin() && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)))
            {
                Geom::Curve const &c = path_it->front();
                p = c.pointAt(0);
                ret = Geom::Translate(p * parent_item->i2doc_affine());

                if (!c.isDegenerate()) {
                    Geom::Point tang = c.unitTangentAt(0);
                    angle = Geom::atan2(tang);
                    ret = Geom::Rotate(angle) * ret;
                    break;
                }
            }

            // mid marker mid positions
            if ( path_it->size_default() > 1) {
                Geom::Path::const_iterator curve_it1 = path_it->begin();
                Geom::Path::const_iterator curve_it2 = ++(path_it->begin());
                while (curve_it2 != path_it->end_default())
                {
                    Geom::Curve const & c1 = *curve_it1;
                    Geom::Curve const & c2 = *curve_it2;

                    p = c1.pointAt(1);
                    Geom::Curve * c1_reverse = c1.reverse();
                    Geom::Point tang1 = - c1_reverse->unitTangentAt(0);
                    delete c1_reverse;
                    Geom::Point tang2 = c2.unitTangentAt(0);

                    double const angle1 = Geom::atan2(tang1);
                    double const angle2 = Geom::atan2(tang2);

                    angle = .5 * (angle1 + angle2);

                    if ( fabs( angle2 - angle1 ) > M_PI ) {
                        angle += M_PI;
                    }

                    ret = Geom::Rotate(angle) * Geom::Translate(p * parent_item->i2doc_affine());

                    ++curve_it1;
                    ++curve_it2;
                    break;
                }
            }

            // mid marker end position
            if ( path_it != (pathv.end()-1) && !path_it->empty()) {
                Geom::Curve const &c = path_it->back_default();
                p = c.pointAt(1);
                ret = Geom::Translate(p * parent_item->i2doc_affine());

                if ( !c.isDegenerate() ) {
                    Geom::Curve * c_reverse = c.reverse();
                    Geom::Point tang = - c_reverse->unitTangentAt(0);
                    delete c_reverse;
                    angle = Geom::atan2(tang);
                    ret = Geom::Rotate(angle) * ret;
                    break;
                } 
            }
        }

    } else if (marker_type == SP_MARKER_LOC_END) {

        Geom::Path const &path_last = pathv.back();
        unsigned int index = path_last.size_default();
        if (index > 0) index--;

        Geom::Curve const &c = path_last[index];
        p = c.pointAt(1);
        ret = Geom::Translate(p * parent_item->i2doc_affine());

        if ( !c.isDegenerate() ) {
            Geom::Curve * c_reverse = c.reverse();
            Geom::Point tang = - c_reverse->unitTangentAt(0);
            delete c_reverse;
            angle = Geom::atan2(tang);
            ret = Geom::Rotate(angle) * ret;
        } 
    }

    /* scale by stroke width */
    ret = scale * ret;
    /* account for parent transform */
    ret = parent_item->transform.withoutTranslation() * ret;

    ShapeRecord sr;
    sr.object = sp_marker;
    sr.edit_transform = ret;
    sr.edit_rotation = angle * 180.0/M_PI;
    sr.role = SHAPE_ROLE_NORMAL;
    return sr;
}

}}}
