/***************************************************************************
 *   Copyright (C) 2015 Alexander Golubev (Fat-Zer) <fatzer2@gmail.com>    *
 *                                                                         *
 *   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 <QInputDialog>
#include <QMessageBox>
#include <TopExp_Explorer.hxx>

#include <App/Datums.h>
#include <App/Document.h>
#include <App/GeoFeatureGroupExtension.h>
#include <App/Origin.h>
#include <App/Part.h>
#include <Base/Console.h>
#include <Base/Tools.h>
#include <Gui/Command.h>
#include <Gui/Control.h>
#include <Gui/Document.h>
#include <Gui/Application.h>
#include <Gui/MainWindow.h>
#include <Gui/MDIView.h>
#include <Mod/Sketcher/App/SketchObject.h>
#include <Mod/PartDesign/App/Body.h>
#include <Mod/PartDesign/App/FeatureBase.h>
#include <Mod/PartDesign/App/FeatureSketchBased.h>

#include "TaskFeaturePick.h"
#include "Utils.h"
#include "WorkflowManager.h"


//===========================================================================
// Shared functions
//===========================================================================

namespace PartDesignGui
{

/// Returns active part, if there is no such, creates a new part, if it fails, shows a message
App::Part* assertActivePart()
{
    App::Part* rv = Gui::Application::Instance->activeView()->getActiveObject<App::Part*>(PARTKEY);

    if (!rv) {
        Gui::CommandManager& rcCmdMgr = Gui::Application::Instance->commandManager();
        rcCmdMgr.runCommandByName("Std_Part");
        rv = Gui::Application::Instance->activeView()->getActiveObject<App::Part*>(PARTKEY);
        if (!rv) {
            QMessageBox::critical(
                nullptr,
                QObject::tr("Part creation failed"),
                QObject::tr("Failed to create a part object.")
            );
        }
    }

    return rv;
}

}  // namespace PartDesignGui

// PartDesign_Body
//===========================================================================
DEF_STD_CMD_A(CmdPartDesignBody)

CmdPartDesignBody::CmdPartDesignBody()
    : Command("PartDesign_Body")
{
    sAppModule = "PartDesign";
    sGroup = QT_TR_NOOP("PartDesign");
    sMenuText = QT_TR_NOOP("New Body");
    sToolTipText = QT_TR_NOOP("Creates a new body and activates it");
    sWhatsThis = "PartDesign_Body";
    sStatusTip = sToolTipText;
    sPixmap = "PartDesign_Body";
}

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

    App::Part* actPart = PartDesignGui::getActivePart();
    App::Part* partOfBaseFeature = nullptr;

    std::vector<App::DocumentObject*> features = getSelection().getObjectsOfType(
        Part::Feature::getClassTypeId()
    );
    App::DocumentObject* baseFeature = nullptr;
    bool addtogroup = false;

    Base::Reference<ParameterGrp> hGrp = App::GetApplication().GetUserParameter().GetGroup(
        "BaseApp/Preferences/Mod/PartDesign"
    );

    bool allowCompound = hGrp->GetBool("AllowCompoundDefault", true);

    if (!features.empty()) {
        if (features.size() == 1) {
            baseFeature = features[0];
            if (baseFeature->isDerivedFrom(PartDesign::Feature::getClassTypeId())
                && PartDesign::Body::findBodyOf(baseFeature)) {
                // Prevent creating bodies based on features already belonging to other bodies
                QMessageBox::warning(
                    Gui::getMainWindow(),
                    QObject::tr("Bad base feature"),
                    QObject::tr("A body cannot be based on a Part Design feature.")
                );
                baseFeature = nullptr;
            }
            else if (PartDesign::Body::findBodyOf(baseFeature)) {
                QMessageBox::warning(
                    Gui::getMainWindow(),
                    QObject::tr("Bad base feature"),
                    QObject::tr("%1 already belongs to a body and cannot be used as a base feature for another body.")
                        .arg(QString::fromUtf8(baseFeature->Label.getValue()))
                );
                baseFeature = nullptr;
            }
            else if (baseFeature->isDerivedFrom(Part::BodyBase::getClassTypeId())) {
                // Prevent creating bodies based on bodies (but don't pop-up a dialog)
                baseFeature = nullptr;
            }
            else {
                partOfBaseFeature = App::Part::getPartOfObject(baseFeature);
                if (partOfBaseFeature && partOfBaseFeature != actPart) {
                    // prevent cross-part mess
                    QMessageBox::warning(
                        Gui::getMainWindow(),
                        QObject::tr("Bad base feature"),
                        QObject::tr("Base feature (%1) belongs to other part.")
                            .arg(QString::fromUtf8(baseFeature->Label.getValue()))
                    );
                    baseFeature = nullptr;
                }
                else if (baseFeature->isDerivedFrom<Sketcher::SketchObject>()) {
                    // Add sketcher to the body's group property
                    addtogroup = true;
                }
                // if a standard Part feature (not a PartDesign feature) is selected then check
                // the number of solids/shells
                else if (!baseFeature->isDerivedFrom<PartDesign::Feature>()) {
                    const TopoDS_Shape& shape
                        = static_cast<Part::Feature*>(baseFeature)->Shape.getValue();
                    if (!shape.IsNull()) {
                        int numSolids = 0;
                        int numShells = 0;
                        for (TopExp_Explorer xp(shape, TopAbs_SOLID); xp.More(); xp.Next()) {
                            numSolids++;
                        }
                        for (TopExp_Explorer xp(shape, TopAbs_SHELL, TopAbs_SOLID); xp.More();
                             xp.Next()) {
                            numShells++;
                        }

                        QString warning;
                        if (numSolids > 1 && numShells == 0) {
                            warning = QObject::tr(
                                "The selected shape consists of multiple solids.\n"
                                "This may lead to unexpected results."
                            );
                        }
                        else if (numShells > 1 && numSolids == 0) {
                            warning = QObject::tr(
                                "The selected shape consists of multiple shells.\n"
                                "This may lead to unexpected results."
                            );
                        }
                        else if (numShells == 1 && numSolids == 0) {
                            warning = QObject::tr(
                                "The selected shape consists of only a shell.\n"
                                "This may lead to unexpected results."
                            );
                        }
                        else if (numSolids + numShells > 1) {
                            warning = QObject::tr(
                                "The selected shape consists of multiple solids or shells.\n"
                                "This may lead to unexpected results."
                            );
                        }

                        if (!warning.isEmpty()) {
                            QMessageBox::warning(
                                Gui::getMainWindow(),
                                QObject::tr("Base feature"),
                                warning
                            );
                        }
                    }
                }
            }
        }
        else {
            QMessageBox::warning(
                Gui::getMainWindow(),
                QObject::tr("Bad base feature"),
                QObject::tr("Body may be based on no more than one feature.")
            );
            return;
        }
    }

    openCommand(QT_TRANSLATE_NOOP("Command", "Add a Body"));

    std::string bodyName = getUniqueObjectName("Body");
    const char* bodyString = bodyName.c_str();

    // add the Body feature itself, and make it active
    doCommand(Doc, "App.activeDocument().addObject('PartDesign::Body','%s')", bodyString);
    // set Label for i18n/L10N
    std::string labelString = QObject::tr("Body").toUtf8().toStdString();
    labelString = Base::Tools::escapeEncodeString(labelString);
    doCommand(Doc, "App.ActiveDocument.getObject('%s').Label = '%s'", bodyString, labelString.c_str());
    doCommand(
        Doc,
        "App.ActiveDocument.getObject('%s').AllowCompound = %s",
        bodyString,
        allowCompound ? "True" : "False"
    );
    if (baseFeature) {
        if (partOfBaseFeature) {
            // withdraw base feature from Part, otherwise visibility madness results
            doCommand(
                Doc,
                "App.activeDocument().%s.removeObject(App.activeDocument().%s)",
                partOfBaseFeature->getNameInDocument(),
                baseFeature->getNameInDocument()
            );
        }
        if (addtogroup) {
            doCommand(
                Doc,
                "App.activeDocument().%s.Group = [App.activeDocument().%s]",
                bodyString,
                baseFeature->getNameInDocument()
            );
        }
        else {
            doCommand(
                Doc,
                "App.activeDocument().%s.BaseFeature = App.activeDocument().%s",
                bodyString,
                baseFeature->getNameInDocument()
            );
        }
    }
    addModule(Gui, "PartDesignGui");  // import the Gui module only once a session

    if (actPart) {
        doCommand(
            Doc,
            "App.activeDocument().%s.addObject(App.ActiveDocument.%s)",
            actPart->getNameInDocument(),
            bodyString
        );
    }

    doCommand(
        Gui::Command::Gui,
        "Gui.activateView('Gui::View3DInventor', True)\n"
        "Gui.activeView().setActiveObject('%s', App.activeDocument().%s)",
        PDBODYKEY,
        bodyString
    );

    // Make the "Create sketch" prompt appear in the task panel
    doCommand(Gui, "Gui.Selection.clearSelection()");
    doCommand(Gui, "Gui.Selection.addSelection(App.ActiveDocument.%s)", bodyString);

    // check if a proxy object has been created for the base feature inside the body
    if (baseFeature) {
        PartDesign::Body* body = dynamic_cast<PartDesign::Body*>(
            baseFeature->getDocument()->getObject(bodyString)
        );
        if (body) {
            std::vector<App::DocumentObject*> links = body->Group.getValues();
            for (auto it : links) {
                if (it->isDerivedFrom<PartDesign::FeatureBase>()) {
                    PartDesign::FeatureBase* base = static_cast<PartDesign::FeatureBase*>(it);
                    if (base && base->BaseFeature.getValue() == baseFeature) {
                        Gui::Application::Instance->hideViewProvider(baseFeature);
                        break;
                    }
                }
            }

            // for sketches open the feature dialog to rebase it to a new plane
            // as requested in issue #0002862
            if (addtogroup) {
                std::vector<App::DocumentObject*> planes;
                std::vector<PartDesignGui::TaskFeaturePick::featureStatus> status;
                unsigned validPlaneCount = 0;
                for (auto plane : body->getOrigin()->planes()) {
                    planes.push_back(plane);
                    status.push_back(PartDesignGui::TaskFeaturePick::basePlane);
                    validPlaneCount++;
                }

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

                    // Called by dialog when user hits "OK" and accepter returns true
                    auto worker = [baseFeature](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;
                        }
                        App::Plane* plane = static_cast<App::Plane*>(features.front());
                        std::string supportString = Gui::Command::getObjectCmd(plane, "(", ", [''])");

                        FCMD_OBJ_CMD(baseFeature, "AttachmentSupport = " << supportString);
                        FCMD_OBJ_CMD(
                            baseFeature,
                            "MapMode = '"
                                << Attacher::AttachEngine::getModeName(Attacher::mmFlatFace) << "'"
                        );
                        Gui::Command::updateActive();
                    };

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

                    // Show dialog and let user pick plane
                    Gui::TaskView::TaskDialog* dlg = Gui::Control().activeDialog();
                    if (!dlg) {
                        Gui::Selection().clearSelection();
                        Gui::Control().showDialog(
                            new PartDesignGui::TaskDlgFeaturePick(planes, status, accepter, worker, true, quitter)
                        );
                    }
                }
            }
        }
    }

    updateActive();
}

bool CmdPartDesignBody::isActive()
{
    return hasActiveDocument();
}

//===========================================================================
// PartDesign_Migrate
//===========================================================================

DEF_STD_CMD_A(CmdPartDesignMigrate)

CmdPartDesignMigrate::CmdPartDesignMigrate()
    : Command("PartDesign_Migrate")
{
    sAppModule = "PartDesign";
    sGroup = QT_TR_NOOP("PartDesign");
    sMenuText = QT_TR_NOOP("Migrate");
    sToolTipText = QT_TR_NOOP("Migrates the document to the modern Part Design workflow");
    sWhatsThis = "PartDesign_Migrate";
    sStatusTip = sToolTipText;
    sPixmap = "PartDesign_Migrate";
}

void CmdPartDesignMigrate::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    App::Document* doc = getDocument();

    std::set<PartDesign::Feature*> migrateFeatures;


    // Retrieve all PartDesign Features objects and filter out features already belonging to some body
    for (const auto& feat : doc->getObjects()) {
        if (feat->isDerivedFrom(PartDesign::Feature::getClassTypeId())
            && !PartDesign::Body::findBodyOf(feat) && PartDesign::Body::isSolidFeature(feat)) {
            migrateFeatures.insert(static_cast<PartDesign::Feature*>(feat));
        }
    }

    if (migrateFeatures.empty()) {
        if (!PartDesignGui::isModernWorkflow(doc)) {
            // If there is nothing to migrate and workflow is still old just set it to modern
            PartDesignGui::WorkflowManager::instance()->forceWorkflow(
                doc,
                PartDesignGui::Workflow::Modern
            );
        }
        else {
            // Huh? nothing to migrate?
            QMessageBox::warning(
                nullptr,
                QObject::tr("Nothing to migrate"),
                QObject::tr(
                    "No Part Design features without body found"
                    " Nothing to migrate."
                )
            );
        }
        return;
    }

    // Note: this action is undoable, should it be?
    PartDesignGui::WorkflowManager::instance()->forceWorkflow(doc, PartDesignGui::Workflow::Modern);

    // Put features into chains. Each chain should become a separate body.
    std::list<std::list<PartDesign::Feature*>> featureChains;
    std::list<PartDesign::Feature*> chain;  //< the current chain we are working on

    for (auto featIt = migrateFeatures.begin(); !migrateFeatures.empty();) {
        Part::Feature* base = (*featIt)->getBaseObject(/*silent =*/true);

        chain.push_front(*featIt);

        if (!base || !base->isDerivedFrom(PartDesign::Feature::getClassTypeId())
            || PartDesignGui::isAnyNonPartDesignLinksTo(
                static_cast<PartDesign::Feature*>(base),
                /*respectGroups=*/true
            )) {
            // a feature based on nothing as well as on non-partdesign solid starts a new chain
            auto newChainIt = featureChains.emplace(featureChains.end());
            newChainIt->splice(newChainIt->end(), chain);
        }
        else {
            // we are basing on some partdesign feature which supposed to belong to some body
            PartDesign::Feature* baseFeat = static_cast<PartDesign::Feature*>(base);

            auto baseFeatSetIt = migrateFeatures.find(baseFeat);

            if (baseFeatSetIt != migrateFeatures.end()) {
                // base feature is pending for migration, switch to it and continue over
                migrateFeatures.erase(featIt);
                featIt = baseFeatSetIt;
                continue;
            }
            else {
                // The base feature seems already assigned to some chain. Find which
                std::list<PartDesign::Feature*>::iterator baseFeatIt;
                auto isChain =
                    [baseFeat, &baseFeatIt](std::list<PartDesign::Feature*>& fchain) mutable -> bool {
                    baseFeatIt = std::ranges::find(fchain, baseFeat);
                    return baseFeatIt != fchain.end();
                };

                if (auto chainIt = std::ranges::find_if(featureChains, isChain);
                    chainIt != featureChains.end()) {
                    assert(baseFeatIt != chainIt->end());
                    if (std::next(baseFeatIt) == chainIt->end()) {
                        // just append our chain to already found
                        chainIt->splice(chainIt->end(), chain);
                        // TODO: If we will hit a third part everything will be messed up again.
                        //       Probably it will require a yet another smart-ass find_if.
                        //       (2015-08-10, Fat-Zer)
                    }
                    else {
                        // We have a fork of a partDesign feature here
                        // add a chain for current body
                        auto newChainIt = featureChains.emplace(featureChains.end());
                        newChainIt->splice(newChainIt->end(), chain);
                        // add a chain for forked one
                        newChainIt = featureChains.emplace(featureChains.end());
                        newChainIt->splice(
                            newChainIt->end(),
                            *chainIt,
                            std::next(baseFeatIt),
                            chainIt->end()
                        );
                    }
                }
                else {
                    // The feature is not present in list pending for migration,
                    // This generally shouldn't happen but may be if we run into some broken file
                    // Try to find out the body we should insert into
                    // TODO: Some error/warning is needed here (2015-08-10, Fat-Zer)
                    auto newChainIt = featureChains.emplace(featureChains.end());
                    newChainIt->splice(newChainIt->end(), chain);
                }
            }
        }
        migrateFeatures.erase(featIt);
        featIt = migrateFeatures.begin();
        // TODO: Align visibility (2015-08-17, Fat-Zer)
    } /* for */

    // TODO: make it work without parts (2015-09-04, Fat-Zer)
    // add a part if there is no active yet
    App::Part* actPart = PartDesignGui::assertActivePart();

    if (!actPart) {
        return;
    }

    // do the actual migration
    Gui::Command::openCommand(
        QT_TRANSLATE_NOOP("Command", "Migrate legacy Part Design features to bodies")
    );

    for (auto chainIt = featureChains.begin(); !featureChains.empty();
         featureChains.erase(chainIt), chainIt = featureChains.begin()) {
#ifndef FC_DEBUG
        if (chainIt->empty()) {  // prevent crash in release in case of errors
            continue;
        }
#else
        assert(!chainIt->empty());
#endif
        Part::Feature* base = chainIt->front()->getBaseObject(/*silent =*/true);

        // Find a suitable chain to work with
        for (; chainIt != featureChains.end(); chainIt++) {
            base = chainIt->front()->getBaseObject(/*silent =*/true);
            if (!base || !base->isDerivedFrom(PartDesign::Feature::getClassTypeId())) {
                break;  // no base is ok
            }
            else {
                // The base feature is a PartDesign, it's a fork, try to reassign it to a body...
                base = PartDesign::Body::findBodyOf(base);
                if (base) {
                    break;
                }
            }
        }

        if (chainIt == featureChains.end()) {
            // Shouldn't happen, may be only in case of some circular dependency?
            // TODO Some error message (2015-08-11, Fat-Zer)
            chainIt = featureChains.begin();
            base = chainIt->front()->getBaseObject(/*silent =*/true);
        }

        // Construct a Pretty Body name based on the Tip
        std::string bodyName = getUniqueObjectName(
            std::string(chainIt->back()->getNameInDocument()).append("Body").c_str()
        );

        Base::Reference<ParameterGrp> hGrp = App::GetApplication().GetUserParameter().GetGroup(
            "BaseApp/Preferences/Mod/PartDesign"
        );

        bool allowCompound = hGrp->GetBool("AllowCompoundDefault", true);

        // Create a body for the chain
        doCommand(Doc, "App.activeDocument().addObject('PartDesign::Body','%s')", bodyName.c_str());
        doCommand(
            Doc,
            "App.ActiveDocument.getObject('%s').AllowCompound = %s",
            bodyName.c_str(),
            allowCompound ? "True" : "False"
        );
        doCommand(
            Doc,
            "App.activeDocument().%s.addObject(App.ActiveDocument.%s)",
            actPart->getNameInDocument(),
            bodyName.c_str()
        );
        if (base) {
            doCommand(
                Doc,
                "App.activeDocument().%s.BaseFeature = App.activeDocument().%s",
                bodyName.c_str(),
                base->getNameInDocument()
            );
        }

        // Fill the body with features
        for (auto feature : *chainIt) {
            if (feature->isDerivedFrom(PartDesign::ProfileBased::getClassTypeId())) {
                // add the sketch and also reroute it if needed
                PartDesign::ProfileBased* sketchBased = static_cast<PartDesign::ProfileBased*>(feature);
                Part::Part2DObject* sketch = sketchBased->getVerifiedSketch(/*silent =*/true);
                if (sketch) {
                    doCommand(
                        Doc,
                        "App.activeDocument().%s.addObject(App.activeDocument().%s)",
                        bodyName.c_str(),
                        sketch->getNameInDocument()
                    );

                    if (sketch->isDerivedFrom(Sketcher::SketchObject::getClassTypeId())) {
                        try {
                            PartDesignGui::fixSketchSupport(
                                static_cast<Sketcher::SketchObject*>(sketch)
                            );
                        }
                        catch (Base::Exception&) {
                            QMessageBox::critical(
                                Gui::getMainWindow(),
                                QObject::tr("Sketch plane cannot be migrated"),
                                QObject::tr(
                                    "Please edit '%1' and redefine it to use a Base or "
                                    "Datum plane as the sketch plane."
                                )
                                    .arg(QString::fromUtf8(sketch->Label.getValue()))
                            );
                        }
                    }
                    else {
                        // TODO: Message that sketchbased is based not on a sketch (2015-08-11, Fat-Zer)
                    }
                }
            }
            doCommand(
                Doc,
                "App.activeDocument().%s.addObject(App.activeDocument().%s)",
                bodyName.c_str(),
                feature->getNameInDocument()
            );

            PartDesignGui::relinkToBody(feature);
        }
    }

    updateActive();
}

