//=========================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt for details.
//
//  This software is distributed WITHOUT ANY WARRANTY; without even
//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
//  PURPOSE.  See the above copyright notice for more information.
//=========================================================================
#include "smtk/session/aeva/plugin/pqAEVASurfaceFeatureToolBar.h"

#include "smtk/session/aeva/CellSelection.h"
#include "smtk/session/aeva/Resource.h"
#include "smtk/session/aeva/Session.h"
#include "smtk/session/aeva/operators/AdjacencyFeature.h"
#include "smtk/session/aeva/operators/AllPrimitivesFeature.h"
#include "smtk/session/aeva/operators/BooleanIntersect.h"
#include "smtk/session/aeva/operators/BooleanSubtract.h"
#include "smtk/session/aeva/operators/BooleanUnite.h"
#include "smtk/session/aeva/operators/Duplicate.h"
#include "smtk/session/aeva/operators/GrowSelection.h"
#include "smtk/session/aeva/operators/ImprintGeometry.h"
#include "smtk/session/aeva/operators/ImprintImage.h"
#include "smtk/session/aeva/operators/LinearToQuadratic.h"
#include "smtk/session/aeva/operators/NormalFeature.h"
#include "smtk/session/aeva/operators/PointsOfPrimitivesFeature.h"
#include "smtk/session/aeva/operators/ProportionalEdit.h"
#include "smtk/session/aeva/operators/ProximityFeature.h"
#include "smtk/session/aeva/operators/ReconstructSurface.h"
#include "smtk/session/aeva/operators/SmoothSurface.h"
#include "smtk/session/aeva/operators/UnreferencedPrimitives.h"
#include "smtk/session/aeva/operators/VolumeInspect.h"
#include "smtk/session/aeva/operators/VolumeMesher.h"

// SMTK
#include "smtk/view/Selection.h"

#include "smtk/operation/Manager.h"
#include "smtk/operation/Observer.h"
#include "smtk/operation/operators/EditProperties.h"

#include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h"
#include "smtk/extension/paraview/appcomponents/pqSMTKOperationToolboxPanel.h"
#include "smtk/extension/paraview/appcomponents/pqSMTKWrapper.h"
#include "smtk/extension/paraview/server/vtkSMSMTKWrapperProxy.h"
#include "smtk/extension/qt/qtOperationAction.h"
#include "smtk/extension/qt/qtOperationPalette.h"
#include "smtk/extension/qt/qtOperationTypeModel.h"
#include "smtk/extension/vtk/operators/ImageInspector.h"
#include "smtk/extension/vtk/operators/MeshInspector.h"

// ParaView
#include "pqApplicationCore.h"

// VTK
#include "vtkDataSet.h"

// Qt
#include <QAction>
#include <QLabel>
#include <QTimer>

// C++
#include <tuple>
#include <type_traits>
#include <typeinfo>
#include <utility>

namespace
{

template<std::size_t I, typename Tuple>
typename std::enable_if<I == std::tuple_size<Tuple>::value, bool>::type insertActionIntoToolbar(
  QToolBar* toolbar,
  qtOperationTypeModel* model)
{
  (void)model;
  (void)toolbar;
  return true;
}

template<std::size_t I, typename Tuple>
typename std::enable_if<I != std::tuple_size<Tuple>::value, bool>::type insertActionIntoToolbar(
  QToolBar* toolbar,
  qtOperationTypeModel* model)
{
  model->actionFor<typename std::tuple_element<I, Tuple>::type>()->forceStyle(
    Qt::ToolButtonIconOnly, [toolbar](qtOperationAction* action) { toolbar->addAction(action); });

  return insertActionIntoToolbar<I + 1, Tuple>(toolbar, model);
}

template<typename OperationTuple>
void insertIntoToolbar(QToolBar* toolbar, qtOperationTypeModel* model)
{
  insertActionIntoToolbar<0, OperationTuple>(toolbar, model);
}

} // anonymous namespace

static pqAEVASurfaceFeatureToolBar* s_surfaceFeatureToolBar = nullptr;

class pqAEVASurfaceFeatureToolBar::pqInternal
{
  pqInternal(pqAEVASurfaceFeatureToolBar* toolbar)
    : m_toolbar(toolbar)
  {
    // Construct this label immediately but do not insert until populateToolbar().
    // This allows the selection callback to update the label even if buttons are
    // not present in the toolbar.
    m_cellSelectionSizeLabel = new QLabel("0 primitives selected", toolbar);
  }

