/***************************************************************************
 *   Copyright (c) 2008 Jürgen Riegel <juergen.riegel@web.de>              *
 *                                                                         *
 *   This file is part of the FreeCAD CAx development system.              *
 *                                                                         *
 *   This library is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU Library General Public           *
 *   License as published by the Free Software Foundation; either          *
 *   version 2 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 Library General Public License for more details.                  *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with this library; see the file COPYING.LIB. If not,    *
 *   write to the Free Software Foundation, Inc., 59 Temple Place,         *
 *   Suite 330, Boston, MA  02111-1307, USA                                *
 *                                                                         *
 ***************************************************************************/

#include <QApplication>
#include <QCheckBox>
#include <QGridLayout>
#include <QVBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QSignalBlocker>
#include <QWidgetAction>


#include <App/DocumentObjectGroup.h>
#include <App/Datums.h>
#include <Gui/Action.h>
#include <Gui/Application.h>
#include <Gui/BitmapFactory.h>
#include <Gui/CommandT.h>
#include <Gui/Control.h>
#include <Gui/Document.h>
#include <Gui/MainWindow.h>
#include <Gui/Notifications.h>
#include <Gui/PrefWidgets.h>
#include <Gui/QuantitySpinBox.h>
#include <Gui/Selection/SelectionFilter.h>
#include <Gui/Selection/SelectionObject.h>
#include <Mod/Part/App/Attacher.h>
#include <Mod/Part/App/Part2DObject.h>
#include <Mod/Part/Gui/AttacherTexts.h>
#include <Mod/Sketcher/App/Constraint.h>
#include <Mod/Sketcher/App/SketchObject.h>

#include "SketchMirrorDialog.h"
#include "SketchOrientationDialog.h"
#include "TaskSketcherValidation.h"
#include "Utils.h"
#include "ViewProviderSketch.h"
#include "Command.h"

// Hint: this is to prevent to re-format big parts of the file. Remove it later again.
// clang-format off
using namespace std;
using namespace SketcherGui;
using namespace Part;
using namespace Attacher;


namespace SketcherGui
{

class ExceptionWrongInput: public Base::Exception
{
public:
    ExceptionWrongInput()
        : ErrMsg(QString())
    {}

    // Pass untranslated strings, enclosed in QT_TR_NOOP()
    explicit ExceptionWrongInput(const char* ErrMsg)
    {
        this->ErrMsg = QObject::tr(ErrMsg);
        this->setMessage(ErrMsg);
    }

    ~ExceptionWrongInput() noexcept override
    {}

    QString ErrMsg;
};


Attacher::eMapMode SuggestAutoMapMode(Attacher::SuggestResult::eSuggestResult* pMsgId = nullptr,
                                      QString* message = nullptr,
                                      std::vector<Attacher::eMapMode>* allmodes = nullptr)
{
    // convert pointers into valid references, to avoid checking for null pointers everywhere
    Attacher::SuggestResult::eSuggestResult buf;
    if (!pMsgId)
        pMsgId = &buf;
    Attacher::SuggestResult::eSuggestResult& msg = *pMsgId;
    QString buf2;
    if (!message)
        message = &buf2;
    QString& msg_str = *message;

    App::PropertyLinkSubList tmpSupport;
    Gui::Selection().getAsPropertyLinkSubList(tmpSupport);

    Attacher::SuggestResult sugr;
    AttachEngine3D eng;
    eng.setUp(tmpSupport);
    eng.suggestMapModes(sugr);
    if (allmodes)
        *allmodes = sugr.allApplicableModes;
    msg = sugr.message;
    switch (msg) {
        case Attacher::SuggestResult::srOK:
            break;
        case Attacher::SuggestResult::srNoModesFit:
            msg_str = QObject::tr("There are no modes that accept the selected set of subelements");
            break;
        case Attacher::SuggestResult::srLinkBroken:
            msg_str = QObject::tr("Broken link to support subelements");
            break;
        case Attacher::SuggestResult::srUnexpectedError:
            msg_str = QObject::tr("Unexpected error");
            break;
        case Attacher::SuggestResult::srIncompatibleGeometry:
            if (tmpSupport.getSubValues()[0].substr(0, 4) == std::string("Face"))
                msg_str = QObject::tr("Face is non-planar");
            else
                msg_str = QObject::tr("Selected shapes are of wrong form (e.g., a curved edge "
                                      "where a straight one is needed)");
            break;
        default:
            msg_str = QObject::tr("Unexpected error");
            assert(0 /*no message for eSuggestResult enum item*/);
    }

    return sugr.bestFitMode;
}
}// namespace SketcherGui


/* Sketch commands =======================================================*/
DEF_STD_CMD_A(CmdSketcherNewSketch)

CmdSketcherNewSketch::CmdSketcherNewSketch()
    : Command("Sketcher_NewSketch")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("New Sketch");
    sToolTipText = QT_TR_NOOP("Creates a new sketch");
    sWhatsThis = "Sketcher_NewSketch";
    sStatusTip = sToolTipText;
    sPixmap = "Sketcher_NewSketch";
}

void CmdSketcherNewSketch::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    Attacher::eMapMode mapmode = Attacher::mmDeactivated;
    std::string groupName;
    bool bAttach = false;
    bool groupSelected = false;
    if (Gui::Selection().countObjectsOfType<App::DocumentObjectGroup>() > 0) {
        auto selection = Gui::Selection().getSelection();
        if (selection.size() > 1) {
            Gui::TranslatedUserWarning(
                getActiveGuiDocument(),
                QObject::tr("Invalid selection"),
                QObject::tr("Too many objects selected"));
                return;
        }

        groupName = selection[0].FeatName;
        groupSelected = true;
    }
    else if (Gui::Selection().hasSelection()) {
        Attacher::SuggestResult::eSuggestResult msgid = Attacher::SuggestResult::srOK;
        QString msg_str;
        std::vector<Attacher::eMapMode> validModes;
        mapmode = SuggestAutoMapMode(&msgid, &msg_str, &validModes);
        if (msgid == Attacher::SuggestResult::srOK)
            bAttach = true;
        if (msgid != Attacher::SuggestResult::srOK
            && msgid != Attacher::SuggestResult::srNoModesFit) {
            Gui::TranslatedUserWarning(
                getActiveGuiDocument(),
                QObject::tr("Sketch mapping"),
                QObject::tr("Cannot map the sketch to the selected object. %1.").arg(msg_str));
            return;
        }
        if (validModes.size() > 1) {
            validModes.insert(validModes.begin(), Attacher::mmDeactivated);
            bool ok;
            QStringList items;
            items.push_back(QObject::tr("Do not attach"));
            int iSugg = 0;// index of the auto-suggested mode in the list of valid modes
            for (size_t i = 0; i < validModes.size(); ++i) {
                auto uiStrings =
                    AttacherGui::getUIStrings(AttachEnginePlane::getClassTypeId(), validModes[i]);
                items.push_back(uiStrings[0]);
                if (validModes[i] == mapmode)
                    iSugg = items.size() - 1;
            }
            QString text = QInputDialog::getItem(
                Gui::getMainWindow(),
                qApp->translate("Sketcher_NewSketch", "Sketch Attachment"),
                qApp->translate("Sketcher_NewSketch",
                                "Select the method to attach this sketch to selected object"),
                items,
                iSugg,
                false,
                &ok,
                Qt::MSWindowsFixedSizeDialogHint);
            if (!ok)
                return;
            int index = items.indexOf(text);
            if (index == 0) {
                bAttach = false;
                mapmode = Attacher::mmDeactivated;
            }
            else {
                bAttach = true;
                mapmode = validModes[index - 1];
            }
        }
    }

    if (bAttach) {

        std::vector<Gui::SelectionObject> objects = Gui::Selection().getSelectionEx();
        // assert (objects.size() == 1); //should have been filtered out by SuggestAutoMapMode
        // Gui::SelectionObject &sel_support = objects[0];
        App::PropertyLinkSubList support;
        Gui::Selection().getAsPropertyLinkSubList(support);
        std::string supportString = support.getPyReprString();

        // create Sketch on Face
        std::string FeatName = getUniqueObjectName("Sketch");

        openCommand(QT_TRANSLATE_NOOP("Command", "Create a new sketch on a face"));
        doCommand(Doc,
                  "App.activeDocument().addObject('Sketcher::SketchObject', '%s')",
                  FeatName.c_str());
        if (mapmode < Attacher::mmDummy_NumberOfModes)
            doCommand(Gui,
                      "App.activeDocument().%s.MapMode = \"%s\"",
                      FeatName.c_str(),
                      AttachEngine::getModeName(mapmode).c_str());
        else
            assert(0 /* mapmode index out of range */);
        doCommand(
            Gui, "App.activeDocument().%s.AttachmentSupport = %s", FeatName.c_str(), supportString.c_str());
        doCommand(Gui, "App.activeDocument().recompute()");// recompute the sketch placement based
                                                           // on its support
        doCommand(Gui, "Gui.activeDocument().setEdit('%s')", FeatName.c_str());

        Part::Feature* part = static_cast<Part::Feature*>(
            support.getValue());// if multi-part support, this will return 0
        if (part) {
            App::DocumentObjectGroup* grp = part->getGroup();
            if (grp) {
                doCommand(Doc,
                          "App.activeDocument().%s.addObject(App.activeDocument().%s)",
                          grp->getNameInDocument(),
                          FeatName.c_str());
            }
        }
    }
    else {
        // ask user for orientation
        SketchOrientationDialog Dlg;

        Dlg.adjustSize();
        if (Dlg.exec() != QDialog::Accepted)
            return;// canceled
        Base::Vector3d p = Dlg.Pos.getPosition();
        Base::Rotation r = Dlg.Pos.getRotation();

        std::string FeatName = getUniqueObjectName("Sketch");

        openCommand(QT_TRANSLATE_NOOP("Command", "Create a new sketch"));
        if (groupSelected) {
            doCommand(Doc,
                    "App.activeDocument().getObject('%s').addObject(App.activeDocument().addObject('Sketcher::SketchObject', '%s'))",
                    groupName.c_str(),
                    FeatName.c_str());
        }
        else {
            doCommand(Doc,
                  "App.activeDocument().addObject('Sketcher::SketchObject', '%s')",
                  FeatName.c_str());
        }

        doCommand(Doc,
                  "App.activeDocument().%s.Placement = App.Placement(App.Vector(%f, %f, %f), "
                  "App.Rotation(%f, %f, %f, %f))",
                  FeatName.c_str(),
                  p.x,
                  p.y,
                  p.z,
                  r[0],
                  r[1],
                  r[2],
                  r[3]);
        doCommand(Doc,
                  "App.activeDocument().%s.MapMode = \"%s\"",
                  FeatName.c_str(),
                  AttachEngine::getModeName(Attacher::mmDeactivated).c_str());
        doCommand(Gui, "Gui.activeDocument().setEdit('%s')", FeatName.c_str());
    }
}

