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

#include "smtk/io/AttributeReader.h"
#include "smtk/io/Logger.h"

#include "smtk/attribute/Attribute.h"
#include "smtk/attribute/Definition.h"
#include "smtk/attribute/IntItem.h"

#include "smtk/model/DefaultSession.h"
#include "smtk/model/Model.h"
#include "smtk/model/Resource.h"
#include "smtk/model/Session.h"
#include "smtk/model/SessionRef.h"

#include "smtk/common/testing/cxx/helpers.h"
#include "smtk/model/testing/cxx/helpers.h"

#include "smtk/Options.h"

// Encoded XML describing the operator classes below.
// These are generated by CMake from unitXXXOperation.sbt.
#include "unitOutcomeOperation_xml.h"

using namespace smtk::operation;
using smtk::attribute::IntItem;

template <typename T>
void printVec(const std::vector<T>& v, const char* typeName, char sep = '\0')
{
  if (v.empty())
    return;
  std::cout << " " << typeName << "(" << v.size() << "): [";
  std::cout << " " << v[0];
  if (sep)
    for (typename std::vector<T>::size_type i = 1; i < v.size(); ++i)
      std::cout << sep << " " << v[i];
  else
    for (typename std::vector<T>::size_type i = 1; i < v.size(); ++i)
      std::cout << " " << v[i];
  std::cout << " ]";
}

int WillOperateWatcher(Operation::EventType event, const Operation& op, void* user)
{
  test(event == smtk::operation::Operation::WILL_OPERATE,
    "WillOperate callback invoked with bad event type");
  int shouldCancel = *reinterpret_cast<int*>(user);
  std::cout << "Operation " << op.name()
            << " about to run: " << (shouldCancel ? "will" : "will not")
            << " request cancellation.\n";
  return shouldCancel;
}

int DidOperateWatcher(
  Operation::EventType event, const Operation& op, Operator::Result result, void* user)
{
  test(event == smtk::operation::Operation::DID_OPERATE,
    "DidOperate callback invoked with bad event type");
  IntItem::Ptr outcome = result->findInt("outcome");
  std::cout << "Operation " << op.name() << " did operate"
            << " (outcome = \"" << outcomeAsString(outcome->value()) << "\")\n";
  // increment the number of times the parameter was modified.
  (*reinterpret_cast<int*>(user))++;
  return 0;
}

void testSessionList(smtk::model::Resource::Ptr resource)
{
  std::cout << "Available sessions\n";
  smtk::model::StringList sessions = resource->sessionTypeNames();
  for (smtk::model::StringList::iterator it = sessions.begin(); it != sessions.end(); ++it)
    std::cout << "  " << *it << "\n";
  std::cout << "\n";
}

class TestOutcomeOperation : public smtk::operation::Operation
{
public:
  smtkTypeMacro(TestOutcomeOperation);
  smtkCreateMacro(TestOutcomeOperation);
  smtkSharedFromThisMacro(Operation);
  smtkDeclareModelOperation();

  static smtk::operation::OperationPtr baseCreate();

  TestOutcomeOperation()
    : m_able(false) // fail operation until told otherwise
  {
  }
  bool ableToOperate() override
  {
    this->ensureSpecification();
    return
      // Force failure?
      m_able && m_specification && m_specification->isValid();
  }
  bool m_able; // Used to force UNABLE_TO_OPERATE result.

protected:
  Operation::Result operateInternal() override
  {
    return this->createResult(this->specification()->findInt("shouldSucceed")->value()
        ? TestOutcomeOperation::OPERATION_SUCCEEDED
        : TestOutcomeOperation::OPERATION_FAILED);
  }
};

// Implementation corresponding to smtkDeclareModelOperation() above.

/* Do not invoke smtkImplementsModelOperation so that we can test
 * post-session-construction registration of operators.
 */
/*
smtkImplementsModelOperation(
  ,
  TestOutcomeOperation,
  OutcomeOp,
  "outcome test",
  unitOutcomeOperation_xml,
  smtk::model::DefaultSession);
 */

smtk::operation::OperationPtr TestOutcomeOperation::baseCreate()
{
  return TestOutcomeOperation::create();
}

std::string TestOutcomeOperation::operatorName("outcome test");
std::string TestOutcomeOperation::className() const
{
  return "TestOutcomeOperation";
}

