//=========================================================================
//  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.
//=========================================================================

// Local includes
#include "smtk/simulation/ace3p/Metadata.h"
#include "smtk/simulation/ace3p/Project.h"
#include "smtk/simulation/ace3p/Registrar.h"
#include "smtk/simulation/ace3p/operations/Create.h"
#include "smtk/simulation/ace3p/operations/NewStage.h"
#include "smtk/simulation/ace3p/operations/Write.h"

// SMTK includes
#include "smtk/attribute/Attribute.h"
#include "smtk/attribute/DirectoryItem.h"
#include "smtk/attribute/FileItem.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/Registrar.h"
#include "smtk/attribute/Resource.h"
#include "smtk/attribute/ResourceItem.h"
#include "smtk/attribute/StringItem.h"
#include "smtk/model/Registrar.h"
#include "smtk/model/Resource.h"
#include "smtk/operation/Manager.h"
#include "smtk/operation/Operation.h"
#include "smtk/project/Manager.h"
#include "smtk/project/Registrar.h"
#include "smtk/resource/Manager.h"
#include "smtk/session/vtk/Registrar.h"

#include <boost/filesystem.hpp>

#include <cassert>
#include <iostream>
#include <sstream>

const int OP_SUCCEEDED = static_cast<int>(smtk::operation::Operation::Outcome::SUCCEEDED);

std::shared_ptr<smtk::simulation::ace3p::Project> buildRfGunProject(
  smtk::operation::ManagerPtr opManager,
  const std::string& projectDirectory,
  const boost::filesystem::path& dataPath);
std::shared_ptr<smtk::simulation::ace3p::Project> buildWindowProject(
  smtk::operation::ManagerPtr opManager,
  const std::string& projectDirectory,
  const boost::filesystem::path& dataPath);
std::shared_ptr<smtk::simulation::ace3p::Project> buildCustomProject(
  smtk::operation::ManagerPtr opManager,
  const std::string& projectDirectory,
  const boost::filesystem::path& dataPath,
  int argc,
  char* argv[],
  int nonModelArgs);