bool CmdSketcherNewSketch::isActive()
{
    if (getActiveGuiDocument())
        return true;
    else
        return false;
}

DEF_STD_CMD_A(CmdSketcherEditSketch)

CmdSketcherEditSketch::CmdSketcherEditSketch()
    : Command("Sketcher_EditSketch")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Edit Sketch");
    sToolTipText = QT_TR_NOOP("Opens the selected sketch for editing");
    sWhatsThis = "Sketcher_EditSketch";
    sStatusTip = sToolTipText;
    sPixmap = "Sketcher_EditSketch";
}

void CmdSketcherEditSketch::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    Gui::SelectionFilter SketchFilter("SELECT Sketcher::SketchObject COUNT 1");

    if (SketchFilter.match()) {
        Sketcher::SketchObject* Sketch =
            static_cast<Sketcher::SketchObject*>(SketchFilter.Result[0][0].getObject());
        doCommand(Gui, "Gui.activeDocument().setEdit('%s')", Sketch->getNameInDocument());
    }
}

bool CmdSketcherEditSketch::isActive()
{
    return Gui::Selection().countObjectsOfType<Sketcher::SketchObject>() == 1;
}

DEF_STD_CMD_A(CmdSketcherLeaveSketch)

CmdSketcherLeaveSketch::CmdSketcherLeaveSketch()
    : Command("Sketcher_LeaveSketch")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Leave Sketch");
    sToolTipText = QT_TR_NOOP("Exits the active sketch");
    sWhatsThis = "Sketcher_LeaveSketch";
    sStatusTip = sToolTipText;
    sPixmap = "Sketcher_LeaveSketch";
    eType = 0;
}

void CmdSketcherLeaveSketch::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    Gui::Document* doc = getActiveGuiDocument();

    if (doc) {
        // checks if a Sketch Viewprovider is in Edit and is in no special mode
        SketcherGui::ViewProviderSketch* vp =
            dynamic_cast<SketcherGui::ViewProviderSketch*>(doc->getInEdit());
        if (vp && vp->getSketchMode() != ViewProviderSketch::STATUS_NONE)
            vp->purgeHandler();
    }

    // See also TaskDlgEditSketch::reject
    doCommand(Gui, "Gui.activeDocument().resetEdit()");
    doCommand(Doc, "App.ActiveDocument.recompute()");
}

bool CmdSketcherLeaveSketch::isActive()
{
    return isSketchInEdit(getActiveGuiDocument());
}

DEF_STD_CMD_A(CmdSketcherStopOperation)

CmdSketcherStopOperation::CmdSketcherStopOperation()
    : Command("Sketcher_StopOperation")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Stop Operation");
    sToolTipText = QT_TR_NOOP("Stops the active operation while in edit mode");


    sWhatsThis = "Sketcher_StopOperation";
    sStatusTip = sToolTipText;
    sPixmap = "process-stop";
    eType = 0;
}

void CmdSketcherStopOperation::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    Gui::Document* doc = getActiveGuiDocument();

    if (doc) {
        SketcherGui::ViewProviderSketch* vp =
            dynamic_cast<SketcherGui::ViewProviderSketch*>(doc->getInEdit());
        if (vp) {
            vp->purgeHandler();
        }
    }
}

bool CmdSketcherStopOperation::isActive()
{
    return isSketchInEdit(getActiveGuiDocument());
}

DEF_STD_CMD_A(CmdSketcherReorientSketch)

CmdSketcherReorientSketch::CmdSketcherReorientSketch()
    : Command("Sketcher_ReorientSketch")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Reorient Sketch");
    sToolTipText = QT_TR_NOOP("Places the selected sketch on one of the global coordinate planes.\n"
                              "This will clear the AttachmentSupport property.");
    sWhatsThis = "Sketcher_ReorientSketch";
    sStatusTip = sToolTipText;
    sPixmap = "Sketcher_ReorientSketch";
}

