/**************************************************************************
 *   Copyright (c) 2022 Werner Mayer <wmayer[at]users.sourceforge.net>     *
 *                                                                         *
 *   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 <TopoDS.hxx>
#include <TopoDS_Face.hxx>
#include <boost/signals2.hpp>
#include <map>
#include <string>
#include <vector>
#include <QMessageBox>


#include "SketchWorkflow.h"
#include "DlgActiveBody.h"
#include "TaskFeaturePick.h"
#include "Utils.h"
#include "ViewProviderBody.h"
#include "WorkflowManager.h"
#include "ui_DlgReference.h"
#include <Mod/PartDesign/App/Body.h>
#include <Mod/PartDesign/App/DatumPlane.h>
#include <Mod/PartDesign/App/ShapeBinder.h>
#include <Mod/Part/App/Attacher.h>
#include <Mod/Part/App/TopoShape.h>
#include <Mod/Sketcher/Gui/ViewProviderSketch.h>

#include <App/Document.h>
#include <App/Link.h>
#include <App/Origin.h>
#include <App/Datums.h>
#include <App/Part.h>
#include <Gui/Application.h>
#include <Gui/Command.h>
#include <Gui/Control.h>
#include <Gui/Document.h>
#include <Gui/MainWindow.h>
#include <Gui/ViewParams.h>
#include <Gui/ViewProviderPlane.h>
#include <Gui/Selection/SelectionFilter.h>

using namespace PartDesignGui;

namespace
{
struct RejectException
{
};

struct WrongSelectionException
{
};

struct WrongSupportException
{
};

struct SupportNotPlanarException
{
};

struct MissingPlanesException
{
};

class SupportFaceValidator
{
public:
    explicit SupportFaceValidator(Gui::SelectionObject faceSelection)
        : faceSelection(faceSelection)
    {}

    void handleSelectedBody(PartDesign::Body* activeBody)
    {
        App::DocumentObject* object = faceSelection.getObject();
        std::vector<std::string> elements = faceSelection.getSubNames();

        // In case the selected face belongs to the body then it means its
        // Display Mode Body is set to Tip. But the body face is not allowed
        // to be used as support because otherwise it would cause a cyclic
        // dependency. So, instead we use the tip object as reference.
        // https://forum.freecad.org/viewtopic.php?f=3&t=37448
        if (object == activeBody) {
            App::DocumentObject* tip = activeBody->Tip.getValue();
            if (tip && tip->isDerivedFrom<Part::Feature>() && elements.size() == 1) {
                Gui::SelectionChanges msg;
                msg.pDocName = faceSelection.getDocName();
                msg.pObjectName = tip->getNameInDocument();
                msg.pSubName = elements[0].c_str();
                msg.pTypeName = tip->getTypeId().getName();

                faceSelection = Gui::SelectionObject {msg};

                // automatically switch to 'Through' mode
                setThroughModeOfBody(activeBody);
            }
        }
    }

    void throwIfInvalid()
    {
        App::DocumentObject* object = faceSelection.getObject();
        std::vector<std::string> elements = faceSelection.getSubNames();

        Part::Feature* partobject = dynamic_cast<Part::Feature*>(object);
        if (!partobject) {
            throw WrongSelectionException();
        }

        if (elements.size() != 1) {
            throw WrongSelectionException();
        }

        // get the selected sub shape (a Face)
        const Part::TopoShape& shape = partobject->Shape.getValue();
        Part::TopoShape subshape(shape.getSubShape(elements[0].c_str()));
        if (subshape.isNull()) {
            throw WrongSupportException();
        }

        if (!subshape.isPlanar(Attacher::AttachEnginePlane::planarPrecision())) {
            throw SupportNotPlanarException();
        }
    }

    std::string getSupport() const
    {
        return faceSelection.getAsPropertyLinkSubString();
    }

    App::DocumentObject* getObject() const
    {
        return faceSelection.getObject();
    }

private:
    void setThroughModeOfBody(PartDesign::Body* activeBody)
    {
        // automatically switch to 'Through' mode
        PartDesignGui::ViewProviderBody* vpBody = dynamic_cast<PartDesignGui::ViewProviderBody*>(
            Gui::Application::Instance->getViewProvider(activeBody)
        );
        if (vpBody) {
            vpBody->DisplayModeBody.setValue("Through");
        }
    }

private:
    mutable Gui::SelectionObject faceSelection;
};

class SupportPlaneValidator
{
public:
    explicit SupportPlaneValidator(Gui::SelectionObject faceSelection)
        : faceSelection(faceSelection)
    {}

    std::string getSupport() const
    {
        return faceSelection.getAsPropertyLinkSubString();
    }

    App::DocumentObject* getObject() const
    {
        return faceSelection.getObject();
    }

private:
    mutable Gui::SelectionObject faceSelection;
};

class SketchPreselection
{
public:
    SketchPreselection(
        Gui::Document* guidocument,
        PartDesign::Body* activeBody,
        std::tuple<Gui::SelectionFilter, Gui::SelectionFilter, Gui::SelectionFilter> filter
    )
        : guidocument(guidocument)
        , activeBody(activeBody)
        , faceFilter(std::get<0>(filter))
        , planeFilter(std::get<1>(filter))
        , sketchFilter(std::get<2>(filter))
    {}

    bool matches()
    {
        return faceFilter.match() || planeFilter.match() || sketchFilter.match();
    }

    std::string getSupport() const
    {
        return supportString;
    }

    void createSupport()
    {
        createBodyOrThrow();

        // get the selected object
        App::DocumentObject* selectedObject {};

        if (faceFilter.match()) {
            Gui::SelectionObject faceSelObject = faceFilter.Result[0][0];
            SupportFaceValidator validator {faceSelObject};
            validator.handleSelectedBody(activeBody);
            validator.throwIfInvalid();

            selectedObject = validator.getObject();
            supportString = validator.getSupport();
        }
        else if (planeFilter.match()) {
            SupportPlaneValidator validator(planeFilter.Result[0][0]);
            selectedObject = validator.getObject();
            supportString = validator.getSupport();
        }
        else {
            // For a sketch, the support is the object itself with no sub-element.
            Gui::SelectionObject sketchSelObject = sketchFilter.Result[0][0];
            selectedObject = sketchSelObject.getObject();
            supportString = sketchSelObject.getAsPropertyLinkSubString();
        }

        handleIfSupportOutOfBody(selectedObject);
    }

    void createSketchOnSupport(const std::string& supportString)
    {
        // create Sketch on Face or Plane
        App::Document* appdocument = guidocument->getDocument();
        std::string FeatName = appdocument->getUniqueObjectName("Sketch");

        guidocument->openCommand(QT_TRANSLATE_NOOP("Command", "Sketch on Face"));
        FCMD_OBJ_CMD(activeBody, "newObject('Sketcher::SketchObject','" << FeatName << "')");
        auto Feat = activeBody->getDocument()->getObject(FeatName.c_str());
        FCMD_OBJ_CMD(Feat, "AttachmentSupport = " << supportString);
        if (sketchFilter.match()) {
            FCMD_OBJ_CMD(
                Feat,
                "MapMode = '" << Attacher::AttachEngine::getModeName(Attacher::mmObjectXY) << "'"
            );
        }
        else {  // For Face or Plane
            FCMD_OBJ_CMD(
                Feat,
                "MapMode = '" << Attacher::AttachEngine::getModeName(Attacher::mmFlatFace) << "'"
            );
        }
        Gui::Command::updateActive();
        PartDesignGui::setEdit(Feat, activeBody);
    }

private:
    void createBodyOrThrow()
    {
        if (!activeBody) {
            activeBody = PartDesignGui::getBody(/* messageIfNot = */ true);
            if (activeBody) {
                tryAddNewBodyToActivePart();
            }
            else {
                throw RejectException();
            }
        }
    }

    void tryAddNewBodyToActivePart()
    {
        App::Part* activePart = PartDesignGui::getActivePart();
        if (activePart) {
            activePart->addObject(activeBody);
        }
    }

    void handleIfSupportOutOfBody(App::DocumentObject* selectedObject)
    {
        if (!activeBody->hasObject(selectedObject)) {
            if (!selectedObject->isDerivedFrom(App::Plane::getClassTypeId())) {
                // TODO check here if the plane associated with right part/body (2015-09-01, Fat-Zer)

                // check the prerequisites for the selected objects
                // the user has to decide which option we should take if external references are used
                //  TODO share this with UnifiedDatumCommand() (2015-10-20, Fat-Zer)
                QDialog dia(Gui::getMainWindow());
                PartDesignGui::Ui_DlgReference dlg;
                dlg.setupUi(&dia);
                dia.setModal(true);
                int result = dia.exec();
                if (result == QDialog::Rejected) {
                    throw RejectException();
                }

                if (!dlg.radioXRef->isChecked()) {
                    guidocument->openCommand(QT_TRANSLATE_NOOP("Command", "Make copy"));
                    auto copy = makeCopy(selectedObject, dlg.radioIndependent->isChecked());
                    supportString = supportFromCopy(copy);
                    guidocument->commitCommand();
                }
            }
        }
    }

    App::DocumentObject* makeCopy(App::DocumentObject* selectedObject, bool independent)
    {
        std::string sub;
        if (faceFilter.match()) {
            sub = faceFilter.Result[0][0].getSubNames()[0];
        }
        auto copy = PartDesignGui::TaskFeaturePick::makeCopy(selectedObject, sub, independent);

        addToBodyOrPart(copy);

        return copy;
    }

    std::string supportFromCopy(App::DocumentObject* copy)
    {
        std::string supportString;
        if (planeFilter.match()) {
            supportString = Gui::Command::getObjectCmd(copy, "(", ",'')");
        }
        else {
            // it is ensured that only a single face is selected, hence it must always be Face1 of
            // the shapebinder
            supportString = Gui::Command::getObjectCmd(copy, "(", ",'Face1')");
        }
        return supportString;
    }

    void addToBodyOrPart(App::DocumentObject* object)
    {
        auto activePart = PartDesignGui::getPartFor(activeBody, false);
        if (activeBody) {
            activeBody->addObject(object);
        }
        else if (activePart) {
            activePart->addObject(object);
        }
    }