int main(int argc, char* argv[])
{
  if (argc == 1)
  {
    std::cout << "This is a tool for creation of an ACE3P test project." << std::endl;
    std::cout << std::endl;
    std::cout << "Usage examples:" << std::endl;
    std::cout << "    createProject [-s1 | -s2] [-o path-for-new-project]" << std::endl;
    std::cout << "    createProject [-f] [-o path-for-new-project] [path-to-model1] "
                 "[path-to-model2] ..."
              << std::endl;
    std::cout << std::endl;
    std::cout << "Possible arguments:" << std::endl;
    std::cout << "-s1        : output the RfGun project" << std::endl;
    std::cout << "-s2        : output the Window project" << std::endl;
    std::cout << "-f         : force overwrite of old project directory contents" << std::endl;
    std::cout << "-o <path>  : specify the output project path" << std::endl;
    std::cout << "If creating a custom project, you must specify both the -o <path> and list the "
                 "model path(s) as the last argument(s) of the command. Otherwise, the -o <path> "
                 "is optional. "
              << std::endl;

    return 0;
  }

  std::stringstream ss;
  ss << SCRATCH_DIR << "/cxx/";

  // process the input arguments
  int nonModelArgs = 1; // starts with one, for the program name
  int standardProjectCount = 0;
  int oArgIndex = -9;
  bool bS1ArgumentPresent = false;
  bool bS2ArgumentPresent = false;
  bool bOArgumentPresent = false;
  bool bForceOverwrite = false;
  for (int i = 1; i < argc; i++)
  {
    if (strcmp(argv[i], "-s1") == 0)
    {
      bS1ArgumentPresent = true;
      standardProjectCount++;
      nonModelArgs++;
    }
    else if (strcmp(argv[i], "-s2") == 0)
    {
      bS2ArgumentPresent = true;
      standardProjectCount++;
      nonModelArgs++;
    }
    else if (strcmp(argv[i], "-o") == 0)
    {
      bOArgumentPresent = true;
      oArgIndex = i;
      nonModelArgs += 2; // include the path argument in this tally
    }
    else if (strcmp(argv[i], "-f") == 0)
    {
      bForceOverwrite = true;
      nonModelArgs++;
    }
  }

  // validate the command line input
  if (standardProjectCount == 0)
  {
    if (!bOArgumentPresent)
    {
      std::cout << "ERROR: You appear to want create a custom project, but have not specifed the "
                   "output path"
                << std::endl;
      return 1;
    }
    if (nonModelArgs == argc)
    {
      std::cout << "ERROR: You must specify at least one input model to create a custom project"
                << std::endl;
      return 1;
    }
  }
  if (standardProjectCount == 1)
  {
    if (nonModelArgs < argc)
    {
      std::cout << "ERROR: You cannot specify input models for a standard project model (-s1/-s2)"
                << std::endl;
      return 1;
    }
  }
  if (standardProjectCount == 2)
  {
    std::cout << "ERROR: You cannot create more than project at a time" << std::endl;
    return 1;
  }

  int standardProject = -1;
  if (bS1ArgumentPresent)
  {
    std::cout << "Generating RfGun project..." << std::endl;
    standardProject = 0;
    ss << "RfGun";
  }
  else if (bS2ArgumentPresent)
  {
    std::cout << "Generating Window project..." << std::endl;
    standardProject = 1;
    ss << "Window";
  }
  else
  {
    if (argc < 3)
    {
      std::cout << "Line " << __LINE__ << " Not enough arguments for a custom project" << std::endl;
      return 1;
    }
    std::cout << "Generating custom project..." << std::endl;
    ss << argv[1];
  }

  // use the user-specified output directory, if present
  if (bOArgumentPresent)
  {
    ss.str(""); // clear the stringstream

    // convert the supplied path into a canonical-ish path (in case it's a relative path)
    std::string projectPath = argv[oArgIndex + 1];
    boost::filesystem::path absolutePath = boost::filesystem::absolute(projectPath).string();
    std::string canonicalishPath = absolutePath.lexically_normal().string();
    ss << canonicalishPath;
  }

  std::string projectDirectory = ss.str();

  // test if output folder already exists
  if (boost::filesystem::is_directory(projectDirectory))
  {
    bool bOverwrite = false;
    if (bForceOverwrite)
    {
      bOverwrite = true;
    }
    else
    {
      std::cout << "The output directory already exists.  Would you like to overwrite it? (Y/n) ";
      char response;
      std::cin >> response;
      if (response == 'Y' || response == 'y')
      {
        bOverwrite = true;
      }
      else
      {
        std::cout << "Project creation aborted" << std::endl;
        return 1;
      }
    }

    if (bOverwrite)
    {
      std::cout << "Line " << __LINE__ << ": erasing current project directory" << std::endl;
      boost::filesystem::remove_all(projectDirectory);
    }
  }
  std::cout << "Line " << __LINE__ << ": project directory " << projectDirectory << std::endl;

  // Create smtk managers
  smtk::resource::ManagerPtr resManager = smtk::resource::Manager::create();
  smtk::attribute::Registrar::registerTo(resManager);
  smtk::model::Registrar::registerTo(resManager);
  smtk::session::vtk::Registrar::registerTo(resManager);

  smtk::operation::ManagerPtr opManager = smtk::operation::Manager::create();
  smtk::operation::Registrar::registerTo(opManager);
  smtk::attribute::Registrar::registerTo(opManager);
  smtk::session::vtk::Registrar::registerTo(opManager);

  smtk::project::ManagerPtr projectManager = smtk::project::Manager::create(resManager, opManager);
  smtk::project::Registrar::registerTo(projectManager);
  smtk::simulation::ace3p::Registrar::registerTo(projectManager);

// Application must set workflows directory
#ifndef WORKFLOWS_SOURCE_DIR
#error WORKFLOWS_SOURCE_DIR must be defined
#endif
  smtk::simulation::ace3p::Metadata::WORKFLOWS_DIRECTORY = WORKFLOWS_SOURCE_DIR;

  // Data directory is set by CMake (target_compile_definitions)
  boost::filesystem::path dataPath(DATA_DIR);

  // Create project
  std::shared_ptr<smtk::simulation::ace3p::Project> project;
  if (standardProject == 0)
  {
    project = buildRfGunProject(opManager, projectDirectory, dataPath);
  }
  else if (standardProject == 1)
  {
    project = buildWindowProject(opManager, projectDirectory, dataPath);
  }
  else
  {
    project = buildCustomProject(opManager, projectDirectory, dataPath, argc, argv, nonModelArgs);
  }

  // Write project to file system
  std::cout << "Line " << __LINE__ << ": writing project" << std::endl;
  auto writeOp = opManager->create<smtk::simulation::ace3p::Write>();
  writeOp->parameters()->associate(project);
  auto result = writeOp->operate();
  int outcome = result->findInt("outcome")->value();
  assert(outcome == OP_SUCCEEDED);

  std::cout << "finis" << std::endl;
  return 0;
}

std::map<std::string, std::shared_ptr<smtk::model::Resource>> fileResourceMap;