void CmdSketcherReorientSketch::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    Sketcher::SketchObject* sketch =
        Gui::Selection().getObjectsOfType<Sketcher::SketchObject>().front();
    if (sketch->AttachmentSupport.getValue()) {
        int ret = QMessageBox::question(
            Gui::getMainWindow(),
            qApp->translate("Sketcher_ReorientSketch", "Sketch Has Support"),
            qApp->translate("Sketcher_ReorientSketch",
                            "Sketch with a support face cannot be reoriented.\n"
                            "Detach it from the support?"),
            QMessageBox::Yes | QMessageBox::No);
        if (ret == QMessageBox::No)
            return;
        sketch->AttachmentSupport.setValue(nullptr);
    }

    // ask user for orientation
    SketchOrientationDialog Dlg;

    if (Dlg.exec() != QDialog::Accepted)
        return;// canceled
    Base::Vector3d p = Dlg.Pos.getPosition();
    Base::Rotation r = Dlg.Pos.getRotation();

    // do the right view direction
    std::string camstring;
    switch (Dlg.DirType) {
        case 0:
            camstring = "#Inventor V2.1 ascii\\n"
                        "OrthographicCamera {\\n"
                        " viewportMapping ADJUST_CAMERA\\n"
                        "  position 0 0 87\\n"
                        "  orientation 0 0 1  0\\n"
                        "  nearDistance -112.88701\\n"
                        "  farDistance 287.28702\\n"
                        "  aspectRatio 1\\n"
                        "  focalDistance 87\\n"
                        "  height 143.52005 }";
            break;
        case 1:
            camstring = "#Inventor V2.1 ascii\\n"
                        "OrthographicCamera {\\n"
                        " viewportMapping ADJUST_CAMERA\\n"
                        "  position 0 0 -87\\n"
                        "  orientation -1 0 0  3.1415927\\n"
                        "  nearDistance -112.88701\\n"
                        "  farDistance 287.28702\\n "
                        "  aspectRatio 1\\n"
                        "  focalDistance 87\\n"
                        "  height 143.52005 }";
            break;
        case 2:
            camstring = "#Inventor V2.1 ascii\\n"
                        "OrthographicCamera {\\n"
                        " viewportMapping ADJUST_CAMERA\\n"
                        "  position 0 -87 0\\n"
                        "  orientation -1 0 0  4.712389\\n"
                        "  nearDistance -112.88701\\n"
                        "  farDistance 287.28702\\n"
                        "  aspectRatio 1\\n"
                        "  focalDistance 87\\n"
                        "  height 143.52005\\n\\n}";
            break;
        case 3:
            camstring = "#Inventor V2.1 ascii\\n"
                        "OrthographicCamera {\\n"
                        " viewportMapping ADJUST_CAMERA\\n"
                        "  position 0 87 0\\n"
                        "  orientation 0 0.70710683 0.70710683  3.1415927\\n"
                        "  nearDistance -112.88701\\n"
                        "  farDistance 287.28702\\n"
                        "  aspectRatio 1\\n"
                        "  focalDistance 87\\n"
                        "  height 143.52005\\n\\n}";
            break;
        case 4:
            camstring = "#Inventor V2.1 ascii\\n"
                        "OrthographicCamera {\\n"
                        " viewportMapping ADJUST_CAMERA\\n"
                        "  position 87 0 0\\n"
                        "  orientation 0.57735026 0.57735026 0.57735026  2.0943952\\n"
                        "  nearDistance -112.887\\n"
                        "  farDistance 287.28699\\n"
                        "  aspectRatio 1\\n"
                        "  focalDistance 87\\n"
                        "  height 143.52005\\n\\n}";
            break;
        case 5:
            camstring = "#Inventor V2.1 ascii\\n"
                        "OrthographicCamera {\\n"
                        " viewportMapping ADJUST_CAMERA\\n"
                        "  position -87 0 0\\n"
                        "  orientation -0.57735026 0.57735026 0.57735026  4.1887903\\n"
                        "  nearDistance -112.887\\n"
                        "  farDistance 287.28699\\n"
                        "  aspectRatio 1\\n"
                        "  focalDistance 87\\n"
                        "  height 143.52005\\n\\n}";
            break;
    }

    openCommand(QT_TRANSLATE_NOOP("Command", "Reorient sketch"));
    Gui::cmdAppObjectArgs(
        sketch,
        "Placement = App.Placement(App.Vector(%f, %f, %f), App.Rotation(%f, %f, %f, %f))",
        p.x,
        p.y,
        p.z,
        r[0],
        r[1],
        r[2],
        r[3]);
    doCommand(Gui, "Gui.ActiveDocument.setEdit('%s')", sketch->getNameInDocument());
}

bool CmdSketcherReorientSketch::isActive()
{
    return Gui::Selection().countObjectsOfType<Sketcher::SketchObject>() == 1;
}

DEF_STD_CMD_A(CmdSketcherMapSketch)

CmdSketcherMapSketch::CmdSketcherMapSketch()
    : Command("Sketcher_MapSketch")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Attach Sketch");
    sToolTipText = QT_TR_NOOP(
        "Attaches a sketch to the selected geometry element");
    sWhatsThis = "Sketcher_MapSketch";
    sStatusTip = sToolTipText;
    sPixmap = "Sketcher_MapSketch";
}

void CmdSketcherMapSketch::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    QString msg_str;
    try {
        Attacher::eMapMode suggMapMode;
        std::vector<Attacher::eMapMode> validModes;

        // check that selection is valid for at least some mapping mode.
        Attacher::SuggestResult::eSuggestResult msgid = Attacher::SuggestResult::srOK;
        suggMapMode = SuggestAutoMapMode(&msgid, &msg_str, &validModes);
        bool sketchInSelection = false;
        std::vector<App::DocumentObject*> selectedSketches = Gui::Selection()
                .getObjectsOfType(Part::Part2DObject::getClassTypeId());
        App::Document* doc = App::GetApplication().getActiveDocument();
        std::vector<App::DocumentObject*> sketches =
            doc->getObjectsOfType(Part::Part2DObject::getClassTypeId());

        /** remove any sketches that are in the current selection to avoid
         *  the case where the user attaches the sketch to itself issue #17629
         *  circular dependency check happens later, but a sketch does not appear
         *  in its own outlist, so we remove it from the dialog list proactively
         *  rather than wait and generate an error after the fact.
         */
        const auto newEnd = std::ranges::remove_if(sketches,
            [&selectedSketches, &sketchInSelection](App::DocumentObject* obj) {
                if (const auto sketch = dynamic_cast<Part::Part2DObject*>(obj);
                    sketch && std::ranges::find(selectedSketches, sketch) != selectedSketches.end()) {
                    sketchInSelection = true;
                    return true;
                }
                return false;
            }).begin();
        sketches.erase(newEnd, sketches.end());

        if (sketches.empty()) {
            Gui::TranslatedUserWarning(
                doc->Label.getStrValue(),
                qApp->translate("Sketcher_MapSketch", "No sketch found"),
                sketchInSelection
                ? qApp->translate("Sketcher_MapSketch", "Cannot attach sketch to itself!")
                : qApp->translate("Sketcher_MapSketch", "The document does not contain a sketch"));

            return;
        }
        std::sort(sketches.begin(), sketches.end(), [](const auto &a, const auto &b) {
            return QString::fromUtf8(a->Label.getValue()) < QString::fromUtf8(b->Label.getValue());
        });

        bool ok;
        QStringList items;
        for (std::vector<App::DocumentObject*>::iterator it = sketches.begin();
             it != sketches.end();
             ++it)
            items.push_back(QString::fromUtf8((*it)->Label.getValue()));
        QString text = QInputDialog::getItem(
            Gui::getMainWindow(),
            qApp->translate("Sketcher_MapSketch", "Select Sketch"),
            sketchInSelection
            ? qApp->translate("Sketcher_MapSketch",
                "Select a sketch (some sketches not shown to prevent a circular dependency)")
            : qApp->translate("Sketcher_MapSketch", "Select a sketch from the list"),
            items,
            0,
            false,
            &ok,
            Qt::MSWindowsFixedSizeDialogHint);
        if (!ok)
            return;
        int index = items.indexOf(text);
        Part2DObject* sketch = static_cast<Part2DObject*>(sketches[index]);

        // check circular dependency
        std::vector<Gui::SelectionObject> selobjs = Gui::Selection().getSelectionEx();
        for (size_t i = 0; i < selobjs.size(); ++i) {
            App::DocumentObject* part = static_cast<Part::Feature*>(selobjs[i].getObject());
            if (!part) {
                assert(0);
                throw Base::ValueError(
                    "Unexpected null pointer in CmdSketcherMapSketch::activated");
            }
            if (std::vector<App::DocumentObject*> input = part->getOutListRecursive();
                std::ranges::find(input, sketch) != input.end()) {
                throw ExceptionWrongInput(
                    QT_TR_NOOP("Some of the selected objects depend on the sketch to be mapped. "
                               "Circular dependencies are not allowed."));
            }
        }

        // Ask for a new mode.
        // outline:
        //  * find out the modes that are compatible with selection.
        //  * Test if current mode is OK.
        //  * fill in the dialog
        //  * execute the dialog
        //  * collect dialog result
        //  * action

        bool bAttach = true;
        bool bCurIncompatible = false;
        // * find out the modes that are compatible with selection.
        const auto  curMapMode = eMapMode(sketch->MapMode.getValue());
        // * Test if current mode is OK.
        if (std::ranges::find(validModes, curMapMode) == validModes.end())
            bCurIncompatible = true;

        // * fill in the dialog
        validModes.insert(validModes.begin(), Attacher::mmDeactivated);
        if (bCurIncompatible)
            validModes.push_back(curMapMode);
        // bool ok; //already defined
        // QStringList items; //already defined
        items.clear();
        items.push_back(QObject::tr("Do not attach"));
        int iSugg = 0;// index of the auto-suggested mode in the list of valid modes
        int iCurr = 0;// index of current mode in the list of valid modes
        for (size_t i = 0; i < validModes.size(); ++i) {
            // Get the 2-element vector of caption, tooltip -- this class cannot use the tooltip,
            // so it is just ignored.
            auto uiStrings =
                AttacherGui::getUIStrings(AttachEnginePlane::getClassTypeId(), validModes[i]);
            items.push_back(uiStrings[0]);
            if (validModes[i] == curMapMode) {
                iCurr = items.size() - 1;
                items.back().append(
                    bCurIncompatible
                        ? qApp->translate("Sketcher_MapSketch", " (incompatible with selection)")
                        : qApp->translate("Sketcher_MapSketch", " (current)"));
            }
            if (validModes[i] == suggMapMode) {
                iSugg = items.size() - 1;
                if (iSugg == 1) {
                    iSugg = 0;// redirect deactivate to detach
                }
                else {
                    items.back().append(qApp->translate("Sketcher_MapSketch", " (suggested)"));
                }
            }
        }
        // * execute the dialog
        text = QInputDialog::getItem(
            Gui::getMainWindow(),
            qApp->translate("Sketcher_MapSketch", "Sketch Attachment"),
            bCurIncompatible
                ? qApp->translate(
                    "Sketcher_MapSketch",
                    "Current attachment mode is incompatible with the new selection.\n"
                    "Select the method to attach this sketch to selected objects.")
                : qApp->translate("Sketcher_MapSketch",
                                  "Select the method to attach this sketch to selected objects."),
            items,
            bCurIncompatible ? iSugg : iCurr,
            false,
            &ok,
            Qt::MSWindowsFixedSizeDialogHint);
        // * collect dialog result
        if (!ok)
            return;
        index = items.indexOf(text);
        if (index == 0) {
            bAttach = false;
            suggMapMode = Attacher::mmDeactivated;
        }
        else {
            bAttach = true;
            suggMapMode = validModes[index - 1];
        }

        // * action
        if (bAttach) {
            App::PropertyLinkSubList support;
            Gui::Selection().getAsPropertyLinkSubList(support);
            std::string supportString = support.getPyReprString();

            openCommand(QT_TRANSLATE_NOOP("Command", "Attach sketch"));
            Gui::cmdAppObjectArgs(
                sketch, "MapMode = \"%s\"", AttachEngine::getModeName(suggMapMode).c_str());
            Gui::cmdAppObjectArgs(sketch, "AttachmentSupport = %s", supportString.c_str());
            commitCommand();
            doCommand(Gui, "App.activeDocument().recompute()");
        }
        else {
            openCommand(QT_TRANSLATE_NOOP("Command", "Detach sketch"));
            Gui::cmdAppObjectArgs(
                sketch, "MapMode = \"%s\"", AttachEngine::getModeName(suggMapMode).c_str());
            Gui::cmdAppObjectArgs(sketch, "AttachmentSupport = None");
            commitCommand();
            doCommand(Gui, "App.activeDocument().recompute()");
        }
    }
    catch (ExceptionWrongInput& e) {
        Gui::TranslatedUserWarning(getActiveGuiDocument(),
                                   qApp->translate("Sketcher_MapSketch", "Map sketch"),
                                   qApp->translate("Sketcher_MapSketch",
                                                   "Can't map a sketch to support:\n"
                                                   "%1")
                                       .arg(e.ErrMsg.length() ? e.ErrMsg : msg_str));
    }
}