  void populateToolbar()
  {
    // Because populateToolbar is called after the event loop starts
    // (and thus after plugins have loaded), the toolbox should have
    // a valid qtOperationTypeModel for us.
    auto* core = pqApplicationCore::instance();
    auto* panel =
      qobject_cast<pqSMTKOperationToolboxPanel*>(core->manager("smtk operation toolbox"));
    if (!panel)
    {
      smtkErrorMacro(smtk::io::Logger::instance(), "No toolbox panel (needed for toolbar).");
      return;
    }
    auto toolbox = panel->toolbox();
    if (!toolbox)
    {
      smtkErrorMacro(smtk::io::Logger::instance(), "No toolbox (needed for toolbar).");
      return;
    }
    auto* model = toolbox->operationModel();
    if (!model)
    {
      smtkErrorMacro(smtk::io::Logger::instance(), "No operation model (needed for toolbar).");
      return;
    }

    using FeatureSelectionToolbar = std::tuple<smtk::session::aeva::GrowSelection,
      smtk::session::aeva::NormalFeature,
      smtk::session::aeva::ProximityFeature,
      smtk::session::aeva::AdjacencyFeature,
      smtk::session::aeva::AllPrimitivesFeature,
      smtk::session::aeva::PointsOfPrimitivesFeature,
      smtk::session::aeva::Duplicate>;
    insertIntoToolbar<FeatureSelectionToolbar>(m_toolbar, model);
    // Add a label that our selection-observer updates with primitive counts:
    m_toolbar->addWidget(m_cellSelectionSizeLabel);
    // Keep track of the Duplication action for our global keyboard shortcut.
    m_duplicate = model->actionFor<smtk::session::aeva::Duplicate>();

    // Really we should create multiple toolbars...
    // ... but save that for another day.
    m_toolbar->addSeparator();

    using SideSetToolbar = std::tuple<smtk::session::aeva::BooleanIntersect,
      smtk::session::aeva::BooleanSubtract,
      smtk::session::aeva::BooleanUnite,
      smtk::session::aeva::UnreferencedPrimitives,
      smtk::operation::EditProperties>;
    insertIntoToolbar<SideSetToolbar>(m_toolbar, model);

    m_toolbar->addSeparator();

    using PrimaryGeometryToolbar = std::tuple<smtk::session::aeva::ReconstructSurface,
      smtk::session::aeva::VolumeMesher,
      smtk::session::aeva::SmoothSurface,
      smtk::session::aeva::LinearToQuadratic,
      smtk::session::aeva::ProportionalEdit,
      smtk::session::aeva::ImprintGeometry,
      smtk::session::aeva::ImprintImage>;
    insertIntoToolbar<PrimaryGeometryToolbar>(m_toolbar, model);

    m_toolbar->addSeparator();

    using InspectionToolbar = std::tuple<smtk::session::aeva::VolumeInspect,
      smtk::geometry::ImageInspector,
      smtk::geometry::MeshInspector>;
    insertIntoToolbar<InspectionToolbar>(m_toolbar, model);
  }

  ~pqInternal() = default;

protected:
  friend class pqAEVASurfaceFeatureToolBar;

  pqAEVASurfaceFeatureToolBar* m_toolbar{ nullptr };
  QLabel* m_cellSelectionSizeLabel;
  QPointer<qtOperationAction> m_duplicate;
  smtk::view::SelectionObservers::Key m_selnObserver;
  smtk::operation::Observers::Key m_opObserver;
};

pqAEVASurfaceFeatureToolBar::pqAEVASurfaceFeatureToolBar(QWidget* parent)
  : Superclass(parent)
{
  m_p = new pqInternal(this);
  this->setObjectName("SurfaceFeatures");
  this->setWindowTitle("AEVA Operations");

  auto* behavior = pqSMTKBehavior::instance();
  QObject::connect(behavior,
    SIGNAL(addedManagerOnServer(pqSMTKWrapper*, pqServer*)),
    this,
    SLOT(observeWrapper(pqSMTKWrapper*, pqServer*)));
  QObject::connect(behavior,
    SIGNAL(removingManagerFromServer(pqSMTKWrapper*, pqServer*)),
    this,
    SLOT(unobserveWrapper(pqSMTKWrapper*, pqServer*)));
  // Initialize with current wrapper(s), if any:
  behavior->visitResourceManagersOnServers([this](pqSMTKWrapper* wrapper, pqServer* server) {
    this->observeWrapper(wrapper, server);
    return false; // terminate early
  });

  // Wait until the event loop has started to populate the toolbar
  // so that the toolbox panel is guaranteed to exist and have a model
  // we can reference.
  QTimer::singleShot(0, [this]() { m_p->populateToolbar(); });

  if (!s_surfaceFeatureToolBar)
  {
    s_surfaceFeatureToolBar = this;
  }
}

