//=============================================================================
// 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/io/Logger.h"

#include "smtk/session/aeva/CellSelection.h"
#include "smtk/session/aeva/Predicates.h"
#include "smtk/session/aeva/operators/Boolean.h"
#include "smtk/session/aeva/operators/Delete.h"

#include "vtk/aeva/ext/vtkGlobalIdBooleans.h"

#include "smtk/model/Face.h"
#include "smtk/model/Model.h"

#include "smtk/attribute/Attribute.h"
#include "smtk/attribute/ComponentItem.h"
#include "smtk/attribute/DoubleItem.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/StringItem.h"
#include "smtk/attribute/VoidItem.h"

#include "smtk/operation/Manager.h"
#include "smtk/operation/MarkGeometry.h"

#include "smtk/view/Selection.h"

#include "vtkAppendDataSets.h"
#include "vtkCellData.h"
#include "vtkDataSetSurfaceFilter.h"
#include "vtkDistancePolyDataFilter.h"
#include "vtkDoubleArray.h"
#include "vtkImplicitPolyDataDistance.h"
#include "vtkMath.h"
#include "vtkPointData.h"
#include "vtkPolyData.h"
#include "vtkPolyDataNormals.h"
#include "vtkThreshold.h"
#include "vtkUnsignedCharArray.h"
#include "vtkUnstructuredGrid.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"

#include <cmath>

#include "smtk/session/aeva/operators/Boolean_xml.h"

// On Windows MSVC 2015+, something is included that defines
// a macro named ERROR to be 0. This causes smtkErrorMacro()
// to expand into garbage (because smtk::io::Logger::ERROR
// gets expanded to smtk::io::Logger::0).
#ifdef ERROR
#undef ERROR
#endif

namespace
{

std::set<smtk::model::Entity*> Accumulate(
  const std::shared_ptr<smtk::attribute::ReferenceItem>& workpieces,
  const std::shared_ptr<smtk::attribute::ComponentItem>& tools)
{
  std::set<smtk::model::Entity*> result;

  for (auto it = workpieces->begin(); it != workpieces->end(); ++it)
  {
    if (it.isSet())
    {
      auto* entry = dynamic_cast<smtk::model::Entity*>((*it).get());
      if (entry)
      {
        result.insert(entry);
      }
    }
  }

  if (tools)
  {
    for (auto it = tools->begin(); it != tools->end(); ++it)
    {
      if (it.isSet())
      {
        auto* entry = dynamic_cast<smtk::model::Entity*>((*it).get());
        if (entry)
        {
          result.insert(entry);
        }
      }
    }
  }
  return result;
}

} // anonymous namespace