bool CmdPartDesignMigrate::isActive()
{
    return hasActiveDocument();
}

//===========================================================================
// PartDesign_MoveTip
//===========================================================================
DEF_STD_CMD_A(CmdPartDesignMoveTip)

CmdPartDesignMoveTip::CmdPartDesignMoveTip()
    : Command("PartDesign_MoveTip")
{
    sAppModule = "PartDesign";
    sGroup = QT_TR_NOOP("PartDesign");
    sMenuText = QT_TR_NOOP("Set Tip");
    sToolTipText = QT_TR_NOOP("Moves the tip of the body to the selected feature");
    sWhatsThis = "PartDesign_MoveTip";
    sStatusTip = sToolTipText;
    sPixmap = "PartDesign_MoveTip";
}

void CmdPartDesignMoveTip::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    std::vector<App::DocumentObject*> features = getSelection().getObjectsOfType(
        Part::Feature::getClassTypeId()
    );
    App::DocumentObject* selFeature;
    PartDesign::Body* body = nullptr;

    if (features.size() == 1) {
        selFeature = features.front();
        if (selFeature->isDerivedFrom<PartDesign::Body>()) {
            body = static_cast<PartDesign::Body*>(selFeature);
        }
        else {
            body = PartDesignGui::getBodyFor(selFeature, /* messageIfNot =*/false);
        }
    }
    else {
        selFeature = nullptr;
    }

    if (!selFeature) {
        QMessageBox::warning(
            nullptr,
            QObject::tr("Selection error"),
            QObject::tr("Select exactly one Part Design feature or a body.")
        );
        return;
    }
    else if (!body) {
        QMessageBox::warning(
            nullptr,
            QObject::tr("Selection error"),
            QObject::tr(
                "Could not determine a body for the selected feature '%s'.",
                selFeature->Label.getValue()
            )
        );
        return;
    }
    else if (!selFeature->isDerivedFrom(PartDesign::Feature::getClassTypeId()) && selFeature != body
             && body->BaseFeature.getValue() != selFeature) {
        QMessageBox::warning(
            nullptr,
            QObject::tr("Selection error"),
            QObject::tr("Only a solid feature can be the tip of a body.")
        );
        return;
    }

    App::DocumentObject* oldTip = body->Tip.getValue();
    if (oldTip == selFeature) {  // it's not generally an error, so print only a console message
        Base::Console().message("%s is already the tip of the body\n", selFeature->getNameInDocument());
        return;
    }

    openCommand(QT_TRANSLATE_NOOP("Command", "Move tip to selected feature"));

    if (selFeature == body) {
        FCMD_OBJ_CMD(body, "Tip = None");
    }
    else {
        FCMD_OBJ_CMD(body, "Tip = " << getObjectCmd(selFeature));

        // Adjust visibility to show only the Tip feature
        FCMD_OBJ_SHOW(selFeature);
    }

    // TODO: Hide all datum features after the Tip feature? But the user might have already hidden
    // some and wants to see others, so we would have to remember their state somehow
    updateActive();
}

