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

#include "smtk/model/Entity.h"
#include "smtk/model/Face.h"
#include "smtk/model/Group.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/operation/MarkGeometry.h"

#include "vtkCellData.h"
#include "vtkDataSetSurfaceFilter.h"
#include "vtkDoubleArray.h"
#include "vtkMath.h"
#include "vtkPointData.h"
#include "vtkPolyData.h"
#include "vtkThreshold.h"
#include "vtkVector.h"
#include "vtkVectorOperators.h"

#include <cmath>

#include "smtk/session/aeva/operators/Duplicate_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 smtk
{
namespace session
{
namespace aeva
{

namespace
{

void duplicateProperties(const smtk::model::Entity::Ptr& src, const smtk::model::Entity::Ptr& dst)
{
  if (!src || !dst)
  {
    return;
  }
  // Always create side sets, even if the original was primary geometry.
  // To create primary geometry, new global IDs would need to be assigned
  // to the underlying VTK data.
  Session::setPrimary(*dst, false);

  // Copy color, if any.
  if (src->properties().contains<std::vector<double> >("color"))
  {
    dst->properties().get<std::vector<double> >()["color"] =
      src->properties().at<std::vector<double> >("color");
  }
}

template<typename C>
void addCandidateOnMatch(smtk::model::Model& model,
  smtk::model::CellEntity& cell,
  const std::array<int, 2>& range,
  bool isNodal,
  C& candidates)
{
  if (Session::isSideSet(*cell.component()))
  {
    // Skip side sets; we only want primary "element block" cells.
    return;
  }
  auto cellData = Operation::storage(cell.component());
  if (cellData)
  {
    auto* dataset = vtkDataSet::SafeDownCast(cellData);
    auto* ids = vtkIntArray::SafeDownCast(
      isNodal ? dataset->GetPointData()->GetGlobalIds() : dataset->GetCellData()->GetGlobalIds());
    if (!ids)
    {
      return;
    }
    int* dr = ids->GetValueRange(0);
    if (dr[0] <= range[1] && range[0] <= dr[1])
    {
      candidates.insert(std::make_pair(model, cell.entityRecord().get()));
    }
  }
  // Descend to find lower-dimensional boundaries with geometry
  auto bdys = cell.boundingCells();
  for (auto& bdy : bdys)
  {
    addCandidateOnMatch(model, bdy, range, isNodal, candidates);
  }
}

template<typename C>
void addCandidates(smtk::model::Model& model,
  const std::array<int, 2>& range,
  bool isNodal,
  C& candidates)
{
  auto cells = model.cells();
  for (auto& cell : cells)
  {
    addCandidateOnMatch(model, cell, range, isNodal, candidates);
  }
}

std::set<smtk::model::Model> parentModels(const smtk::model::Entity::Ptr& dupe,
  vtkSmartPointer<vtkDataObject> const& dupeDG)
{
  std::set<smtk::model::Model> result;
  auto resource = dupe ? dynamic_pointer_cast<smtk::session::aeva::Resource>(dupe->resource())
                       : smtk::session::aeva::Resource::Ptr();
  auto* dupeDS = vtkDataSet::SafeDownCast(dupeDG);
  if (!resource || !dupe || !dupeDG || !dupeDS)
  {
    return result;
  }
  auto session = resource->session();
  bool isNodal = dupe->dimension() == 0 || EstimateParametricDimension(dupeDG) == 0;
  auto* ids = vtkIntArray::SafeDownCast(
    isNodal ? dupeDS->GetPointData()->GetGlobalIds() : dupeDS->GetCellData()->GetGlobalIds());
  if (!ids)
  {
    return result;
  }
  int* dr = ids->GetValueRange(0);
  std::array<int, 2> dupeRange;
  dupeRange[0] = dr[0];
  dupeRange[1] = dr[1];
  // std::cout << "Looking for models with IDs in [" << dupeRange[0] << "  " << dupeRange[1] << "]\n";
  auto models =
    resource->entitiesMatchingFlagsAs<smtk::model::Models>(smtk::model::MODEL_ENTITY, false);
  std::set<std::pair<smtk::model::Model, smtk::model::Entity*> > candidates;
  for (auto& model : models)
  {
    addCandidates(model, dupeRange, isNodal, candidates);
    for (const auto& entry : candidates)
    {
      // TODO: Filter based on actual values.
      result.insert(entry.first);
    }
  }
  return result;
}

}

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

  auto assocs = this->parameters()->associations();
  auto created = result->findComponent("created");
  smtk::operation::MarkGeometry geomMarker(resource);
  for (const auto& assoc : *assocs)
  {
    auto ent = std::dynamic_pointer_cast<smtk::model::Entity>(assoc);
    if (!ent)
    {
      smtkWarningMacro(this->log(), "Skipping " << assoc->name());
      continue;
    }
    // TODO: Check whether ent is a CellSelection and subdivide into per-model
    //       pieces now instead of waiting for the train wreck below.
    auto data = Duplicate::storage(ent);
    vtkSmartPointer<vtkDataObject> dupeDG;
    if (data)
    {
      dupeDG = data->NewInstance();
      dupeDG->DeepCopy(data);
    }
    auto dupe = resource->insertEntityOfTypeAndDimension(ent->entityFlags(), -1)->second;
    auto dupeER = smtk::model::EntityRef(dupe);
    if (dynamic_cast<CellSelection*>(ent.get()))
    {
      // Primitive selections don't have names or owning models.
      dupeER.setName("copy of selection");
      // Primitive selections are restricted to side sets for now.
      Session::setPrimary(*dupe, false);

      // Find parent model from cell IDs.
      auto models = parentModels(dupe, dupeDG);
      if (models.empty())
      {
        smtkErrorMacro(this->log(), "Primitives did not belong to any model.");
      }
      else
      {
        if (models.size() > 1)
        {
          std::ostringstream msg;
          msg << "Primitives belonged to " << models.size() << " models:\n";
          for (const auto& model : models)
          {
            msg << "  " << model.name() << "\n";
          }
          msg << "Placing new cell in " << models.begin()->name() << ".";
          smtkWarningMacro(this->log(), msg.str());
        }
        smtk::model::Model(*models.begin()).addCell(smtk::model::CellEntity(dupe));
      }
    }
    else
    {
      smtk::model::EntityRef src(ent);
      dupeER.setName("copy of " + ent->name());
      duplicateProperties(ent, dupe);
      if (ent->isCellEntity())
      {
        src.owningModel().addCell(dupeER);
      }
      else if (ent->isGroup())
      {
        src.owningModel().addGroup(dupeER);
        // Copy membership to dupe
        auto srcGrp = smtk::model::Group(src);
        auto dstGrp = smtk::model::Group(dupeER);
        dstGrp.setMembershipMask(srcGrp.membershipMask());
        auto members = srcGrp.members<std::set<smtk::model::EntityRef> >();
        dstGrp.addEntities(members);
      }
    }
    if (dupeDG)
    {
      session->addStorage(dupeER.entity(), dupeDG);
      geomMarker.markModified(dupeER.entityRecord());
    }
    created->appendValue(dupeER.entityRecord());
  }

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

const char* Duplicate::xmlDescription() const
{
  return Duplicate_xml;
}

}
}
}