void testExPostFactoOperationRegistration(smtk::model::Resource::Ptr resource)
{
  // Add a default session.
  smtk::model::SessionRef defSess = resource->createSession("native");

  typedef std::vector<smtk::attribute::DefinitionPtr> OpListType;
  // Add operator descriptions to the default session of our resource.
  smtk::model::SessionPtr session = defSess.session();
  smtk::attribute::DefinitionPtr opBase = session->operatorCollection()->findDefinition("operator");
  OpListType origOpList;
  session->operatorCollection()->derivedDefinitions(opBase, origOpList);

  // Register the operator
  session->registerOperation(
    TestOutcomeOperation::operatorName, unitOutcomeOperation_xml, &TestOutcomeOperator::baseCreate);

  // Now enumerate attribute definitions that inherit "operator".
  OpListType opList;
  session->operatorCollection()->derivedDefinitions(opBase, opList);
  std::cout << "Imported XML for operators:\n";
  for (OpListType::iterator it = opList.begin(); it != opList.end(); ++it)
  {
    std::cout << "  \"" << (*it)->type() << "\"\n";
  }
  std::cout << "\n";

  if (opList.size() != origOpList.size() + 1)
  {
    std::ostringstream err;
    err << "Error: Expected " << (origOpList.size() + 1) << " operator(s), found " << opList.size()
        << "\n";
    std::cerr << err.str();
    throw err.str();
  }
}

void testOperationOutcomes(smtk::model::Resource::Ptr resource)
{
  TestOutcomeOperation::Ptr op = smtk::dynamic_pointer_cast<TestOutcomeOperation>(
    resource->sessions().begin()->op("outcome test"));

  int shouldCancel = 1;
  int numberOfFailedOperations = 0;
  op->observe(smtk::operation::Operation::WILL_OPERATE, WillOperateWatcher, &shouldCancel);
  op->observe(
    smtk::operation::Operation::DID_OPERATE, DidOperateWatcher, &numberOfFailedOperations);

  // Verify that ableToOperate() is called and complains:
  test(op->operate()->findInt("outcome")->value() == smtk::operation::Operation::UNABLE_TO_OPERATE,
    "Should have been unable to operate.");
  std::cout << "Operation " << op->name() << " should be unable to operate"
            << " (outcome = \"" << outcomeAsString(op->operate()->findInt("outcome")->value())
            << "\""
            << ").\n--\n";

  // Verify that the WILL_OPERATE observer is called and cancels the operation:
  op->m_able = true;
  test(op->operate()->findInt("outcome")->value() == smtk::operation::Operation::OPERATION_CANCELED,
    "Operation should have been canceled.");
  std::cout << "--\n";

  // Verify that the operation fails when "shouldSucceed" parameter is zero.
  shouldCancel = 0;
  test(op->operate()->findInt("outcome")->value() == smtk::operation::Operation::OPERATION_FAILED,
    "Operation should have failed.");
  std::cout << "--\n";

  // Verify that removing observer bypasses cancellation.
  op->unobserve(smtk::operation::Operation::WILL_OPERATE, WillOperateWatcher, &shouldCancel);
  shouldCancel = 1; // Force cancellation if observer wasn't removed.

  // Verify that the parameter-change callback is invoked.
  op->specification()->findInt("shouldSucceed")->setValue(1);

  // Verify that the operation succeeds when "shouldSucceed" parameter is non-zero.
  test(
    op->operate()->findInt("outcome")->value() == smtk::operation::Operation::OPERATION_SUCCEEDED,
    "Operation should have succeeded.");
  std::cout << "--\n";

  // Now test values passed to callbacks:
  test(numberOfFailedOperations > 0, "Expected more operator failures.");

  op->unobserve(
    smtk::operation::Operation::DID_OPERATE, DidOperateWatcher, &numberOfFailedOperations);
}

void testSessionAssociation(smtk::model::Resource::Ptr resource)
{
  // Test that operators added by previous tests still exist
  smtk::model::Model model = resource->addModel(3, 3, "Model Airplane");
  model.setSession(*resource->sessions().begin());
  smtk::model::StringList modelOpNames = model.operatorNames();
  test(std::find(modelOpNames.begin(), modelOpNames.end(), "outcome test") != modelOpNames.end(),
    "Expected \"outcome test\" operator defined for the test model.");

  // Test op(name) method
  smtk::operation::Operation::Ptr op = model.op("outcome test");
  test(op ? 1 : 0, "Model::op(\"outcome test\") returned a \"null\" shared pointer.");

  // Test Operation->Session association
  test(
    op->session() == resource->sessions().begin()->session(), "Bad session reported by operator.");

  // Test Operation->Resource association
  test(op->resource() == resource, "Bad resource reported by operator.");

  // Test operatorNames()
  smtk::model::StringList opNames = model.session().operatorNames();
  std::cout << "Printing";
  printVec(opNames, "operator names", ',');
  std::cout << "\n";
}

int main()
{
  int status = 0;

  smtk::model::Resource::Ptr resource = smtk::model::Resource::create();

  try
  {

    testSessionList(resource);
    testExPostFactoOperationRegistration(resource);
    testOperationOutcomes(resource);
    testSessionAssociation(resource);
  }
  catch (const std::string& msg)
  {
    (void)msg; // Ignore the message; it's already been printed.
    std::cerr << "Exiting...\n";
    status = -1;
  }

  return status;
}