bool CmdPartDesignMoveTip::isActive()
{
    return hasActiveDocument();
}

//===========================================================================
// PartDesign_DuplicateSelection
//===========================================================================

DEF_STD_CMD_A(CmdPartDesignDuplicateSelection)

CmdPartDesignDuplicateSelection::CmdPartDesignDuplicateSelection()
    : Command("PartDesign_DuplicateSelection")
{
    sAppModule = "PartDesign";
    sGroup = QT_TR_NOOP("PartDesign");
    sMenuText = QT_TR_NOOP("Duplicate &Object");
    sToolTipText = QT_TR_NOOP("Duplicates the selected object and adds it to the active body");
    sWhatsThis = "PartDesign_DuplicateSelection";
    sStatusTip = sToolTipText;
}

void CmdPartDesignDuplicateSelection::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    PartDesign::Body* pcActiveBody = PartDesignGui::getBody(/*messageIfNot = */ false);

    std::vector<App::DocumentObject*> beforeFeatures = getDocument()->getObjects();

    openCommand(QT_TRANSLATE_NOOP("Command", "Duplicate a Part Design object"));
    doCommand(Doc, "FreeCADGui.runCommand('Std_DuplicateSelection')");

    if (pcActiveBody) {
        // Find the features that were added
        std::vector<App::DocumentObject*> afterFeatures = getDocument()->getObjects();
        std::vector<App::DocumentObject*> newFeatures;
        std::sort(beforeFeatures.begin(), beforeFeatures.end());
        std::sort(afterFeatures.begin(), afterFeatures.end());
        std::set_difference(
            afterFeatures.begin(),
            afterFeatures.end(),
            beforeFeatures.begin(),
            beforeFeatures.end(),
            std::back_inserter(newFeatures)
        );

        for (auto feature : newFeatures) {
            if (PartDesign::Body::isAllowed(feature)) {
                // if feature already is in a body, then we don't put it into the active body issue #6278
                auto body = App::GeoFeatureGroupExtension::getGroupOfObject(feature);
                if (!body) {
                    FCMD_OBJ_CMD(pcActiveBody, "addObject(" << getObjectCmd(feature) << ")");
                    FCMD_OBJ_HIDE(feature);
                }
            }
        }

        // Adjust visibility of features
        if (!newFeatures.empty()) {
            FCMD_OBJ_SHOW(newFeatures.back());
        }
    }

    updateActive();
}