namespace smtk
{
namespace session
{
namespace aeva
{

bool Boolean::ableToOperate()
{
  if (!this->Superclass::ableToOperate())
  {
    return false;
  }

  smtk::session::aeva::Resource::Ptr resource;
  smtk::session::aeva::Session::Ptr session;
  if (!this->fetchResourceAndSession(resource, session))
  {
    return false;
  }

  auto workpieces = this->parameters()->associations();
  auto tools = this->parameters()->findComponent("tools");
  auto optype = this->type();

  // Verify all workpieces are side sets and have primitive geometry.
  std::size_t numberOfWorkpieces = 0;
  std::size_t numberOfTools = 0;
  for (auto entry = workpieces->begin(); entry != workpieces->end(); ++entry)
  {
    smtk::model::Entity* ent;
    if (entry.isSet() && (ent = dynamic_cast<smtk::model::Entity*>((*entry).get())))
    {
      if (session->isPrimary(*ent))
      {
        smtkErrorMacro(this->log(), "Cannot modify primary geometry (" << entry->name() << ").");
        return false;
      }
      if (!Boolean::storage(*entry))
      {
        smtkErrorMacro(this->log(), "Workpiece (" << entry->name() << ") has no geometry.");
        return false;
      }
      ++numberOfWorkpieces;
    }
  }
  if (numberOfWorkpieces == 0)
  {
    smtkErrorMacro(this->log(), "No valid workpieces.");
    return false;
  }

  // Verify all tools have primitive geometry and count them.
  if (tools)
  {
    for (auto entry = tools->begin(); entry != tools->end(); ++entry)
    {
      if (entry.isSet())
      {
        if (!Boolean::storage(*entry))
        {
          smtkErrorMacro(this->log(), "Tool (" << entry->name() << ") has no geometry.");
          return false;
        }
        ++numberOfTools;
      }
    }
  }
  switch (optype)
  {
    case Unite: // fall through
    case Intersect:
      if (numberOfWorkpieces + numberOfTools < 2)
      {
        smtkErrorMacro(this->log(), "Must have at least 2 parts to unite or intersect.");
        return false;
      }
      break;
    case Subtract:
      if (numberOfTools < 1)
      {
        smtkErrorMacro(this->log(), "Must have both a workpiece and a tool compute difference.");
        return false;
      }
      break;
    case SubtractSymmetrically:
      if (numberOfWorkpieces + numberOfTools < 2)
      {
        smtkErrorMacro(this->log(), "Must have at least 2 parts to compute symmetric difference.");
        return false;
      }
      break;
  }
  return true;
}

Boolean::BooleanOpType Boolean::type() const
{
  int optype = this->parameters()->findInt("operation")->value();
  return static_cast<BooleanOpType>(optype);
}

bool Boolean::unite(const std::set<smtk::model::Entity*>& inputs,
  Session& session,
  vtkSmartPointer<vtkDataObject>& resultData,
  smtk::model::Model& parent)
{
  if (inputs.size() >= 2)
  {
    auto it = inputs.begin();
    vtkSmartPointer<vtkDataObject> workpiece = session.findStorage((*it)->id());
    auto* resource = (*it)->resource().get();
    ++it;
    vtkSmartPointer<vtkDataObject> tool = session.findStorage((*it)->id());
    // TODO: Do not ignore more inputs.
    vtkNew<vtkGlobalIdBooleans> filter;
    filter->SetWorkpiece(workpiece);
    filter->SetTool(tool);
    filter->SetOperationToUnion();
    filter->Update();
    resultData = filter->GetOutputDataObject(0);
    std::set<smtk::model::Entity*> primaries;
    if (session.primaryGeometries(resultData, EntityType(resultData), primaries, resource))
    {
      smtk::model::Entity* primary = *primaries.begin();
      parent = smtk::model::EntityRef(primary->shared_from_this()).owningModel();
    }
    return true;
  }
  return false;
}

bool Boolean::intersect(const std::shared_ptr<smtk::attribute::ReferenceItem>& workpieces,
  const std::shared_ptr<smtk::attribute::ComponentItem>& tools,
  Session& session,
  vtkSmartPointer<vtkDataObject>& resultData,
  smtk::model::Model& parent)
{
  // TODO: Do not ignore more inputs.
  vtkNew<vtkGlobalIdBooleans> filter;
  filter->SetOperationToIntersection();
  bool haveInputs = false;
  std::shared_ptr<smtk::model::Entity> workpieceCell;
  if (workpieces->numberOfValues() == 1 && workpieces->isSet(0) && tools->numberOfValues() == 1 &&
    tools->isSet(0))
  {
    // Invoked via "Boolean.sbt" which has both workpiece and tool
    workpieceCell = std::dynamic_pointer_cast<smtk::model::Entity>(workpieces->value(0));
    vtkSmartPointer<vtkDataObject> workpiece = session.findStorage(workpieceCell->id());
    vtkSmartPointer<vtkDataObject> tool = session.findStorage(tools->value(0)->id());
    filter->SetWorkpiece(workpiece);
    filter->SetTool(tool);
    haveInputs = true;
  }
  else if (workpieces->numberOfValues() == 2 && workpieces->isSet(0) && workpieces->isSet(1))
  {
    // Invoked via "BooleanIntersect.sbt" which has both workpiece and tool
    workpieceCell = std::dynamic_pointer_cast<smtk::model::Entity>(workpieces->value(0));
    vtkSmartPointer<vtkDataObject> workpiece = session.findStorage(workpieceCell->id());
    vtkSmartPointer<vtkDataObject> tool = session.findStorage(workpieces->value(1)->id());
    filter->SetWorkpiece(workpiece);
    filter->SetTool(tool);
    haveInputs = true;
  }
  if (haveInputs)
  {
    filter->Update();
    resultData = filter->GetOutputDataObject(0);
    parent = smtk::model::EntityRef(workpieceCell).owningModel();
    return true;
  }
  return false;
}

bool Boolean::subtract(const std::shared_ptr<smtk::attribute::ReferenceItem>& workpieces,
  const std::shared_ptr<smtk::attribute::ComponentItem>& tools,
  Session& session,
  vtkSmartPointer<vtkDataObject>& resultData,
  smtk::model::Model& parent)
{
  if (workpieces->numberOfValues() == 1 && workpieces->isSet(0) && tools->numberOfValues() == 1 &&
    tools->isSet(0))
  {
    auto workpieceCell = std::dynamic_pointer_cast<smtk::model::Entity>(workpieces->value(0));
    vtkSmartPointer<vtkDataObject> workpiece = session.findStorage(workpieceCell->id());
    vtkSmartPointer<vtkDataObject> tool = session.findStorage(tools->value(0)->id());
    vtkNew<vtkGlobalIdBooleans> filter;
    filter->SetWorkpiece(workpiece);
    filter->SetTool(tool);
    filter->SetOperationToDifference();
    filter->Update();
    resultData = filter->GetOutputDataObject(0);
    parent = smtk::model::EntityRef(workpieceCell).owningModel();
    return true;
  }
  return false;
}

bool Boolean::symmetricSubtract(const std::set<smtk::model::Entity*>& inputs,
  Session& session,
  vtkSmartPointer<vtkDataObject>& resultData,
  smtk::model::Model& parent)
{
  (void)inputs;
  (void)session;
  (void)resultData;
  (void)parent;
  return false;
}

Boolean::Result Boolean::operateInternal()
{
  smtk::session::aeva::Resource::Ptr resource;
  smtk::session::aeva::Session::Ptr session;
  Boolean::Result result = this->createResult(smtk::operation::Operation::Outcome::FAILED);
  this->prepareResourceAndSession(result, resource, session);

  auto workpieces = this->parameters()->associations();
  auto tools = this->parameters()->findComponent("tools");
  bool keepInputs = this->parameters()->findVoid("keep inputs")->isEnabled();
  auto optype = this->type();

  vtkSmartPointer<vtkDataObject> resultData;
  smtk::model::Model
    parent; // What model (if any) should own the resulting selection? No model => make a CellSelection.
  std::string outputName;

  // If not keeping inputs intact and:
  // 1. there is a single input workpiece cell, or
  // 2. there is no tools item (so workpieces must be plural),
  // then we will simply replace the first workpiece's input
  // geometry. Otherwise, replaceCell will be null and we
  // will create a new output cell (if parent is valid) or a
  // primitive selection (if parent is invalid).
  smtk::model::Entity* replaceCell = nullptr;
  if (!keepInputs)
  {
    if ((workpieces->numberOfValues() == 1 || (!tools && workpieces->numberOfValues() >= 1)) &&
      workpieces->isSet(0))
    {
      replaceCell = dynamic_cast<smtk::model::Entity*>(workpieces->value(0).get());
    }
  }

  switch (optype)
  {
    case Unite:
      if (!Boolean::unite(Accumulate(workpieces, tools), *session, resultData, parent))
      {
        return result;
      }
      outputName = "union";
      break;
    case Intersect:
      if (!Boolean::intersect(workpieces, tools, *session, resultData, parent))
      {
        return result;
      }
      outputName = "intersection";
      break;
    case Subtract:
      if (!Boolean::subtract(workpieces, tools, *session, resultData, parent))
      {
        return result;
      }
      outputName = "difference";
      break;
    case SubtractSymmetrically:
      if (!Boolean::symmetricSubtract(Accumulate(workpieces, tools), *session, resultData, parent))
      {
        return result;
      }
      outputName = "symmetric difference";
      break;
  }

  // Delete inputs if not told to keep them.
  if (!keepInputs)
  {
    auto deleteOp = this->manager()->create<Delete>();
    auto deleteEnts = deleteOp->parameters()->associations();
    auto deleteItem = result->findComponent("expunged");
    for (auto it = workpieces->begin(); it != workpieces->end(); ++it)
    {
      if (it.isSet())
      {
        auto ent = std::dynamic_pointer_cast<smtk::resource::Component>(*it);
        if (ent && ent.get() != replaceCell)
        {
          deleteEnts->appendValue(ent);
          deleteItem->appendValue(ent);
        }
      }
    }

    if (tools)
    {
      for (auto it = tools->begin(); it != tools->end(); ++it)
      {
        if (it.isSet())
        {
          auto ent = std::dynamic_pointer_cast<smtk::resource::Component>(*it);
          deleteEnts->appendValue(ent);
          deleteItem->appendValue(ent);
        }
      }
    }
    deleteOp->operate(Key{});
  }

  if (parent)
  {
    if (replaceCell)
    {
      session->addStorage(replaceCell->id(), resultData);
      smtk::operation::MarkGeometry().markModified(replaceCell->shared_from_this());
      result->findComponent("modified")->appendValue(replaceCell->shared_from_this());
    }
    else
    {
      // Turn the result into an SMTK cell owned by "parent."
      auto resultCell =
        resource->insertCellOfDimension(EstimateParametricDimension(resultData))->second;
      parent.addCell(resultCell);
      smtk::model::EntityRef(resultCell).setName(outputName);
      session->addStorage(resultCell->id(), resultData);
      session->setPrimary(*resultCell, false);
      smtk::operation::MarkGeometry().markModified(resultCell);
      result->findComponent("modified")->appendValue(parent.entityRecord());
      result->findComponent("created")->appendValue(resultCell);
    }
  }
  else
  {
    // Turn the result into a new CellSelection.
    auto cellSelection = CellSelection::instance();
    if (cellSelection)
    {
      cellSelection->replaceData(resultData);
      result->findComponent("modified")->appendValue(cellSelection);
    }
    else
    {
      cellSelection = CellSelection::create(resource, resultData, this->manager());
      result->findComponent("created")->appendValue(cellSelection);
    }
    smtk::view::Selection::Ptr selection;
    if (auto managers = this->managers())
    {
      selection = managers->get<smtk::view::Selection::Ptr>();
    }
    // Select the selection (reset the application selection to be the primitive selection).
    if (selection)
    {
      std::set<smtk::model::EntityPtr> selected;
      selected.insert(cellSelection);
      int selectedValue = selection->selectionValueFromLabel("selected");
      selection->modifySelection(selected,
        "boolean",
        selectedValue,
        smtk::view::SelectionAction::UNFILTERED_REPLACE,
        /* bitwise */ true,
        /* notify */ false);
    }
  }

  result->findInt("outcome")->setValue(static_cast<int>(Boolean::Outcome::SUCCEEDED));
  return result;
}

const char* Boolean::xmlDescription() const
{
  return Boolean_xml;
}

}
}
}