private:
    Gui::Document* guidocument;
    PartDesign::Body* activeBody;
    Gui::SelectionFilter faceFilter;
    Gui::SelectionFilter planeFilter;
    Gui::SelectionFilter sketchFilter;
    std::string supportString;
};

class PlaneFinder
{
public:
    PlaneFinder(App::Document* appdocument, PartDesign::Body* activeBody)
        : appdocument(appdocument)
        , activeBody(activeBody)
    {}

    std::vector<App::DocumentObject*> getPlanes() const
    {
        return planes;
    }

    std::vector<PartDesignGui::TaskFeaturePick::featureStatus> getStatus() const
    {
        return status;
    }

    unsigned countValidPlanes() const
    {
        return validPlaneCount;
    }

    void findBasePlanes()
    {
        try {
            tryFindBasePlanes();
        }
        catch (const Base::Exception& ex) {
            Base::Console().error("%s\n", ex.what());
        }
    }

    void findDatumPlanes()
    {
        App::GeoFeatureGroupExtension* geoGroup = getGroupExtensionOfBody();
        const std::vector<Base::Type> types
            = {PartDesign::Plane::getClassTypeId(), App::Plane::getClassTypeId()};
        auto datumPlanes = appdocument->getObjectsOfType(types);

        for (auto plane : datumPlanes) {
            if (std::find(planes.begin(), planes.end(), plane) != planes.end()) {
                continue;  // Skip if already in planes (for base planes)
            }

            planes.push_back(plane);
            // Check whether this plane belongs to the active body
            if (activeBody->hasObject(plane, true)) {
                if (!activeBody->isAfterInsertPoint(plane)) {
                    validPlaneCount++;
                    status.push_back(PartDesignGui::TaskFeaturePick::validFeature);
                }
                else {
                    status.push_back(PartDesignGui::TaskFeaturePick::afterTip);
                }
            }
            else {
                PartDesign::Body* planeBody = PartDesign::Body::findBodyOf(plane);
                if (planeBody) {
                    if ((geoGroup && geoGroup->hasObject(planeBody, true))
                        || !App::GeoFeatureGroupExtension::getGroupOfObject(planeBody)) {
                        status.push_back(PartDesignGui::TaskFeaturePick::otherBody);
                    }
                    else {
                        status.push_back(PartDesignGui::TaskFeaturePick::otherPart);
                    }
                }
                else {
                    if ((geoGroup && geoGroup->hasObject(plane, true))
                        || App::GeoFeatureGroupExtension::getGroupOfObject(plane)) {
                        status.push_back(PartDesignGui::TaskFeaturePick::otherPart);
                    }
                    else {
                        status.push_back(PartDesignGui::TaskFeaturePick::notInBody);
                    }
                }
            }
        }
    }