pqAEVASurfaceFeatureToolBar::~pqAEVASurfaceFeatureToolBar()
{
  delete m_p;
  if (s_surfaceFeatureToolBar == this)
  {
    s_surfaceFeatureToolBar = nullptr;
  }
}

pqAEVASurfaceFeatureToolBar* pqAEVASurfaceFeatureToolBar::instance()
{
  return s_surfaceFeatureToolBar;
}

void pqAEVASurfaceFeatureToolBar::activateDeselect()
{
  auto* behavior = pqSMTKBehavior::instance();
  // For all resource managers on all servers, empty the selection:
  behavior->visitResourceManagersOnServers([](pqSMTKWrapper* wrapper, pqServer* /*server*/) {
    if (wrapper)
    {
      std::set<smtk::resource::PersistentObject::Ptr> empty;
      wrapper->smtkSelection()->modifySelection(
        empty, "deselect", 0, smtk::view::SelectionAction::UNFILTERED_REPLACE, false, false);
    }
    return true; // continue
  });
}

void pqAEVASurfaceFeatureToolBar::activateDuplicate()
{
  // "Press" the duplicate toolbar button.
  if (m_p->m_duplicate)
  {
    m_p->m_duplicate->acceptDefaults();
  }
}

void pqAEVASurfaceFeatureToolBar::observeWrapper(pqSMTKWrapper* wrapper, pqServer* /*server*/)
{
  m_p->m_selnObserver = wrapper->smtkSelection()->observers().insert(
    [this](const std::string& source, smtk::view::Selection::Ptr const& seln) {
      this->onSelectionChanged(source, seln);
    },
    std::numeric_limits<smtk::view::Selection::Observers::Priority>::lowest(),
    /* invoke observer on current selection */ true,
    "pqAEVASurfaceFeatureToolBar: Show primitive count on selection.");
  m_p->m_opObserver = wrapper->smtkOperationManager()->observers().insert(
    [this](const smtk::operation::Operation& /*op*/,
      smtk::operation::EventType event,
      smtk::operation::Operation::Result const &
      /*result*/) -> int {
      if (event == smtk::operation::EventType::DID_OPERATE)
      {
        // TODO: If this is too much, we could inspect Result to see if CellSelection was created/modified.
        this->onPrimitivesSelected(smtk::session::aeva::CellSelection::instance().get());
      }
      return 0;
    },
    std::numeric_limits<smtk::operation::Observers::Priority>::lowest(),
    /* invoke observer on current selection */ false,
    "pqAEVASurfaceFeatureToolBar: Show primitive count on selection modification.");
}

void pqAEVASurfaceFeatureToolBar::unobserveWrapper(pqSMTKWrapper* /*wrapper*/, pqServer* /*server*/)
{
  m_p->m_selnObserver = smtk::view::SelectionObservers::Key();
  m_p->m_opObserver = smtk::operation::Observers::Key();
  this->onPrimitivesSelected(nullptr);
}

void pqAEVASurfaceFeatureToolBar::onSelectionChanged(const std::string& /*selnSource*/,
  const std::shared_ptr<smtk::view::Selection>& seln)
{
  if (seln)
  {
    for (const auto& entry : seln->currentSelection())
    {
      if (entry.second)
      {
        auto* prims = dynamic_cast<smtk::session::aeva::CellSelection*>(entry.first.get());
        if (prims)
        {
          this->onPrimitivesSelected(prims);
          return;
        }
      }
    }
  }
  this->onPrimitivesSelected(nullptr);
}

void pqAEVASurfaceFeatureToolBar::onPrimitivesSelected(smtk::session::aeva::CellSelection* cellSeln)
{
  std::ostringstream label;
  if (cellSeln)
  {
    auto* rsrc = dynamic_cast<smtk::session::aeva::Resource*>(cellSeln->resource().get());
    if (rsrc)
    {
      vtkSmartPointer<vtkDataObject> obj = rsrc->session()->findStorage(cellSeln->id());
      auto* dataset = vtkDataSet::SafeDownCast(obj);
      if (dataset)
      {
        label << dataset->GetNumberOfCells() << " primitives selected";
      }
      else if (obj)
      {
        label << obj->GetClassName() << " selected";
      }
    }
  }
  if (label.str().empty())
  {
    label << "0 primitives selected";
  }
  m_p->m_cellSelectionSizeLabel->setText(label.str().c_str());
}