bool CmdPartDesignDuplicateSelection::isActive()
{
    return hasActiveDocument();
}

//===========================================================================
// PartDesign_MoveFeature
//===========================================================================

DEF_STD_CMD_A(CmdPartDesignMoveFeature)

CmdPartDesignMoveFeature::CmdPartDesignMoveFeature()
    : Command("PartDesign_MoveFeature")
{
    sAppModule = "PartDesign";
    sGroup = QT_TR_NOOP("PartDesign");
    sMenuText = QT_TR_NOOP("Move Object To…");
    sToolTipText = QT_TR_NOOP("Moves the selected object to another body");
    sWhatsThis = "PartDesign_MoveFeature";
    sStatusTip = sToolTipText;
    sPixmap = "PartDesign_MoveFeature";
}

void CmdPartDesignMoveFeature::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    std::vector<App::DocumentObject*> features = getSelection().getObjectsOfType(
        Part::Feature::getClassTypeId()
    );
    if (features.empty()) {
        return;
    }

    // Check if all features are valid to move
    if (std::any_of(std::begin(features), std::end(features), [](App::DocumentObject* obj) {
            return !PartDesignGui::isFeatureMovable(obj);
        })) {
        // show messagebox and cancel
        QMessageBox::warning(
            Gui::getMainWindow(),
            QObject::tr("Features cannot be moved"),
            QObject::tr("Some of the selected features have dependencies in the source body")
        );
        return;
    }

    // Collect dependencies of the selected features
    std::vector<App::DocumentObject*> dependencies = PartDesignGui::collectMovableDependencies(
        features
    );
    if (!dependencies.empty()) {
        features.insert(std::end(features), std::begin(dependencies), std::end(dependencies));
    }

    // Create a list of all bodies in this part
    std::vector<App::DocumentObject*> bodies = getDocument()->getObjectsOfType(
        Part::BodyBase::getClassTypeId()
    );

    std::set<App::DocumentObject*> source_bodies;
    for (auto feat : features) {
        // Note: 'source' can be null which means that the feature doesn't belong to a body.
        PartDesign::Body* source = PartDesign::Body::findBodyOf(feat);
        source_bodies.insert(static_cast<App::DocumentObject*>(source));
    }

    if (source_bodies.size() != 1) {
        // show messagebox and cancel
        QMessageBox::warning(
            Gui::getMainWindow(),
            QObject::tr("Features cannot be moved"),
            QObject::tr("Only features of a single source body can be moved")
        );
        return;
    }

    auto source_body = *source_bodies.begin();

    std::vector<App::DocumentObject*> target_bodies;
    for (auto body : bodies) {
        if (!source_bodies.count(body)) {
            target_bodies.push_back(body);
        }
    }

    if (target_bodies.empty()) {
        QMessageBox::warning(
            Gui::getMainWindow(),
            QObject::tr("Features cannot be moved"),
            QObject::tr("There are no other bodies to move to")
        );
        return;
    }

    // Ask user to select the target body (remove source bodies from list)
    bool ok;
    QStringList items;
    for (auto body : target_bodies) {
        items.push_back(QString::fromUtf8(body->Label.getValue()));
    }
    QString text = QInputDialog::getItem(
        Gui::getMainWindow(),
        qApp->translate("PartDesign_MoveFeature", "Select Body"),
        qApp->translate("PartDesign_MoveFeature", "Select a body from the list"),
        items,
        0,
        false,
        &ok,
        Qt::MSWindowsFixedSizeDialogHint
    );
    if (!ok) {
        return;
    }
    int index = items.indexOf(text);
    if (index < 0) {
        return;
    }

    PartDesign::Body* target = static_cast<PartDesign::Body*>(target_bodies[index]);

    openCommand(QT_TRANSLATE_NOOP("Command", "Move an object"));

    std::stringstream stream;
    stream << "features_ = [" << getObjectCmd(features.back());
    features.pop_back();

    for (auto feat : features) {
        stream << ", " << getObjectCmd(feat);
    }

    stream << "]";
    runCommand(Doc, stream.str().c_str());
    FCMD_OBJ_CMD(source_body, "removeObjects(features_)");
    FCMD_OBJ_CMD(target, "addObjects(features_)");
    /*

        // Find body of this feature
        Part::BodyBase* source = PartDesign::Body::findBodyOf(feat);
        bool featureWasTip = false;

        if (source == target) continue;

        // Remove from the source body if the feature belonged to a body
        if (source) {
            featureWasTip = (source->Tip.getValue() == feat);
            doCommand(Doc,"App.activeDocument().%s.removeObject(App.activeDocument().%s)",
                      source->getNameInDocument(), (feat)->getNameInDocument());
        }

        App::DocumentObject* targetOldTip = target->Tip.getValue();

        // Add to target body (always at the Tip)
        doCommand(Doc,"App.activeDocument().%s.addObject(App.activeDocument().%s)",
                      target->getNameInDocument(), (feat)->getNameInDocument());
        // Recompute to update the shape
        doCommand(Gui,"App.activeDocument().recompute()");

        // Adjust visibility of features
        // TODO: May be something can be done in view provider (2015-08-05, Fat-Zer)
        // If we removed the tip of the source body, make the new tip visible
        if ( featureWasTip ) {
            App::DocumentObject * sourceNewTip = source->Tip.getValue();
            if (sourceNewTip)
                doCommand(Gui,"Gui.activeDocument().show(\"%s\")",
    sourceNewTip->getNameInDocument());
        }

        // Hide old tip and show new tip (the moved feature) of the target body
        App::DocumentObject* targetNewTip = target->Tip.getValue();
        if ( targetOldTip != targetNewTip ) {
            if ( targetOldTip ) {
                doCommand(Gui,"Gui.activeDocument().hide(\"%s\")",
    targetOldTip->getNameInDocument());
            }
            if (targetNewTip) {
                doCommand(Gui,"Gui.activeDocument().show(\"%s\")",
    targetNewTip->getNameInDocument());
            }
        }

        // Fix sketch support
        if (feat->isDerivedFrom<Sketcher::SketchObject>()) {
            Sketcher::SketchObject *sketch = static_cast<Sketcher::SketchObject*>(feat);
            try {
                PartDesignGui::fixSketchSupport(sketch);
            } catch (Base::Exception &) {
                QMessageBox::warning( Gui::getMainWindow(), QObject::tr("Sketch plane cannot be
    migrated"), QObject::tr("Please edit '%1' and redefine it to use a Base or Datum plane as the
    sketch plane."). arg( QString::fromLatin1( sketch->Label.getValue () ) ) );
            }
        }

        //relink origin for sketches and datums (coordinates)
        PartDesignGui::relinkToOrigin(feat, target);
    }*/

    updateActive();
}