    void findShapeBinderPlanes()
    {

        // Collect also shape binders consisting of a single planar face
        auto shapeBinders(appdocument->getObjectsOfType(PartDesign::ShapeBinder::getClassTypeId()));
        auto binders(appdocument->getObjectsOfType(PartDesign::SubShapeBinder::getClassTypeId()));
        shapeBinders.insert(shapeBinders.end(), binders.begin(), binders.end());
        for (auto binder : shapeBinders) {
            // Check whether this plane belongs to the active body
            if (activeBody->hasObject(binder)) {
                Part::TopoShape shape = static_cast<Part::Feature*>(binder)->Shape.getShape();
                if (shape.isPlanar()) {
                    if (!activeBody->isAfterInsertPoint(binder)) {
                        validPlaneCount++;
                        planes.push_back(binder);
                        status.push_back(PartDesignGui::TaskFeaturePick::validFeature);
                    }
                }
            }
        }
    }

private:
    void tryFindBasePlanes()
    {
        auto* origin = activeBody->getOrigin();
        for (auto plane : origin->planes()) {
            planes.push_back(plane);
            status.push_back(PartDesignGui::TaskFeaturePick::basePlane);
            validPlaneCount++;
        }
    }

    App::GeoFeatureGroupExtension* getGroupExtensionOfBody() const
    {
        App::GeoFeatureGroupExtension* geoGroup {nullptr};
        if (activeBody) {
            auto group(App::GeoFeatureGroupExtension::getGroupOfObject(activeBody));
            if (group) {
                geoGroup = group->getExtensionByType<App::GeoFeatureGroupExtension>();
            }
        }

        return geoGroup;
    }

private:
    App::Document* appdocument;
    PartDesign::Body* activeBody;
    unsigned validPlaneCount = 0;
    std::vector<App::DocumentObject*> planes;
    std::vector<PartDesignGui::TaskFeaturePick::featureStatus> status;
};

