//=========================================================================
//  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/Predicates.h"
#include "smtk/session/aeva/Registrar.h"
#include "smtk/session/aeva/Resource.h"
#include "smtk/session/aeva/Session.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/Read.h"

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

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

#include "smtk/resource/Manager.h"

#include "smtk/operation/Manager.h"

#include "vtkDataObject.h"
#include "vtkDataSetAttributes.h"
#include "vtkIntArray.h"

namespace
{
std::string dataRoot = AEVA_DATA_DIR;

smtk::resource::Manager::Ptr resourceManager;
smtk::operation::Manager::Ptr operationManager;

std::shared_ptr<smtk::session::aeva::Resource> LoadTestModel()
{
  std::shared_ptr<smtk::session::aeva::Resource> resource;
  // Create a read operation
  smtk::session::aeva::Read::Ptr readOp = operationManager->create<smtk::session::aeva::Read>();
  if (!readOp)
  {
    std::cerr << "  No read operation\n";
    return resource;
  }

  // Set the file path
  std::string testFilePath(dataRoot);
  testFilePath += "/smtk/simple.smtk";
  readOp->parameters()->findFile("filename")->setValue(testFilePath);

  // Test the ability to operate
  if (!readOp->ableToOperate())
  {
    std::cerr << "  Import operation unable to operate\n";
    return resource;
  }

  // Execute the operation
  smtk::operation::Operation::Result readOpResult = readOp->operate();
  if (readOpResult->findInt("outcome")->value() !=
    static_cast<int>(smtk::operation::Operation::Outcome::SUCCEEDED))
  {
    std::cerr << "  Read operation failed\n";
    return resource;
  }

  auto resourceItem = readOpResult->findResource("resource");
  resource = std::dynamic_pointer_cast<smtk::session::aeva::Resource>(resourceItem->value());
  if (!resource || !resource->session())
  {
    std::cerr << "  Resulting resource is invalid\n";
    return resource;
  }

  return resource;
}

bool GetTestEntities(const smtk::session::aeva::Resource::Ptr& resource,
  std::shared_ptr<smtk::model::Entity>& primary,
  std::shared_ptr<smtk::model::Entity>& all,
  std::shared_ptr<smtk::model::Entity>& lf,
  std::shared_ptr<smtk::model::Entity>& md,
  std::shared_ptr<smtk::model::Entity>& rt)
{
  smtk::resource::Component::Visitor visitor =
    [&](const smtk::resource::Component::Ptr& comp) -> void {
    std::string name;
    auto ent = std::dynamic_pointer_cast<smtk::model::Entity>(comp);
    if (ent)
    {
      name = ent->name();
      if (name == "primary")
      {
        primary = ent;
      }
      else if (name == "all")
      {
        all = ent;
      }
      else if (name == "lf")
      {
        lf = ent;
      }
      else if (name == "md")
      {
        md = ent;
      }
      else if (name == "rt")
      {
        rt = ent;
      }
    }
  };
  resource->visit(visitor);
  return primary && all && lf && md && rt;
}

int TestBooleanIntersect(bool keepInputs)
{
  std::cout << "Testing intersection, " << (keepInputs ? "keeping inputs" : "replacing inputs")
            << "\n";
  auto resource = LoadTestModel();
  std::shared_ptr<smtk::model::Entity> primary;
  std::shared_ptr<smtk::model::Entity> all;
  std::shared_ptr<smtk::model::Entity> lf;
  std::shared_ptr<smtk::model::Entity> md;
  std::shared_ptr<smtk::model::Entity> rt;
  if (!GetTestEntities(resource, primary, all, lf, md, rt))
  {
    return 1;
  }

  auto boolOp = operationManager->create<smtk::session::aeva::BooleanIntersect>();
  if (!boolOp)
  {
    std::cerr << "  No intersect operation.\n";
    return 1;
  }

  auto assoc = boolOp->parameters()->associations();
  if (!assoc->appendValue(all))
  {
    std::cerr << "  Could not add 'all'\n";
    return 1;
  }
  if (!assoc->appendValue(primary))
  {
    std::cerr << "  Could not add 'primary'\n";
    return 1;
  }
  // Verify that intersect currently only accepts 2 operands.
  if (assoc->appendValue(lf))
  {
    std::cerr << "  Could add superfluous 'lf'\n";
    return 1;
  }

  boolOp->parameters()->findVoid("keep inputs")->setIsEnabled(keepInputs);

  // Test expected failure.
  auto result = boolOp->operate();
  if (result->findInt("outcome")->value() !=
    static_cast<int>(smtk::operation::Operation::Outcome::UNABLE_TO_OPERATE))
  {
    std::cerr << "  Boolean operation " << boolOp->parameters()->name()
              << " did not fail when passed primary geometry\n";
    return 1;
  }

  // Fix parameter and try again.
  for (int ii = 0; ii < 2; ++ii)
  {
    if (assoc->value(ii)->name() == "primary")
    {
      assoc->setValue(ii, md);
    }
  }
  result = boolOp->operate();
  if (result->findInt("outcome")->value() !=
    static_cast<int>(smtk::operation::Operation::Outcome::SUCCEEDED))
  {
    std::cerr << "  Boolean operation " << boolOp->parameters()->name() << " failed\n";
    return 1;
  }

  vtkSmartPointer<vtkDataObject> geom;
  if (keepInputs)
  {
    auto created = result->findComponent("created");
    if (created->numberOfValues() != 1)
    {
      std::cerr << "  Created " << created->numberOfValues() << " components, not 1.\n";
      return 1;
    }
    auto comp = std::dynamic_pointer_cast<smtk::model::Entity>(created->value());
    if (!comp)
    {
      std::cerr << "  Invalid created component.\n";
      return 1;
    }
    geom = resource->session()->findStorage(comp->id());
  }
  else
  {
    auto modified = result->findComponent("modified");
    if (modified->numberOfValues() != 1)
    {
      std::cerr << "  Modified " << modified->numberOfValues() << " components, not 1.\n";
      return 1;
    }
    auto comp = std::dynamic_pointer_cast<smtk::model::Entity>(modified->value());
    if (!comp)
    {
      std::cerr << "  Invalid modified component.\n";
      return 1;
    }
    geom = resource->session()->findStorage(comp->id());
  }
  auto* gids = vtkIntArray::SafeDownCast(
    geom ? geom->GetAttributes(vtkDataObject::CELL)->GetGlobalIds() : nullptr);
  if (!geom || !gids)
  {
    std::cerr << "  Resulting component has no VTK data (" << geom << ") or no ids.\n";
    return 1;
  }

  if (gids->GetNumberOfTuples() != 2)
  {
    std::cerr << "  Created component has " << gids->GetNumberOfTuples() << " tuples, not 2.\n";
    return 1;
  }

  std::set<vtkIdType> expected{ 2, 3 };
  for (vtkIdType ii = 0; ii < 2; ++ii)
  {
    if (expected.find(gids->GetValue(ii)) == expected.end())
    {
      std::cerr << "  Unexpected global id " << gids->GetValue(ii) << ".\n";
      return 1;
    }
  }

  std::cout << "  All expected cells present in boolean result.\n";
  resourceManager->remove(resource);
  return 0;
}

int TestBooleanSubtract(bool keepInputs)
{
  std::cout << "Testing subtraction, " << (keepInputs ? "keeping inputs" : "replacing inputs")
            << "\n";
  auto resource = LoadTestModel();
  std::shared_ptr<smtk::model::Entity> primary;
  std::shared_ptr<smtk::model::Entity> all;
  std::shared_ptr<smtk::model::Entity> lf;
  std::shared_ptr<smtk::model::Entity> md;
  std::shared_ptr<smtk::model::Entity> rt;
  if (!GetTestEntities(resource, primary, all, lf, md, rt))
  {
    return 1;
  }

  auto boolOp = operationManager->create<smtk::session::aeva::BooleanSubtract>();
  if (!boolOp)
  {
    std::cerr << "  No subtract operation.\n";
    return 1;
  }

  auto assoc = boolOp->parameters()->associations();
  if (!assoc->appendValue(all))
  {
    std::cerr << "  Could not add 'all'\n";
    return 1;
  }
  // Verify that subtract currently only accepts 1 workpiece operand.
  if (assoc->appendValue(md))
  {
    std::cerr << "  Could add superfluous 'md'\n";
    return 1;
  }

  auto tools = boolOp->parameters()->findComponent("tools");
  if (!tools->appendValue(md))
  {
    std::cerr << "  Could not add 'md'\n";
    return 1;
  }
  // Verify that subtract currently only accepts 1 workpiece operand.
  if (tools->appendValue(rt))
  {
    std::cerr << "  Could add superfluous 'rt'\n";
    return 1;
  }

  boolOp->parameters()->findVoid("keep inputs")->setIsEnabled(keepInputs);

  auto result = boolOp->operate();
  if (result->findInt("outcome")->value() !=
    static_cast<int>(smtk::operation::Operation::Outcome::SUCCEEDED))
  {
    std::cerr << "  Boolean operation " << boolOp->parameters()->name() << " failed\n";
    return 1;
  }

  vtkSmartPointer<vtkDataObject> geom;
  if (keepInputs)
  {
    auto created = result->findComponent("created");
    if (created->numberOfValues() != 1)
    {
      std::cerr << "  Created " << created->numberOfValues() << " components, not 1.\n";
      return 1;
    }
    auto comp = std::dynamic_pointer_cast<smtk::model::Entity>(created->value());
    if (!comp)
    {
      std::cerr << "  Invalid created component.\n";
      return 1;
    }
    geom = resource->session()->findStorage(comp->id());
  }
  else
  {
    auto modified = result->findComponent("modified");
    if (modified->numberOfValues() != 1)
    {
      std::cerr << "  Modified " << modified->numberOfValues() << " components, not 1.\n";
      return 1;
    }
    auto comp = std::dynamic_pointer_cast<smtk::model::Entity>(modified->value());
    if (!comp)
    {
      std::cerr << "  Invalid modified component.\n";
      return 1;
    }
    geom = resource->session()->findStorage(comp->id());
  }
  auto* gids = vtkIntArray::SafeDownCast(
    geom ? geom->GetAttributes(vtkDataObject::CELL)->GetGlobalIds() : nullptr);
  if (!geom || !gids)
  {
    std::cerr << "  Created component has no VTK data (" << geom << ") or no ids.\n";
    return 1;
  }

  if (gids->GetNumberOfTuples() != 4)
  {
    std::cerr << "  Created component has " << gids->GetNumberOfTuples() << " tuples, not 4.\n";
    return 1;
  }

  std::set<vtkIdType> expected{ 0, 1, 4, 5 };
  for (vtkIdType ii = 0; ii < 4; ++ii)
  {
    if (expected.find(gids->GetValue(ii)) == expected.end())
    {
      std::cerr << "  Unexpected global id " << gids->GetValue(ii) << ".\n";
      return 1;
    }
  }

  std::cout << "  All expected cells present in boolean result.\n";
  resourceManager->remove(resource);
  return 0;
}

int TestBooleanUnite(bool keepInputs)
{
  std::cout << "Testing union, " << (keepInputs ? "keeping inputs" : "replacing inputs") << "\n";
  auto resource = LoadTestModel();
  std::shared_ptr<smtk::model::Entity> primary;
  std::shared_ptr<smtk::model::Entity> all;
  std::shared_ptr<smtk::model::Entity> lf;
  std::shared_ptr<smtk::model::Entity> md;
  std::shared_ptr<smtk::model::Entity> rt;
  if (!GetTestEntities(resource, primary, all, lf, md, rt))
  {
    return 1;
  }

  auto boolOp = operationManager->create<smtk::session::aeva::BooleanUnite>();
  if (!boolOp)
  {
    std::cerr << "  No unite operation.\n";
    return 1;
  }

  auto assoc = boolOp->parameters()->associations();
  if (!assoc->appendValue(lf))
  {
    std::cerr << "  Could not add 'lf'\n";
    return 1;
  }
  if (!assoc->appendValue(rt))
  {
    std::cerr << "  Could not add 'rt'\n";
    return 1;
  }
  // Verify that unite currently only accepts 2 operands.
  if (assoc->appendValue(md))
  {
    std::cerr << "  Could add superfluous 'md'\n";
    return 1;
  }

  boolOp->parameters()->findVoid("keep inputs")->setIsEnabled(keepInputs);

  auto result = boolOp->operate();
  if (result->findInt("outcome")->value() !=
    static_cast<int>(smtk::operation::Operation::Outcome::SUCCEEDED))
  {
    std::cerr << "  Boolean operation " << boolOp->parameters()->name() << " failed\n";
    return 1;
  }

  vtkSmartPointer<vtkDataObject> geom;
  if (keepInputs)
  {
    auto created = result->findComponent("created");
    if (created->numberOfValues() != 1)
    {
      std::cerr << "  Created " << created->numberOfValues() << " components, not 1.\n";
      return 1;
    }
    auto comp = std::dynamic_pointer_cast<smtk::model::Entity>(created->value());
    if (!comp)
    {
      std::cerr << "  Invalid created component.\n";
      return 1;
    }
    geom = resource->session()->findStorage(comp->id());
  }
  else
  {
    auto modified = result->findComponent("modified");
    if (modified->numberOfValues() != 1)
    {
      std::cerr << "  Modified " << modified->numberOfValues() << " components, not 1.\n";
      return 1;
    }
    auto comp = std::dynamic_pointer_cast<smtk::model::Entity>(modified->value());
    if (!comp)
    {
      std::cerr << "  Invalid modified component.\n";
      return 1;
    }
    geom = resource->session()->findStorage(comp->id());
  }

  auto* gids = vtkIntArray::SafeDownCast(
    geom ? geom->GetAttributes(vtkDataObject::CELL)->GetGlobalIds() : nullptr);
  if (!geom || !gids)
  {
    std::cerr << "  Created component has no VTK data (" << geom << ") or no ids.\n";
    return 1;
  }

  if (gids->GetNumberOfTuples() != 4)
  {
    std::cerr << "  Created component has " << gids->GetNumberOfTuples() << " tuples, not 4.\n";
    return 1;
  }

  std::set<vtkIdType> expected{ 0, 1, 4, 5 };
  for (vtkIdType ii = 0; ii < 4; ++ii)
  {
    if (expected.find(gids->GetValue(ii)) == expected.end())
    {
      std::cerr << "  Unexpected global id " << gids->GetValue(ii) << ".\n";
      return 1;
    }
  }

  std::cout << "  All expected cells present in boolean result.\n";
  resourceManager->remove(resource);
  return 0;
}

}

int TestBooleans(int argc, char* argv[])
{
  (void)argc;
  (void)argv;
  int result = 0;

  resourceManager = smtk::resource::Manager::create();
  {
    smtk::session::aeva::Registrar::registerTo(resourceManager);
  }
  operationManager = smtk::operation::Manager::create();
  {
    smtk::session::aeva::Registrar::registerTo(operationManager);
  }
  operationManager->registerResourceManager(resourceManager);

  result |= TestBooleanIntersect(true);
  result |= TestBooleanIntersect(false);
  result |= TestBooleanSubtract(true);
  result |= TestBooleanSubtract(false);
  result |= TestBooleanUnite(true);
  result |= TestBooleanUnite(false);

  return result;
}