bool CmdSketcherMapSketch::isActive()
{
    App::Document* doc = App::GetApplication().getActiveDocument();
    std::vector<Gui::SelectionObject> selobjs = Gui::Selection().getSelectionEx();
    return doc && doc->countObjectsOfType<Part::Part2DObject>() > 0 && !selobjs.empty();
}

DEF_STD_CMD_A(CmdSketcherViewSketch)

CmdSketcherViewSketch::CmdSketcherViewSketch()
    : Command("Sketcher_ViewSketch")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Align View to Sketch");
    sToolTipText = QT_TR_NOOP("Aligns the camera orientation perpendicular to the active sketch plane");
    sWhatsThis = "Sketcher_ViewSketch";
    sStatusTip = sToolTipText;
    sPixmap = "Sketcher_ViewSketch";
    sAccel = "Q, P";
    eType = 0;
}

void CmdSketcherViewSketch::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    Gui::Document* doc = getActiveGuiDocument();
    SketcherGui::ViewProviderSketch* vp =
        dynamic_cast<SketcherGui::ViewProviderSketch*>(doc->getInEdit());
    if (vp) {
        runCommand(Gui,
                   "Gui.ActiveDocument.ActiveView.setCameraOrientation("
                   "App.Placement(Gui.editDocument().EditingTransform).Rotation.Q)");
    }
}

bool CmdSketcherViewSketch::isActive()
{
    return isSketchInEdit(getActiveGuiDocument());
}

DEF_STD_CMD_A(CmdSketcherValidateSketch)

CmdSketcherValidateSketch::CmdSketcherValidateSketch()
    : Command("Sketcher_ValidateSketch")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Validate Sketch");
    sToolTipText = QT_TR_NOOP("Validates a sketch by checking for missing coincidences,\n"
                              "invalid constraints, and degenerate geometry");
    sWhatsThis = "Sketcher_ValidateSketch";
    sStatusTip = sToolTipText;
    eType = 0;
    sPixmap = "Sketcher_ValidateSketch";
}

void CmdSketcherValidateSketch::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    std::vector<Gui::SelectionObject> selection =
        getSelection().getSelectionEx(nullptr, Sketcher::SketchObject::getClassTypeId());
    if (selection.size() != 1) {
        Gui::TranslatedUserWarning(
            getActiveGuiDocument(),
            qApp->translate("CmdSketcherValidateSketch", "Wrong selection"),
            qApp->translate("CmdSketcherValidateSketch", "Select only 1 sketch."));
        return;
    }

    Sketcher::SketchObject* Obj = static_cast<Sketcher::SketchObject*>(selection[0].getObject());
    Gui::Control().showDialog(new TaskSketcherValidation(Obj));
}

bool CmdSketcherValidateSketch::isActive()
{
    if (Gui::Control().activeDialog())
        return false;
    return Gui::Selection().countObjectsOfType<Sketcher::SketchObject>() == 1;
}

DEF_STD_CMD_A(CmdSketcherMirrorSketch)

CmdSketcherMirrorSketch::CmdSketcherMirrorSketch()
    : Command("Sketcher_MirrorSketch")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Mirror Sketch");
    sToolTipText = QT_TR_NOOP("Creates a new mirrored sketch for each selected sketch\n"
                              "by using the X or Y axes, or the origin point,\n"
                              "as mirroring reference");
    sWhatsThis = "Sketcher_MirrorSketch";
    sStatusTip = sToolTipText;
    eType = 0;
    sPixmap = "Sketcher_MirrorSketch";
}