class SketchRequestSelection
{
public:
    SketchRequestSelection(Gui::Document* guidocument, PartDesign::Body* activeBody)
        : guidocument(guidocument)
        , activeBody(activeBody)
    {}

    void findSupport()
    {
        try {
            // Start command early, so undo will undo any Body creation
            guidocument->openCommand(QT_TRANSLATE_NOOP("Command", "New Sketch"));
            tryFindSupport();
        }
        catch (const RejectException&) {
            guidocument->abortCommand();
            throw;
        }
        catch (const MissingPlanesException&) {
            guidocument->abortCommand();
            throw;
        }
    }

private:
    void tryFindSupport()
    {
        createBodyOrThrow();

        bool useAttachment = App::GetApplication()
                                 .GetParameterGroupByPath(
                                     "User parameter:BaseApp/Preferences/Mod/PartDesign"
                                 )
                                 ->GetBool("NewSketchUseAttachmentDialog", false);
        if (useAttachment) {
            createSketchAndShowAttachment();
        }
        else {
            findAndSelectPlane();
        }
    }

    void createBodyOrThrow()
    {
        if (!activeBody) {
            App::Document* appdocument = guidocument->getDocument();
            activeBody = PartDesignGui::makeBody(appdocument);
            if (activeBody) {
                tryAddNewBodyToActivePart();
            }
            else {
                throw RejectException();
            }
        }
    }