std::shared_ptr<smtk::simulation::ace3p::Project> createProject(
  smtk::operation::ManagerPtr opManager,
  const std::string& projectDirectory,
  const std::string& meshPath)
{
  auto createOp = opManager->create<smtk::simulation::ace3p::Create>();
  smtk::attribute::AttributePtr params = createOp->parameters();
  params->findDirectory("location")->setValue(projectDirectory);
  params->findString("stage-name")->setValue("Stage1");

  auto fileItem = params->findFile("analysis-mesh");
  fileItem->setIsEnabled(true);
  fileItem->setValue(meshPath);

  auto result = createOp->operate();
  int outcome = result->findInt("outcome")->value();
  assert(outcome == OP_SUCCEEDED);

  smtk::attribute::ResourceItemPtr projectItem = result->findResource("resource");
  auto resourceTEMP = projectItem->value();
  std::shared_ptr<smtk::simulation::ace3p::Project> project =
    std::dynamic_pointer_cast<smtk::simulation::ace3p::Project>(resourceTEMP);
  assert(project != nullptr);

  // store the Mesh in the map, in case it is re-used by other stages
  std::shared_ptr<smtk::model::Resource> resource = project->stage(0)->modelResource();
  fileResourceMap.insert(std::make_pair(meshPath, resource));

  return project;
}

void addStage(
  std::shared_ptr<smtk::simulation::ace3p::Project> project,
  smtk::operation::ManagerPtr opManager,
  const std::string& meshPath,
  const std::string& stageName)
{
  auto stageOp = opManager->create<smtk::simulation::ace3p::NewStage>();
  smtk::attribute::AttributePtr params = stageOp->parameters();
  params->associate(project);
  params->findString("stage-name")->setValue(stageName);

  // re-use the Resource for a given Mesh, if it has already been loaded
  if (fileResourceMap.find(meshPath) != fileResourceMap.end())
  {
    params->findString("assign-mesh")->setValue("link-existing-mesh");
    std::shared_ptr<smtk::model::Resource> resource = fileResourceMap[meshPath];
    /*auto resourceItem = */ params->findResource("existing-analysis-mesh")->setValue(resource);
  }
  else
  {
    params->findString("assign-mesh")->setValue("open-new-mesh");
    /*auto fileItem = */ params->findFile("analysis-mesh-file")->setValue(meshPath);

    std::shared_ptr<smtk::model::Resource> resource = project->stage(0)->modelResource();
    fileResourceMap.insert(std::make_pair(meshPath, resource));
  }

  auto result = stageOp->operate();
  int outcome = result->findInt("outcome")->value();
  assert(outcome == OP_SUCCEEDED);
}

std::shared_ptr<smtk::simulation::ace3p::Project> buildRfGunProject(
  smtk::operation::ManagerPtr opManager,
  const std::string& projectDirectory,
  const boost::filesystem::path& dataPath)
{
  // Create Project (and add first stage)
  std::cout << "Line " << __LINE__ << ": creating RfGun project" << std::endl;
  boost::filesystem::path meshPath = dataPath / "model" / "3d" / "genesis" / "RfGunVacuum.gen";
  std::shared_ptr<smtk::simulation::ace3p::Project> project =
    createProject(opManager, projectDirectory, meshPath.string());

  // Add second stage
  std::cout << "Line " << __LINE__ << ": creating second stage" << std::endl;
  meshPath = dataPath / "model" / "3d" / "genesis" / "RfGunBody.gen";
  addStage(project, opManager, meshPath.string(), "Stage2");

  return project;
}

std::shared_ptr<smtk::simulation::ace3p::Project> buildWindowProject(
  smtk::operation::ManagerPtr opManager,
  const std::string& projectDirectory,
  const boost::filesystem::path& dataPath)
{

  // Create Project (and add first stage)
  std::cout << "Line " << __LINE__ << ": creating Window project" << std::endl;
  boost::filesystem::path meshPath = dataPath / "model" / "3d" / "netcdf" / "Window.ncdf";
  std::shared_ptr<smtk::simulation::ace3p::Project> project =
    createProject(opManager, projectDirectory, meshPath.string());

  // Add second stage
  std::cout << "Line " << __LINE__ << ": creating second stage" << std::endl;
  addStage(project, opManager, meshPath.string(), "Stage2");

  return project;
}

std::shared_ptr<smtk::simulation::ace3p::Project> buildCustomProject(
  smtk::operation::ManagerPtr opManager,
  const std::string& projectDirectory,
  const boost::filesystem::path& dataPath,
  int argc,
  char* argv[],
  int nonModelArgs)
{
  int nModels = argc - nonModelArgs;

  std::shared_ptr<smtk::simulation::ace3p::Project> project;
  for (int i = 0; i < nModels; i++)
  {
    // convert the supplied path into a canonical-ish path (in case it's a relative path)
    std::string meshPath = argv[i + nonModelArgs];
    boost::filesystem::path absolutePath = boost::filesystem::absolute(meshPath).string();
    std::string canonicalishPath = absolutePath.lexically_normal().string();

    if (i == 0) // create project on 1st pass
    {
      std::cout << "Line " << __LINE__ << ": creating custom project" << std::endl;
      project = createProject(opManager, projectDirectory, canonicalishPath);
    }
    else // otherwise, add new stage & model
    {
      std::string stageName = "Stage" + std::to_string(i + 1);
      std::cout << "Line " << __LINE__ << ": creating " << stageName << std::endl;

      addStage(project, opManager, canonicalishPath, stageName);
    }
  }

  return project;
}
