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

#include "smtk/attribute/FileItem.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/common/CompilerInformation.h"
#include "smtk/common/UUID.h"
#include "smtk/io/LoadJSON.h"
#include "smtk/io/SaveJSON.h"
#include "smtk/io/SaveJSON.txx"
#include "smtk/model/Manager.h"
#include "smtk/model/Operator.h"

SMTK_THIRDPARTY_PRE_INCLUDE
#include "boost/filesystem.hpp"
SMTK_THIRDPARTY_POST_INCLUDE

#include "cJSON.h"

using namespace boost::filesystem;

namespace smtk
{
namespace model
{

/**\brief Populate \a jsonSession with records from the subset of \a models owned by \a sref.
  */
int SessionIOJSON::saveJSON(cJSON* jsonSession, const SessionRef& sref, const Models& models)
{
  if (models.empty())
  {
    return 1; // no models => silent failure.
  }

  smtk::model::Manager::Ptr mgr = models.begin()->manager();
  smtk::model::Models modelsOfSession;
  for (auto model : models)
  {
    if (model.owningSession() == sref)
    {
      modelsOfSession.push_back(model);
    }
  }
  return smtk::io::SaveJSON::addModelsRecord(mgr, modelsOfSession, jsonSession);
}

/**\brief Decode information from \a sessionRec for the given \a modelMgr.
  *
  * Subclasses should return 1 on success and 0 on failure.
  */
int SessionIOJSON::importJSON(
  ManagerPtr modelMgr, const SessionPtr& session, cJSON* sessionRec, bool loadNativeModels)
{
  smtk::common::UUIDs models = modelMgr->entitiesMatchingFlags(smtk::model::MODEL_ENTITY);

  cJSON* modelsObj = cJSON_GetObjectItem(sessionRec, "models");
  if (!modelsObj)
  {
    smtkInfoMacro(modelMgr->log(), "Expecting a \"models\" entry!");
    return 0;
  }

  std::map<smtk::common::UUID, std::string> existingURLs;
  cJSON* modelentry;
  // import all native models model entites, should only have meta info
  for (modelentry = modelsObj->child; modelentry; modelentry = modelentry->next)
  {
    if (!modelentry->string || !modelentry->string[0])
      continue;

    smtk::common::UUID modelid = smtk::common::UUID(modelentry->string);
    if (modelid.isNull())
    {
      smtkInfoMacro(modelMgr->log(), "Invalid model uuid, skipping!");
      continue;
    }
    // import native model if the model does not exist;
    // NOTE: what should we do if it already exists? erase then re-load
    // the original model from file (stored in string property "url")?
    // Else, just import meta info
    if (models.find(modelid) == models.end())
    {
      // find the model entry, and get the native model file name if it exists,
      // by looking at "url" property
      for (cJSON* curChild = modelentry->child; curChild; curChild = curChild->next)
      {
        if (!curChild->string || !curChild->string[0])
        {
          continue;
        }
        // find the model id in dictionary
        if (smtk::common::UUID(curChild->string) != modelid)
        {
          continue;
        }
        // failed to load properties is still OK
        smtk::io::LoadJSON::ofManagerStringProperties(modelid, curChild, modelMgr);
        break;
      }

      if (loadNativeModels)
      {
        std::string nativemodelfile;
        std::string nativefilekey = modelMgr->hasStringProperty(modelid, "url") ? "url" : "";
        if (!nativefilekey.empty())
        {
          smtk::model::StringList const& nprop(modelMgr->stringProperty(modelid, nativefilekey));
          if (!nprop.empty())
          {
            nativemodelfile = nprop[0];
          }
        }

        if (!nativemodelfile.empty())
        {
          // Prefer reference path if filename is relative:
          ::boost::filesystem::path nativeFile(nativemodelfile);
          if (!this->referencePath().empty() && nativeFile.is_relative())
          {
            ::boost::filesystem::path root(this->referencePath());
            nativeFile = root / nativeFile;
            if (::boost::filesystem::exists(nativeFile))
            {
              nativemodelfile = nativeFile.string();
            }
          }
          // failed to load native model is still ok;
          // If the model is loaded successfully, we need to cache the loadedURL so that we
          // can recover it because loadModelsRecord may set the "url" to something else
          std::string loadedURL;
          if (this->loadNativeModel(modelMgr, session, nativemodelfile, loadedURL))
          {
            existingURLs[modelid] = loadedURL;
          }
        }
      }
    }
    else if (modelMgr->hasStringProperty(modelid, "url"))
    {
      smtk::model::StringList const& nprop(modelMgr->stringProperty(modelid, "url"));
      if (!nprop.empty())
      {
        existingURLs[modelid] = nprop[0];
      }
    }
  }
  int status = this->loadModelsRecord(modelMgr, sessionRec);
  status &= this->loadMeshesRecord(modelMgr, sessionRec);
  // recover "url" property for models already loaded
  std::map<smtk::common::UUID, std::string>::const_iterator mit;
  for (mit = existingURLs.begin(); mit != existingURLs.end(); ++mit)
  {
    modelMgr->setStringProperty(mit->first, "url", mit->second);
  }
  return status;
}

/**\brief Parse models info from \a sessionRec for the given \a modelMgr.
  *
  * Subclasses should return 1 on success and 0 on failure.
  */
int SessionIOJSON::loadModelsRecord(ManagerPtr modelMgr, cJSON* sessionRec)
{
  cJSON* modelsObj = cJSON_GetObjectItem(sessionRec, "models");
  if (!modelsObj)
  {
    // It's okay if we don't have "models" entry. Could be a record that
    // did not come from SessionIOJSON;
    smtkInfoMacro(modelMgr->log(), "Expecting a \"models\" entry!");
    return 1;
  }

  int status = 1;
  cJSON* modelentry;
  // import all native models model entites, should only have meta info
  for (modelentry = modelsObj->child; modelentry; modelentry = modelentry->next)
  {
    if (!modelentry->string || !modelentry->string[0])
      continue;

    smtk::common::UUID modelid = smtk::common::UUID(modelentry->string);
    if (modelid.isNull())
    {
      smtkInfoMacro(modelMgr->log(), "Invalid model uuid, skipping!");
      continue;
    }
    // model meta info
    status &= smtk::io::LoadJSON::ofManager(modelentry, modelMgr);
  }

  return status;
}

/**\brief Parse meshes info from \a sessionRec for the given \a modelMgr.
  *
  * Subclasses should return 1 on success and 0 on failure.
  */
int SessionIOJSON::loadMeshesRecord(ManagerPtr modelMgr, cJSON* sessionRec)
{
  cJSON* meshesObj = cJSON_GetObjectItem(sessionRec, "mesh_collections");
  if (!meshesObj)
  {
    // It's okay if we don't have "mesh_collections" entry. Could be a record that
    // did not come from SessionIOJSON;
    // smtkInfoMacro(modelMgr->log(), "Expecting a \"mesh_collections\" entry!");
    return 1;
  }
  return smtk::io::LoadJSON::ofMeshesOfModel(sessionRec, modelMgr, this->referencePath());
}

/**\brief Encode information into \a sessionRec for the given \a modelMgr.
  *
  * Subclasses should return 1 on success and 0 on failure.
  */
int SessionIOJSON::exportJSON(
  ManagerPtr modelMgr, const SessionPtr& session, cJSON* sessionRec, bool writeNativeModels)
{
  (void)modelMgr;
  (void)session;
  (void)sessionRec;
  (void)writeNativeModels;
  return 1;
}

/**\brief Encode information into \a sessionRec for the given \a modelId of the \a modelMgr.
  *
  * Subclasses should return 1 on success and 0 on failure.
  */
int SessionIOJSON::exportJSON(ManagerPtr modelMgr, const SessionPtr& session,
  const smtk::common::UUIDs& modelIds, cJSON* sessionRec, bool writeNativeModels)
{
  (void)sessionRec;

  if (writeNativeModels)
  {
    // We will write each model seperately:
    smtk::common::UUIDs::const_iterator modit;
    for (modit = modelIds.begin(); modit != modelIds.end(); ++modit)
    {
      smtk::model::Model model(modelMgr, *modit);
      std::string outNativeFile = this->getOutputFileNameForNativeModel(modelMgr, session, model);
      if (this->writeNativeModel(modelMgr, session, model, outNativeFile))
      {
        if (!this->referencePath().empty())
        {
          model.setStringProperty("url", relative(outNativeFile, this->referencePath()).string());
        }
        else
        {
          model.setStringProperty("url", outNativeFile);
        }
      }
    }
  }

  return 1;
}

/**\brief Create and write to a file (\a outNativeFile) for the given \a model.
  *
  * Return 1 on success and 0 on failure.
  */
int SessionIOJSON::writeNativeModel(smtk::model::ManagerPtr modelMgr,
  const smtk::model::SessionPtr& sess, const smtk::model::Model& model, std::string& outNativeFile)
{
  // if this is not a valid session, return;
  if (!sess)
  {
    smtkInfoMacro(modelMgr->log(), "Expecting a valid session!");
    return 0;
  }

  smtk::model::OperatorPtr writeOp = sess->op("write");
  if (!writeOp)
  {
    smtkInfoMacro(
      modelMgr->log(), "Failed to create a write operator to write the model for native kernel!");
    return 0;
  }
  writeOp->specification()->findFile("filename")->setValue(outNativeFile);
  writeOp->specification()->associateEntity(model);

  smtk::model::OperatorResult opresult = writeOp->operate();
  if (opresult->findInt("outcome")->value() != smtk::model::OPERATION_SUCCEEDED)
  {
    smtkInfoMacro(modelMgr->log(), "Failed to write the model for native kernel!");
    return 0;
  }

  // the output filename could have been changed by write operator.
  outNativeFile = writeOp->specification()->findFile("filename")->value();
  return 1;
}

/**\brief Get or construct a filename for saving the native model.
  *
  * Return a full file path.
  */
std::string SessionIOJSON::getOutputFileNameForNativeModel(smtk::model::ManagerPtr modelMgr,
  const smtk::model::SessionPtr& sess, const smtk::model::Model& model) const
{
  std::string smtkfilepath, smtkfilename, origfilename, origfileext;
  if (modelMgr->hasStringProperty(model.entity(), "smtk_url"))
  {
    smtk::model::StringList const& nprop(modelMgr->stringProperty(model.entity(), "smtk_url"));
    if (!nprop.empty())
    {
      smtkfilepath = path(nprop[0]).parent_path().string();
      smtkfilename = path(nprop[0]).stem().string();
    }
  }

  if (modelMgr->hasStringProperty(model.entity(), "url"))
  {
    smtk::model::StringList const& nprop(modelMgr->stringProperty(model.entity(), "url"));
    if (!nprop.empty())
    {
      origfilename = path(nprop[0]).stem().string();
      origfileext = path(nprop[0]).extension().string();
    }
  }
  if (smtkfilename.empty() && !origfilename.empty())
    smtkfilename = origfilename;

  if (smtkfilename.empty())
    smtkfilename = sess->name();

  std::ostringstream outfilename;
  outfilename << smtkfilename << "_out" << origfileext;
  return (path(smtkfilepath) / path(outfilename.str())).string();
}

/**\brief Load the native model given the filename (\a inNativeFile).
  *
  * Return 1 on success and 0 on failure.
  */
int SessionIOJSON::loadNativeModel(smtk::model::ManagerPtr modelMgr,
  const smtk::model::SessionPtr& sess, const std::string& inNativeFile, std::string& loadedURL)
{
  // if this is not a valid session, return;
  if (!sess)
  {
    smtkInfoMacro(modelMgr->log(), "Expecting a valid session!");
    return 0;
  }

  if (!inNativeFile.empty())
  {
    smtk::model::OperatorPtr readOp = sess->op("read");
    if (!readOp)
    {
      smtkInfoMacro(
        modelMgr->log(), "Failed to create a read operator to read the model for native kernel!");
      return 0;
    }

    path actualFilename(inNativeFile);
    if (!this->referencePath().empty() && !actualFilename.is_absolute())
    {
      path tryme = this->referencePath() / actualFilename;
      if (exists(tryme))
      {
        actualFilename = canonical(tryme, this->referencePath());
      }
    }
    readOp->specification()->findFile("filename")->setValue(actualFilename.string());
    smtk::model::OperatorResult opresult = readOp->operate();
    if (opresult->findInt("outcome")->value() != smtk::model::OPERATION_SUCCEEDED)
    {
      smtkInfoMacro(modelMgr->log(), "Failed to read the model for native kernel!");
      return 0;
    }
    loadedURL = actualFilename.string();
    return 1;
  }

  return 0;
}

} // namespace model
} // namespace smtk