    void tryAddNewBodyToActivePart()
    {
        App::Part* activePart = PartDesignGui::getActivePart();
        if (activePart) {
            activePart->addObject(activeBody);
        }
    }

    void setOriginTemporaryVisibility()
    {
        auto* origin = activeBody->getOrigin();
        auto* vpo = dynamic_cast<Gui::ViewProviderCoordinateSystem*>(
            Gui::Application::Instance->getViewProvider(origin)
        );
        if (vpo) {
            vpo->setTemporaryVisibility(Gui::DatumElement::Planes | Gui::DatumElement::Axes);
            vpo->setPlaneLabelVisibility(true);
        }
    }

    void createSketchAndShowAttachment()
    {
        setOriginTemporaryVisibility();

        // Create sketch
        App::Document* doc = activeBody->getDocument();
        std::string FeatName = doc->getUniqueObjectName("Sketch");
        FCMD_OBJ_CMD(activeBody, "newObject('Sketcher::SketchObject','" << FeatName << "')");
        auto sketch = doc->getObject(FeatName.c_str());

        PartDesign::Body* partDesignBody = activeBody;
        auto onAccept = [partDesignBody, sketch]() {
            resetOriginVisibility(partDesignBody);

            Gui::Selection().clearSelection();

            PartDesignGui::setEdit(sketch, partDesignBody);
        };
        auto onReject = [partDesignBody]() {
            resetOriginVisibility(partDesignBody);
        };

        Gui::Selection().clearSelection();

        // Open attachment dialog
        auto* vps = dynamic_cast<SketcherGui::ViewProviderSketch*>(
            Gui::Application::Instance->getViewProvider(sketch)
        );
        vps->showAttachmentEditor(onAccept, onReject);
    }

    static void resetOriginVisibility(PartDesign::Body* partDesignBody)
    {
        auto* origin = partDesignBody->getOrigin();
        auto* vpo = dynamic_cast<Gui::ViewProviderCoordinateSystem*>(
            Gui::Application::Instance->getViewProvider(origin)
        );
        if (vpo) {
            vpo->resetTemporaryVisibility();
            vpo->resetTemporarySize();
            vpo->setPlaneLabelVisibility(false);
        }
    }

    void findAndSelectPlane()
    {
        App::Document* appdocument = guidocument->getDocument();
        PlaneFinder planeFinder {appdocument, activeBody};

        planeFinder.findBasePlanes();
        planeFinder.findDatumPlanes();
        planeFinder.findShapeBinderPlanes();

        std::vector<App::DocumentObject*> planes = planeFinder.getPlanes();
        std::vector<PartDesignGui::TaskFeaturePick::featureStatus> status = planeFinder.getStatus();
        unsigned validPlaneCount = planeFinder.countValidPlanes();

        for (auto& plane : planes) {
            auto* planeViewProvider
                = Gui::Application::Instance->getViewProvider<Gui::ViewProviderPlane>(plane);

            // skip updating planes from coordinate systems
            if (!planeViewProvider || !planeViewProvider->getRole().empty()) {
                continue;
            }

            planeViewProvider->setLabelVisibility(true);
            planeViewProvider->setTemporaryScale(
                Gui::ViewParams::instance()->getDatumTemporaryScaleFactor()
            );
        }

        //
        // Lambda definitions
        //
        App::Document* documentOfBody = appdocument;
        PartDesign::Body* partDesignBody = activeBody;

        auto restorePlaneVisibility = [planes]() {
            for (auto& plane : planes) {
                auto* planeViewProvider
                    = Gui::Application::Instance->getViewProvider<Gui::ViewProviderPlane>(plane);
                if (!planeViewProvider) {
                    continue;
                }

                planeViewProvider->resetTemporarySize();
                planeViewProvider->setLabelVisibility(false);
            }
        };

        // Determines if user made a valid selection in dialog
        auto acceptFunction =
            [restorePlaneVisibility](const std::vector<App::DocumentObject*>& features) -> bool {
            restorePlaneVisibility();
            return !features.empty();
        };

        // Called by dialog when user hits "OK" and accepter returns true
        auto processFunction = [documentOfBody,
                                partDesignBody](const std::vector<App::DocumentObject*>& features) {
            SketchRequestSelection::createSketch(documentOfBody, partDesignBody, features);
        };

        // Called by dialog for "Cancel", or "OK" if accepter returns false
        std::string docname = documentOfBody->getName();
        auto rejectFunction = [docname, restorePlaneVisibility]() {
            restorePlaneVisibility();
            Gui::Document* document = Gui::Application::Instance->getDocument(docname.c_str());
            if (document) {
                document->abortCommand();
            }
        };

        //
        // End of lambda definitions
        //

        if (validPlaneCount == 0) {
            throw MissingPlanesException();
        }
        else if (validPlaneCount == 1) {
            processFunction(planes);
        }
        else if (validPlaneCount > 1) {
            checkForShownDialog();
            Gui::Selection().clearSelection();

            // Show dialog and let user pick plane
            Gui::Control().showDialog(new PartDesignGui::TaskDlgFeaturePick(
                planes,
                status,
                acceptFunction,
                processFunction,
                true,
                rejectFunction
            ));
        }
    }