void CmdSketcherMirrorSketch::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    std::vector<Gui::SelectionObject> selection =
        getSelection().getSelectionEx(nullptr, Sketcher::SketchObject::getClassTypeId());
    if (selection.empty()) {
        Gui::TranslatedUserWarning(
            getActiveGuiDocument(),
            qApp->translate("CmdSketcherMirrorSketch", "Wrong selection"),
            qApp->translate("CmdSketcherMirrorSketch", "Select at least 1 sketch"));
        return;
    }

    int refgeoid = -1;
    Sketcher::PointPos refposid = Sketcher::PointPos::none;
    // Ask the user the type of mirroring
    SketchMirrorDialog smd;
    if (smd.exec() != QDialog::Accepted)
        return;

    refgeoid = smd.RefGeoid;
    refposid = smd.RefPosid;

    App::Document* doc = App::GetApplication().getActiveDocument();
    openCommand(QT_TRANSLATE_NOOP("Command", "Create a mirrored sketch for each selected sketch"));

    for (std::vector<Gui::SelectionObject>::const_iterator it = selection.begin();
         it != selection.end();
         ++it) {
        // create Sketch
        std::string FeatName = getUniqueObjectName("MirroredSketch");
        doCommand(Doc,
                  "App.activeDocument().addObject('Sketcher::SketchObject', '%s')",
                  FeatName.c_str());
        Sketcher::SketchObject* mirrorsketch =
            static_cast<Sketcher::SketchObject*>(doc->getObject(FeatName.c_str()));

        const Sketcher::SketchObject* Obj =
            static_cast<const Sketcher::SketchObject*>((*it).getObject());
        Base::Placement pl = Obj->Placement.getValue();
        Base::Vector3d p = pl.getPosition();
        Base::Rotation r = pl.getRotation();

        doCommand(Doc,
                  "App.activeDocument().%s.Placement = App.Placement(App.Vector(%f, %f, %f), "
                  "App.Rotation(%f, %f, %f, %f))",
                  FeatName.c_str(),
                  p.x,
                  p.y,
                  p.z,
                  r[0],
                  r[1],
                  r[2],
                  r[3]);

        Sketcher::SketchObject* tempsketch = new Sketcher::SketchObject();
        int addedGeometries = tempsketch->addGeometry(Obj->getInternalGeometry());
        int addedConstraints = tempsketch->addConstraints(Obj->Constraints.getValues());

        std::vector<int> geoIdList;

        for (int i = 0; i <= addedGeometries; i++)
            geoIdList.push_back(i);

        tempsketch->addSymmetric(geoIdList, refgeoid, refposid);

        std::vector<Part::Geometry*> tempgeo = tempsketch->getInternalGeometry();
        std::vector<Sketcher::Constraint*> tempconstr = tempsketch->Constraints.getValues();

        // If value of addedGeometries or addedConstraints is -1, it gets added to vector begin
        // iterator and that is invalid
        std::vector<Part::Geometry*> mirrorgeo(tempgeo.begin() + (addedGeometries + 1),
                                               tempgeo.end());
        std::vector<Sketcher::Constraint*> mirrorconstr(tempconstr.begin() + (addedConstraints + 1),
                                                        tempconstr.end());

        for (std::vector<Sketcher::Constraint*>::const_iterator itc = mirrorconstr.begin();
             itc != mirrorconstr.end();
             ++itc) {

            if ((*itc)->First != Sketcher::GeoEnum::GeoUndef
                || (*itc)->First == Sketcher::GeoEnum::HAxis
                || (*itc)->First == Sketcher::GeoEnum::VAxis)
                // not x, y axes or origin
                (*itc)->First -= (addedGeometries + 1);
            if ((*itc)->Second != Sketcher::GeoEnum::GeoUndef
                || (*itc)->Second == Sketcher::GeoEnum::HAxis
                || (*itc)->Second == Sketcher::GeoEnum::VAxis)
                // not x, y axes or origin
                (*itc)->Second -= (addedGeometries + 1);
            if ((*itc)->Third != Sketcher::GeoEnum::GeoUndef
                || (*itc)->Third == Sketcher::GeoEnum::HAxis
                || (*itc)->Third == Sketcher::GeoEnum::VAxis)
                // not x, y axes or origin
                (*itc)->Third -= (addedGeometries + 1);
        }

        mirrorsketch->addGeometry(mirrorgeo);
        mirrorsketch->addConstraints(mirrorconstr);
        delete tempsketch;
    }

    doCommand(Gui, "App.activeDocument().recompute()");
}

bool CmdSketcherMirrorSketch::isActive()
{
    return Gui::Selection().countObjectsOfType<Sketcher::SketchObject>() > 0;
}

DEF_STD_CMD_A(CmdSketcherMergeSketches)

CmdSketcherMergeSketches::CmdSketcherMergeSketches()
    : Command("Sketcher_MergeSketches")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Merge Sketches");
    sToolTipText = QT_TR_NOOP("Creates a new sketch by merging at least 2 selected sketches");
    sWhatsThis = "Sketcher_MergeSketches";
    sStatusTip = sToolTipText;
    eType = 0;
    sPixmap = "Sketcher_MergeSketch";
}

void CmdSketcherMergeSketches::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    std::vector<Gui::SelectionObject> selection =
        getSelection().getSelectionEx(nullptr, Sketcher::SketchObject::getClassTypeId());
    if (selection.size() < 2) {
        Gui::TranslatedUserWarning(
            getActiveGuiDocument(),
            qApp->translate("CmdSketcherMergeSketches", "Wrong selection"),
            qApp->translate("CmdSketcherMergeSketches", "Select at least 2 sketches"));
        return;
    }

    App::Document* doc = App::GetApplication().getActiveDocument();

    // create Sketch
    std::string FeatName = getUniqueObjectName("Sketch");

    openCommand(QT_TRANSLATE_NOOP("Command", "Merge sketches"));
    doCommand(
        Doc, "App.activeDocument().addObject('Sketcher::SketchObject', '%s')", FeatName.c_str());

    Sketcher::SketchObject* mergesketch =
        static_cast<Sketcher::SketchObject*>(doc->getObject(FeatName.c_str()));

    int baseGeometry = 0;
    int baseConstraints = 0;

    for (std::vector<Gui::SelectionObject>::const_iterator it = selection.begin();
         it != selection.end();
         ++it) {
        const Sketcher::SketchObject* Obj =
            static_cast<const Sketcher::SketchObject*>((*it).getObject());
        int addedGeometries = mergesketch->addGeometry(Obj->getInternalGeometry());

        int addedConstraints = mergesketch->addCopyOfConstraints(*Obj);

        // Coverity issue 513796: make sure the loop below can complete
        if (addedConstraints < 0) {
            continue;  // There were no constraints
        }
        if (addedConstraints - baseConstraints < 0) {
            throw Base::ValueError("Constraint error in CmdSketcherMergeSketches");
        }

        for (int i = 0; i <= (addedConstraints - baseConstraints); i++) {
            Sketcher::Constraint* constraint =
                mergesketch->Constraints.getValues()[i + baseConstraints];

            if (constraint->First != Sketcher::GeoEnum::GeoUndef
                && constraint->First != Sketcher::GeoEnum::HAxis
                && constraint->First != Sketcher::GeoEnum::VAxis)
                // not x, y axes or origin
                constraint->First += baseGeometry;
            if (constraint->Second != Sketcher::GeoEnum::GeoUndef
                && constraint->Second != Sketcher::GeoEnum::HAxis
                && constraint->Second != Sketcher::GeoEnum::VAxis)
                // not x, y axes or origin
                constraint->Second += baseGeometry;
            if (constraint->Third != Sketcher::GeoEnum::GeoUndef
                && constraint->Third != Sketcher::GeoEnum::HAxis
                && constraint->Third != Sketcher::GeoEnum::VAxis)
                // not x, y axes or origin
                constraint->Third += baseGeometry;
        }

        baseGeometry = addedGeometries + 1;
        baseConstraints = addedConstraints + 1;
    }

    // apply the placement of the first sketch in the list (#0002434)
    doCommand(Doc,
              "App.activeDocument().ActiveObject.Placement = App.activeDocument().%s.Placement",
              selection.front().getFeatName());
    doCommand(Doc, "App.activeDocument().recompute()");
}

bool CmdSketcherMergeSketches::isActive()
{
    return Gui::Selection().countObjectsOfType<Sketcher::SketchObject>() > 1;
}

// Acknowledgement of idea and original python macro goes to SpritKopf:
// https://github.com/Spritkopf/freecad-macros/blob/master/clip-sketch/clip_sketch.FCMacro
// https://forum.freecad.org/viewtopic.php?p=231481#p231085
DEF_STD_CMD_A(CmdSketcherViewSection)