bool CmdPartDesignMoveFeature::isActive()
{
    return hasActiveDocument();
}

DEF_STD_CMD_A(CmdPartDesignMoveFeatureInTree)

CmdPartDesignMoveFeatureInTree::CmdPartDesignMoveFeatureInTree()
    : Command("PartDesign_MoveFeatureInTree")
{
    sAppModule = "PartDesign";
    sGroup = QT_TR_NOOP("PartDesign");
    sMenuText = QT_TR_NOOP("Move Feature After…");
    sToolTipText = QT_TR_NOOP("Moves the selected feature after another feature in the same body");
    sWhatsThis = "PartDesign_MoveFeatureInTree";
    sStatusTip = sToolTipText;
    sPixmap = "PartDesign_MoveFeatureInTree";
}

void CmdPartDesignMoveFeatureInTree::activated(int iMsg)
{
    Q_UNUSED(iMsg);
    std::vector<App::DocumentObject*> features = getSelection().getObjectsOfType(
        Part::Feature::getClassTypeId()
    );

    // also check and include datum objects, ie. plane, line, and point
    std::vector<App::DocumentObject*> datums = getSelection().getObjectsOfType(
        App::DatumElement::getClassTypeId()
    );
    features.insert(features.end(), datums.begin(), datums.end());

    std::vector<App::DocumentObject*> lcs = getSelection().getObjectsOfType(
        App::LocalCoordinateSystem::getClassTypeId()
    );
    features.insert(features.end(), lcs.begin(), lcs.end());

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

    PartDesign::Body* body = PartDesignGui::getBodyFor(features.front(), false);
    App::DocumentObject* bodyBase = nullptr;
    // sanity check
    bool allFeaturesFromSameBody = true;

    if (body) {
        bodyBase = body->BaseFeature.getValue();
        for (auto feat : features) {
            if (!body->hasObject(feat)) {
                allFeaturesFromSameBody = false;
                break;
            }
            if (bodyBase == feat) {
                QMessageBox::warning(
                    nullptr,
                    QObject::tr("Selection error"),
                    QObject::tr("Impossible to move the base feature of a body.")
                );
                return;
            }
        }
    }
    if (!body || !allFeaturesFromSameBody) {
        QMessageBox::warning(
            nullptr,
            QObject::tr("Selection error"),
            QObject::tr("Select one or more features from the same body.")
        );
        return;
    }

    // Create a list of all features in this body
    const std::vector<App::DocumentObject*>& model = body->Group.getValues();

    // Ask user to select the target feature
    bool ok;
    QStringList items;
    if (bodyBase) {
        items.push_back(QString::fromUtf8(bodyBase->Label.getValue()));
    }
    else {
        items.push_back(QObject::tr("Beginning of the body"));
    }
    for (auto feat : model) {
        items.push_back(QString::fromUtf8(feat->Label.getValue()));
    }

    QString text = QInputDialog::getItem(
        Gui::getMainWindow(),
        qApp->translate("PartDesign_MoveFeatureInTree", "Move Feature After…"),
        qApp->translate("PartDesign_MoveFeatureInTree", "Select a feature from the list"),
        items,
        0,
        false,
        &ok,
        Qt::MSWindowsFixedSizeDialogHint
    );
    if (!ok) {
        return;
    }
    int index = items.indexOf(text);
    // first object is the beginning of the body
    App::DocumentObject* target = index != 0 ? model[index - 1] : nullptr;

    openCommand(QT_TRANSLATE_NOOP("Command", "Move a feature inside body"));

    App::DocumentObject* lastObject = target;
    for (auto feat : features) {
        if (feat == target) {
            continue;
        }

        // Remove and re-insert the feature to/from the Body, preserving their order.
        // TODO: if tip was moved the new position of tip is quite undetermined (2015-08-07, Fat-Zer)
        // TODO: warn the user if we are moving an object to some place before the object's link
        // (2015-08-07, Fat-Zer)
        FCMD_OBJ_CMD(body, "removeObject(" << getObjectCmd(feat) << ")");
        FCMD_OBJ_CMD(
            body,
            "insertObject(" << getObjectCmd(feat) << "," << getObjectCmd(lastObject) << ", True)"
        );

        lastObject = feat;
    }

    // Dependency order check.
    // We must make sure the resulting objects of PartDesign::Feature do not
    // depend on later objects
    std::vector<App::DocumentObject*> bodyFeatures;
    std::map<App::DocumentObject*, size_t> orders;
    for (auto obj : body->Group.getValues()) {
        if (obj->isDerivedFrom<PartDesign::Feature>()) {
            orders.emplace(obj, bodyFeatures.size());
            bodyFeatures.push_back(obj);
        }
    }
    bool failed = false;
    std::ostringstream ss;
    for (size_t i = 0; i < bodyFeatures.size(); ++i) {
        auto feat = bodyFeatures[i];
        for (auto obj : feat->getOutList()) {
            if (obj->isDerivedFrom<PartDesign::Feature>()) {
                continue;
            }
            for (auto dep : App::Document::getDependencyList({obj})) {
                auto it = orders.find(dep);
                if (it != orders.end() && it->second > i) {
                    ss << feat->Label.getValue() << ", " << obj->Label.getValue() << " -> "
                       << it->first->Label.getValue();
                    if (!failed) {
                        failed = true;
                    }
                    else {
                        ss << std::endl;
                    }
                }
            }
        }
    }
    if (failed) {
        QMessageBox::critical(
            nullptr,
            QObject::tr("Dependency violation"),
            QObject::tr("Early feature must not depend on later feature.\n\n")
                + QString::fromUtf8(ss.str().c_str())
        );
        abortCommand();
        return;
    }

    // If the selected objects have been moved after the current tip then ask the
    // user if they want the last object to be the new tip.
    // Only do this for features that can hold a tip (not for e.g. datums)
    if (lastObject != target && body->Tip.getValue() == target
        && lastObject->isDerivedFrom<PartDesign::Feature>()) {
        QMessageBox msgBox(Gui::getMainWindow());
        msgBox.setIcon(QMessageBox::Question);
        msgBox.setWindowTitle(qApp->translate("PartDesign_MoveFeatureInTree", "Move Tip"));
        msgBox.setText(qApp->translate(
            "PartDesign_MoveFeatureInTree",
            "The moved feature appears after the currently set tip."
        ));
        msgBox.setInformativeText(
            qApp->translate("PartDesign_MoveFeatureInTree", "Set tip to last feature?")
        );
        msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
        msgBox.setDefaultButton(QMessageBox::No);
        int ret = msgBox.exec();
        if (ret == QMessageBox::Yes) {
            FCMD_OBJ_CMD(body, "Tip = " << getObjectCmd(lastObject));
        }
    }

    updateActive();
}

bool CmdPartDesignMoveFeatureInTree::isActive()
{
    return hasActiveDocument();
}


//===========================================================================
// Initialization
//===========================================================================

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

    rcCmdMgr.addCommand(new CmdPartDesignBody());
    rcCmdMgr.addCommand(new CmdPartDesignMigrate());
    rcCmdMgr.addCommand(new CmdPartDesignMoveTip());

    rcCmdMgr.addCommand(new CmdPartDesignDuplicateSelection());
    rcCmdMgr.addCommand(new CmdPartDesignMoveFeature());
    rcCmdMgr.addCommand(new CmdPartDesignMoveFeatureInTree());
}