    void checkForShownDialog()
    {
        Gui::TaskView::TaskDialog* dlg = Gui::Control().activeDialog();
        PartDesignGui::TaskDlgFeaturePick* pickDlg
            = qobject_cast<PartDesignGui::TaskDlgFeaturePick*>(dlg);
        if (dlg && !pickDlg) {
            QMessageBox msgBox(Gui::getMainWindow());
            msgBox.setText(QObject::tr("A dialog is already open in the task panel"));
            msgBox.setInformativeText(QObject::tr("Close this dialog?"));
            msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
            msgBox.setDefaultButton(QMessageBox::Yes);
            int ret = msgBox.exec();
            if (ret == QMessageBox::Yes) {
                Gui::Control().closeDialog();
            }
            else {
                throw RejectException();
            }
        }

        if (dlg) {
            Gui::Control().closeDialog();
        }
    }

    static void createSketch(
        App::Document* documentOfBody,
        PartDesign::Body* partDesignBody,
        const std::vector<App::DocumentObject*>& features
    )
    {
        // may happen when the user switched to an empty document while the
        // dialog is open
        if (features.empty()) {
            return;
        }
        std::string FeatName = documentOfBody->getUniqueObjectName("Sketch");
        auto* plane = static_cast<App::Plane*>(features.front());
        auto* lcs = plane->getLCS();

        std::string supportString;
        if (lcs) {
            supportString = Gui::Command::getObjectCmd(lcs, "(") + ",['"
                + plane->getNameInDocument() + "'])";
        }
        else {
            supportString = Gui::Command::getObjectCmd(plane, "(", ",[''])");
        }

        App::Document* doc = partDesignBody->getDocument();
        if (!doc->hasPendingTransaction()) {
            doc->openTransaction(QT_TRANSLATE_NOOP("Command", "New Sketch"));
        }

        FCMD_OBJ_CMD(partDesignBody, "newObject('Sketcher::SketchObject','" << FeatName << "')");
        auto Feat = doc->getObject(FeatName.c_str());
        FCMD_OBJ_CMD(Feat, "AttachmentSupport = " << supportString);
        FCMD_OBJ_CMD(
            Feat,
            "MapMode = '" << Attacher::AttachEngine::getModeName(Attacher::mmFlatFace) << "'"
        );
        Gui::Command::updateActive();  // Make sure the AttachmentSupport's Placement property is updated
        PartDesignGui::setEdit(Feat, partDesignBody);
    }

private:
    Gui::Document* guidocument;
    PartDesign::Body* activeBody;
};

}  // namespace

SketchWorkflow::SketchWorkflow(Gui::Document* document)
    : guidocument(document)
{
    appdocument = guidocument->getDocument();
}