CmdSketcherViewSection::CmdSketcherViewSection()
    : Command("Sketcher_ViewSection")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Toggle Section View");
    sToolTipText = QT_TR_NOOP("Toggles between section view and full view");
    sWhatsThis = "Sketcher_ViewSection";
    sStatusTip = sToolTipText;
    sPixmap = "Sketcher_ViewSection";
    sAccel = "Q, S";
    eType = 0;
}

void CmdSketcherViewSection::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    QString cmdStr =
        QLatin1String("ActiveSketch.ViewObject.TempoVis.sketchClipPlane(ActiveSketch, None, %1)\n");
    Gui::Document* doc = getActiveGuiDocument();
    bool revert = false;
    if (doc) {
        SketcherGui::ViewProviderSketch* vp =
            dynamic_cast<SketcherGui::ViewProviderSketch*>(doc->getInEdit());
        if (vp) {
            revert = vp->getViewOrientationFactor() < 0 ? true : false;
        }
    }
    cmdStr = cmdStr.arg(revert ? QLatin1String("True") : QLatin1String("False"));
    doCommand(Doc, cmdStr.toLatin1());
}

bool CmdSketcherViewSection::isActive()
{
    return isSketchInEdit(getActiveGuiDocument());
}

/* Grid tool */
GridSpaceAction::GridSpaceAction(QObject* parent)
    : QWidgetAction(parent)
{
    setEnabled(false);
}

void GridSpaceAction::updateWidget()
{
    auto* sketchView = getView();

    if (sketchView) {

        auto updateCheckBox = [](QCheckBox* checkbox, bool value) {
            auto checked = checkbox->checkState() == Qt::Checked;

            if (value != checked) {
                const QSignalBlocker blocker(checkbox);
                checkbox->setChecked(value);
            }
        };

        auto updateCheckBoxFromProperty = [updateCheckBox](QCheckBox* checkbox,
                                                            App::PropertyBool& property) {
            auto propvalue = property.getValue();

            updateCheckBox(checkbox, propvalue);
        };

        updateCheckBoxFromProperty(gridShow, sketchView->ShowGrid);

        updateCheckBoxFromProperty(gridAutoSpacing, sketchView->GridAuto);

        ParameterGrp::handle hGrp = getParameterPath();
        updateCheckBox(snapToGrid, hGrp->GetBool("SnapToGrid", false));

        gridSizeBox->setValue(sketchView->GridSize.getValue());
    }
}

void GridSpaceAction::languageChange()
{
    gridShow->setText(tr("Display grid"));
    gridShow->setToolTip(tr("Toggles the visibility of the grid in the active sketch"));
    gridShow->setStatusTip(gridAutoSpacing->toolTip());

    gridAutoSpacing->setText(tr("Grid auto-spacing"));
    gridAutoSpacing->setToolTip(tr("Automatically adjusts the grid spacing based on the zoom level"));
    gridAutoSpacing->setStatusTip(gridAutoSpacing->toolTip());

    sizeLabel->setText(tr("Spacing"));
    gridSizeBox->setToolTip(tr("Distance between two subsequent grid lines"));

    snapToGrid->setText(tr("Snap to grid"));
    snapToGrid->setToolTip(
        tr("New points will snap to the nearest grid line.\nPoints must be set closer than a "
            "fifth of the grid spacing to a grid line to snap."));
    snapToGrid->setStatusTip(snapToGrid->toolTip());
}