void SketchWorkflow::createSketch()
{
    try {
        tryCreateSketch();
    }
    catch (const RejectException&) {
    }
    catch (const WrongSelectionException&) {
        QMessageBox::warning(
            Gui::getMainWindow(),
            QObject::tr("Several sub-elements selected"),
            QObject::tr("Select a single face as support for a sketch!")
        );
    }
    catch (const WrongSupportException&) {
        QMessageBox::warning(
            Gui::getMainWindow(),
            QObject::tr("No support face selected"),
            QObject::tr("Select a face as support for a sketch!")
        );
    }
    catch (const SupportNotPlanarException&) {
        QMessageBox::warning(
            Gui::getMainWindow(),
            QObject::tr("No planar support"),
            QObject::tr("Need a planar face as support for a sketch!")
        );
    }
    catch (const MissingPlanesException&) {
        QMessageBox::warning(
            Gui::getMainWindow(),
            QObject::tr("No valid planes in this document"),
            QObject::tr("Create a plane first or select a face to sketch on")
        );
    }
}

void SketchWorkflow::tryCreateSketch()
{
    auto result = shouldCreateBody();
    auto shouldMakeBody = std::get<0>(result);
    activeBody = std::get<1>(result);
    if (shouldAbort(shouldMakeBody)) {
        return;
    }

    auto filters = getFilters();
    SketchPreselection sketchOnFace {guidocument, activeBody, filters};

    if (sketchOnFace.matches()) {
        // create Sketch on Face or Plane
        sketchOnFace.createSupport();
        sketchOnFace.createSketchOnSupport(sketchOnFace.getSupport());
    }
    else {
        SketchRequestSelection requestSelection {guidocument, activeBody};
        requestSelection.findSupport();
    }
}

std::tuple<bool, PartDesign::Body*> SketchWorkflow::shouldCreateBody()
{
    auto shouldMakeBody {false};

    // We need either an active Body, or for there to be no Body
    // objects (in which case, just make one) to make a new sketch.
    // If we are inside a link, we need to use its placement.
    App::DocumentObject* topParent;
    PartDesign::Body* pdBody
        = PartDesignGui::getBody(/* messageIfNot = */ false, true, true, &topParent);
    if (pdBody && topParent->isLink()) {
        auto* xLink = dynamic_cast<App::Link*>(topParent);
        pdBody->Placement.setValue(xLink->Placement.getValue());
    }
    if (!pdBody) {
        if (appdocument->countObjectsOfType<PartDesign::Body>() == 0) {
            shouldMakeBody = true;
        }
        else {
            PartDesignGui::DlgActiveBody dia(Gui::getMainWindow(), appdocument);
            if (dia.exec() == QDialog::Accepted) {
                pdBody = dia.getActiveBody();
            }
        }
    }

    return std::make_tuple(shouldMakeBody, pdBody);
}

bool SketchWorkflow::shouldAbort(bool shouldMakeBody) const
{
    return !shouldMakeBody && !activeBody;
}

std::tuple<Gui::SelectionFilter, Gui::SelectionFilter, Gui::SelectionFilter> SketchWorkflow::getFilters() const
{
    // Hint:
    // The behaviour of this command has changed with respect to a selected sketch:
    // It doesn't try any more to edit a selected sketch but always tries to create
    // a new sketch.
    // See https://forum.freecad.org/viewtopic.php?f=3&t=44070

    Gui::SelectionFilter FaceFilter("SELECT Part::Feature SUBELEMENT Face COUNT 1");
    Gui::SelectionFilter PlaneFilter("SELECT App::Plane COUNT 1", activeBody);
    Gui::SelectionFilter PlaneFilter2("SELECT PartDesign::Plane COUNT 1", activeBody);
    Gui::SelectionFilter SketchFilter("SELECT Part::Part2DObject COUNT 1", activeBody);

    if (PlaneFilter2.match()) {
        PlaneFilter = PlaneFilter2;
    }

    return std::make_tuple(FaceFilter, PlaneFilter, SketchFilter);
}