QWidget* GridSpaceAction::createWidget(QWidget* parent)
{
    gridShow = new QCheckBox();

    gridAutoSpacing = new QCheckBox();

    snapToGrid = new QCheckBox();

    sizeLabel = new QLabel();

    gridSizeBox = new Gui::QuantitySpinBox();
    gridSizeBox->setProperty("unit", QVariant(QStringLiteral("mm")));
    gridSizeBox->setObjectName(QStringLiteral("gridSize"));
    gridSizeBox->setMaximum(99999999.0);
    gridSizeBox->setMinimum(0.001);

    QWidget* gridSizeW = new QWidget(parent);
    auto* layout = new QGridLayout(gridSizeW);
    layout->addWidget(gridShow, 0, 0, 1, 2);
    layout->addWidget(gridAutoSpacing, 1, 0, 1, 2);
    layout->addWidget(snapToGrid, 2, 0, 1, 2);
    layout->addWidget(sizeLabel, 3, 0);
    layout->addWidget(gridSizeBox, 3, 1);

    languageChange();

#if QT_VERSION >= QT_VERSION_CHECK(6,7,0)
    QObject::connect(gridShow, &QCheckBox::checkStateChanged, [this](int state) {
#else
    QObject::connect(gridShow, &QCheckBox::stateChanged, [this](int state) {
#endif
        auto* sketchView = getView();

        if (sketchView) {
            auto enable = (state == Qt::Checked);
            sketchView->ShowGrid.setValue(enable);
        }
    });

#if QT_VERSION >= QT_VERSION_CHECK(6,7,0)
    QObject::connect(gridAutoSpacing, &QCheckBox::checkStateChanged, [this](int state) {
#else
    QObject::connect(gridAutoSpacing, &QCheckBox::stateChanged, [this](int state) {
#endif
        auto* sketchView = getView();

        if (sketchView) {
            auto enable = (state == Qt::Checked);
            sketchView->GridAuto.setValue(enable);
        }
    });

#if QT_VERSION >= QT_VERSION_CHECK(6,7,0)
    QObject::connect(snapToGrid, &QCheckBox::checkStateChanged, [this](int state) {
#else
    QObject::connect(snapToGrid, &QCheckBox::stateChanged, [this](int state) {
#endif
        ParameterGrp::handle hGrp = this->getParameterPath();
        hGrp->SetBool("SnapToGrid", state == Qt::Checked);
    });

    QObject::connect(gridSizeBox,
                        qOverload<double>(&Gui::QuantitySpinBox::valueChanged),
                        [this](double val) {
                            auto* sketchView = getView();
                            if (sketchView) {
                                sketchView->GridSize.setValue(val);
                            }
                        });

    return gridSizeW;
}

ViewProviderSketch* GridSpaceAction::getView()
{
    Gui::Document* doc = Gui::Application::Instance->activeDocument();

    if (doc) {
        return dynamic_cast<SketcherGui::ViewProviderSketch*>(doc->getInEdit());
    }

    return nullptr;
}

ParameterGrp::handle GridSpaceAction::getParameterPath()
    {
        return App::GetApplication().GetParameterGroupByPath(
            "User parameter:BaseApp/Preferences/Mod/Sketcher/Snap");
    }


class CmdSketcherGrid: public Gui::Command
{
public:
    CmdSketcherGrid();
    ~CmdSketcherGrid() override
    {}
    const char* className() const override
    {
        return "CmdSketcherGrid";
    }
    void languageChange() override;

protected:
    void activated(int iMsg) override;
    bool isActive() override;
    Gui::Action* createAction() override;

public:
    CmdSketcherGrid(const CmdSketcherGrid&) = delete;
    CmdSketcherGrid(CmdSketcherGrid&&) = delete;
    CmdSketcherGrid& operator=(const CmdSketcherGrid&) = delete;
    CmdSketcherGrid& operator=(CmdSketcherGrid&&) = delete;
};

CmdSketcherGrid::CmdSketcherGrid()
    : Command("Sketcher_Grid")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Toggle Grid");
    sToolTipText =
        QT_TR_NOOP("Toggles the grid display in the active sketch");
    sWhatsThis = "Sketcher_Grid";
    sStatusTip = sToolTipText;
    eType = 0;
}

void CmdSketcherGrid::activated(int iMsg)
{
    Q_UNUSED(iMsg);

    Gui::Document* doc = getActiveGuiDocument();
    assert(doc);
    auto* sketchView = dynamic_cast<SketcherGui::ViewProviderSketch*>(doc->getInEdit());
    assert(sketchView);

    auto value = sketchView->ShowGrid.getValue();
    sketchView->ShowGrid.setValue(!value);
}

Gui::Action* CmdSketcherGrid::createAction()
{
    auto* pcAction = new Gui::ActionGroup(this, Gui::getMainWindow());
    pcAction->setDropDownMenu(true);
    pcAction->setExclusive(false);
    applyCommandData(this->className(), pcAction);

    GridSpaceAction* gsa = new GridSpaceAction(pcAction);
    pcAction->addAction(gsa);

    _pcAction = pcAction;

    QObject::connect(pcAction, &Gui::ActionGroup::aboutToShow, [gsa](QMenu* menu) {
        Q_UNUSED(menu)
        gsa->updateWidget();
    });

    return pcAction;
}

void CmdSketcherGrid::languageChange()
{
    Command::languageChange();

    if (!_pcAction)
        return;

    Gui::ActionGroup* pcAction = qobject_cast<Gui::ActionGroup*>(_pcAction);
    QList<QAction*> a = pcAction->actions();

    auto* gsa = static_cast<GridSpaceAction*>(a[0]);
    gsa->languageChange();
}

bool CmdSketcherGrid::isActive()
{
    auto* vp = getInactiveHandlerEditModeSketchViewProvider();

    if (vp) {
        return true;
    }

    return false;
}

/* Snap tool */
SnapSpaceAction::SnapSpaceAction(QObject* parent)
        : QWidgetAction(parent)
    {
        setEnabled(false);
    }

void SnapSpaceAction::updateWidget(bool snapenabled)
{

    auto updateCheckBox = [](QCheckBox* checkbox, bool value) {
        auto checked = checkbox->checkState() == Qt::Checked;

        if (value != checked) {
            const QSignalBlocker blocker(checkbox);
            checkbox->setChecked(value);
        }
    };

    auto updateSpinBox = [](Gui::QuantitySpinBox* spinbox, double value) {
        auto currentvalue = spinbox->rawValue();

        if (currentvalue != value) {
            const QSignalBlocker blocker(spinbox);
            spinbox->setValue(value);
        }
    };

    ParameterGrp::handle hGrp = getParameterPath();

    updateCheckBox(snapToObjects, hGrp->GetBool("SnapToObjects", true));

    updateSpinBox(snapAngle, hGrp->GetFloat("SnapAngle", 5.0));

    snapToObjects->setEnabled(snapenabled);
    angleLabel->setEnabled(snapenabled);
    snapAngle->setEnabled(snapenabled);
}

void SnapSpaceAction::languageChange()
{
    snapToObjects->setText(tr("Snap to objects"));
    snapToObjects->setToolTip(tr("New points will snap to the currently preselected object. It "
                                    "will also snap to the middle of lines and arcs."));
    snapToObjects->setStatusTip(snapToObjects->toolTip());

    angleLabel->setText(tr("Snap angle"));
    snapAngle->setToolTip(
        tr("Angular step for tools that use 'Snap at angle'. Hold Ctrl to "
            "enable 'Snap at angle'. The angle starts from the positive X axis of the sketch."));
}

QWidget* SnapSpaceAction::createWidget(QWidget* parent)
{
    snapToObjects = new QCheckBox();

    angleLabel = new QLabel();

    snapAngle = new Gui::QuantitySpinBox();
    snapAngle->setProperty("unit", QVariant(QStringLiteral("deg")));
    snapAngle->setObjectName(QStringLiteral("snapAngle"));
    snapAngle->setMaximum(99999999.0);
    snapAngle->setMinimum(0);

    QWidget* snapW = new QWidget(parent);
    auto* layout = new QGridLayout(snapW);
    layout->addWidget(snapToObjects, 0, 0, 1, 2);
    layout->addWidget(angleLabel, 1, 0);
    layout->addWidget(snapAngle, 1, 1);

    languageChange();

#if QT_VERSION >= QT_VERSION_CHECK(6,7,0)
    QObject::connect(snapToObjects, &QCheckBox::checkStateChanged, [this](int state) {
#else
    QObject::connect(snapToObjects, &QCheckBox::stateChanged, [this](int state) {
#endif
        ParameterGrp::handle hGrp = this->getParameterPath();
        hGrp->SetBool("SnapToObjects", state == Qt::Checked);
    });

    QObject::connect(
        snapAngle, qOverload<double>(&Gui::QuantitySpinBox::valueChanged), [this](double val) {
            ParameterGrp::handle hGrp = this->getParameterPath();
            hGrp->SetFloat("SnapAngle", val);
        });

    return snapW;
}

ParameterGrp::handle SnapSpaceAction::getParameterPath()
{
    return App::GetApplication().GetParameterGroupByPath(
        "User parameter:BaseApp/Preferences/Mod/Sketcher/Snap");
}


class CmdSketcherSnap: public Gui::Command, public ParameterGrp::ObserverType
{
public:
    CmdSketcherSnap();
    ~CmdSketcherSnap() override;
    CmdSketcherSnap(const CmdSketcherSnap&) = delete;
    CmdSketcherSnap(CmdSketcherSnap&&) = delete;
    CmdSketcherSnap& operator=(const CmdSketcherSnap&) = delete;
    CmdSketcherSnap& operator=(CmdSketcherSnap&&) = delete;
    const char* className() const override
    {
        return "CmdSketcherSnap";
    }
    void languageChange() override;

    void OnChange(Base::Subject<const char*>& rCaller, const char* sReason) override;

protected:
    void activated(int iMsg) override;
    bool isActive() override;
    Gui::Action* createAction() override;

private:
    ParameterGrp::handle getParameterPath()
    {
        return App::GetApplication().GetParameterGroupByPath(
            "User parameter:BaseApp/Preferences/Mod/Sketcher/Snap");
    }

    bool snapEnabled = true;
};

CmdSketcherSnap::CmdSketcherSnap()
    : Command("Sketcher_Snap")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Toggle Snap");
    sToolTipText =
        QT_TR_NOOP("Toggles snapping");
    sWhatsThis = "Sketcher_Snap";
    sStatusTip = sToolTipText;
    eType = 0;

    ParameterGrp::handle hGrp = this->getParameterPath();
    hGrp->Attach(this);
}

CmdSketcherSnap::~CmdSketcherSnap()
{

    ParameterGrp::handle hGrp = this->getParameterPath();
    hGrp->Detach(this);
}

void CmdSketcherSnap::OnChange(Base::Subject<const char*>& rCaller, const char* sReason)
{
    Q_UNUSED(rCaller)

    if (strcmp(sReason, "Snap") == 0) {
        snapEnabled = getParameterPath()->GetBool("Snap", true);
    }
}

void CmdSketcherSnap::activated(int iMsg)
{
    Q_UNUSED(iMsg);

    getParameterPath()->SetBool("Snap", !snapEnabled);

    // Update the widget :
    if (!_pcAction)
        return;

    Gui::ActionGroup* pcAction = qobject_cast<Gui::ActionGroup*>(_pcAction);
    QList<QAction*> a = pcAction->actions();

    auto* ssa = static_cast<SnapSpaceAction*>(a[0]);
    ssa->updateWidget(snapEnabled);
}

Gui::Action* CmdSketcherSnap::createAction()
{
    auto* pcAction = new Gui::ActionGroup(this, Gui::getMainWindow());
    pcAction->setDropDownMenu(true);
    pcAction->setExclusive(false);
    applyCommandData(this->className(), pcAction);

    SnapSpaceAction* ssa = new SnapSpaceAction(pcAction);
    pcAction->addAction(ssa);

    _pcAction = pcAction;

    QObject::connect(pcAction, &Gui::ActionGroup::aboutToShow, [ssa, this](QMenu* menu) {
        Q_UNUSED(menu)
        ssa->updateWidget(snapEnabled);
    });

    return pcAction;
}

void CmdSketcherSnap::languageChange()
{
    Command::languageChange();

    if (!_pcAction)
        return;

    Gui::ActionGroup* pcAction = qobject_cast<Gui::ActionGroup*>(_pcAction);
    QList<QAction*> a = pcAction->actions();

    auto* ssa = static_cast<SnapSpaceAction*>(a[0]);
    ssa->languageChange();
}

bool CmdSketcherSnap::isActive()
{
    auto* vp = getInactiveHandlerEditModeSketchViewProvider();

    if (vp) {
        return true;
    }

    return false;
}


/* Rendering Order */
RenderingOrderAction::RenderingOrderAction(QObject* parent)
        : QWidgetAction(parent)
{
    setEnabled(false);
}

void RenderingOrderAction::updateWidget()
{
    auto hGrp = getParameterPath();

    // 1->Normal Geometry, 2->Construction, 3->External
    int topid = hGrp->GetInt("TopRenderGeometryId", 1);
    int midid = hGrp->GetInt("MidRenderGeometryId", 2);
    int lowid = hGrp->GetInt("LowRenderGeometryId", 3);

    auto idToText = [](int id) -> QString {
        switch (id) {
        case 1:
            return tr("Normal geometry");
        case 2:
            return tr("Construction geometry");
        case 3:
            return tr("External geometry");
        default:
            // Fallback for an unexpected ID
            return tr("Unknown geometry");
        }
    };

    {
        QSignalBlocker block(this);
        list->clear();

        QListWidgetItem* itemTop = new QListWidgetItem;
        itemTop->setData(Qt::UserRole, QVariant(topid));
        itemTop->setText(idToText(topid));
        list->insertItem(0, itemTop);

        QListWidgetItem* itemMid = new QListWidgetItem;
        itemMid->setData(Qt::UserRole, QVariant(midid));
        itemMid->setText(idToText(midid));
        list->insertItem(1, itemMid);

        QListWidgetItem* itemLow = new QListWidgetItem;
        itemLow->setData(Qt::UserRole, QVariant(lowid));
        itemLow->setText(idToText(lowid));
        list->insertItem(2, itemLow);
    }
}

void RenderingOrderAction::languageChange()
{
    updateWidget();
}

QWidget* RenderingOrderAction::createWidget(QWidget* parent)
{
    list = new QListWidget();
    list->setDragDropMode(QAbstractItemView::InternalMove);
    list->setDefaultDropAction(Qt::MoveAction);
    list->setSelectionMode(QAbstractItemView::SingleSelection);
    list->setDragEnabled(true);
    list->setFixedSize(200, 50);


    QWidget* renderingWidget = new QWidget(parent);
    auto* label = new QLabel(tr("Rendering order"), renderingWidget);
    auto* layout = new QVBoxLayout(renderingWidget);
    layout->addWidget(label);
    layout->addWidget(list);

    languageChange();

    // Handle change in the order of the list entries
    QObject::connect(list->model(),
                        &QAbstractItemModel::rowsMoved,
                        [this](const QModelIndex& sourceParent,
                            int sourceStart,
                            int sourceEnd,
                            const QModelIndex& destinationParent,
                            int destinationRow) {
                            Q_UNUSED(sourceParent)
                            Q_UNUSED(sourceStart)
                            Q_UNUSED(sourceEnd)
                            Q_UNUSED(destinationParent)
                            Q_UNUSED(destinationRow)

                            int topid = list->item(0)->data(Qt::UserRole).toInt();
                            int midid = list->item(1)->data(Qt::UserRole).toInt();
                            int lowid = list->item(2)->data(Qt::UserRole).toInt();

                            auto hGrp = getParameterPath();

                            hGrp->SetInt("TopRenderGeometryId", topid);
                            hGrp->SetInt("MidRenderGeometryId", midid);
                            hGrp->SetInt("LowRenderGeometryId", lowid);
                        });

    return renderingWidget;
}

ParameterGrp::handle RenderingOrderAction::getParameterPath()
    {
        return App::GetApplication().GetParameterGroupByPath(
            "User parameter:BaseApp/Preferences/Mod/Sketcher/General");
    }


class CmdRenderingOrder: public Gui::Command, public ParameterGrp::ObserverType
{
    enum class ElementType
    {
        Normal = 1,
        Construction = 2,
        External = 3,
    };

public:
    CmdRenderingOrder();
    ~CmdRenderingOrder() override;
    CmdRenderingOrder(const CmdRenderingOrder&) = delete;
    CmdRenderingOrder(CmdRenderingOrder&&) = delete;
    CmdRenderingOrder& operator=(const CmdRenderingOrder&) = delete;
    CmdRenderingOrder& operator=(CmdRenderingOrder&&) = delete;
    const char* className() const override
    {
        return "CmdRenderingOrder";
    }
    void languageChange() override;
    void OnChange(Base::Subject<const char*>& rCaller, const char* sReason) override;

protected:
    void activated(int iMsg) override;
    bool isActive() override;
    Gui::Action* createAction() override;

private:
    ParameterGrp::handle getParameterPath()
    {
        return App::GetApplication().GetParameterGroupByPath(
            "User parameter:BaseApp/Preferences/Mod/Sketcher/General");
    }

    ElementType TopElement = ElementType::Normal;
};

CmdRenderingOrder::CmdRenderingOrder()
    : Command("Sketcher_RenderingOrder")
{
    sAppModule = "Sketcher";
    sGroup = "Sketcher";
    sMenuText = QT_TR_NOOP("Rendering Order");
    sToolTipText = QT_TR_NOOP("Reorders items in the rendering order");
    sWhatsThis = "Sketcher_RenderingOrder";
    sStatusTip = sToolTipText;
    eType = 0;

    ParameterGrp::handle hGrp = this->getParameterPath();
    hGrp->Attach(this);

    TopElement = static_cast<ElementType>(getParameterPath()->GetInt("TopRenderGeometryId", 1));
}

CmdRenderingOrder::~CmdRenderingOrder()
{

    ParameterGrp::handle hGrp = this->getParameterPath();
    hGrp->Detach(this);
}

void CmdRenderingOrder::OnChange(Base::Subject<const char*>& rCaller, const char* sReason)
{
    Q_UNUSED(rCaller)

    if (strcmp(sReason, "TopRenderGeometryId") == 0) {
        TopElement = static_cast<ElementType>(getParameterPath()->GetInt("TopRenderGeometryId", 1));
    }
}

void CmdRenderingOrder::activated(int iMsg)
{
    Q_UNUSED(iMsg);
}

Gui::Action* CmdRenderingOrder::createAction()
{
    auto* pcAction = new Gui::ActionGroup(this, Gui::getMainWindow());
    pcAction->setDropDownMenu(true);
    pcAction->setExclusive(false);
    applyCommandData(this->className(), pcAction);

    RenderingOrderAction* roa = new RenderingOrderAction(pcAction);
    pcAction->addAction(roa);

    _pcAction = pcAction;

    QObject::connect(pcAction, &Gui::ActionGroup::aboutToShow, [roa](QMenu* menu) {
        Q_UNUSED(menu)
        roa->updateWidget();
    });

    return pcAction;
}

void CmdRenderingOrder::languageChange()
{
    Command::languageChange();

    if (!_pcAction)
        return;

    Gui::ActionGroup* pcAction = qobject_cast<Gui::ActionGroup*>(_pcAction);
    QList<QAction*> a = pcAction->actions();

    auto* roa = static_cast<RenderingOrderAction*>(a[0]);
    roa->languageChange();
}

bool CmdRenderingOrder::isActive()
{
    return isSketchInEdit(getActiveGuiDocument());
    ;
}


void CreateSketcherCommands()
{
    Gui::CommandManager& rcCmdMgr = Gui::Application::Instance->commandManager();

    rcCmdMgr.addCommand(new CmdSketcherNewSketch());
    rcCmdMgr.addCommand(new CmdSketcherEditSketch());
    rcCmdMgr.addCommand(new CmdSketcherLeaveSketch());
    rcCmdMgr.addCommand(new CmdSketcherStopOperation());
    rcCmdMgr.addCommand(new CmdSketcherReorientSketch());
    rcCmdMgr.addCommand(new CmdSketcherMapSketch());
    rcCmdMgr.addCommand(new CmdSketcherViewSketch());
    rcCmdMgr.addCommand(new CmdSketcherValidateSketch());
    rcCmdMgr.addCommand(new CmdSketcherMirrorSketch());
    rcCmdMgr.addCommand(new CmdSketcherMergeSketches());
    rcCmdMgr.addCommand(new CmdSketcherViewSection());
    rcCmdMgr.addCommand(new CmdSketcherGrid());
    rcCmdMgr.addCommand(new CmdSketcherSnap());
    rcCmdMgr.addCommand(new CmdRenderingOrder());
}
// clang-format on
