/*
 * RSearchPath.cpp
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

// NOTE: known "holes" in search path persistence include:
//
//   - package internal state
//   - contents of UserDefinedDatabase objects installed by packages
//

#include "RSearchPath.hpp"

#include <string>
#include <vector>
#include <algorithm>
#include <gsl/gsl-lite.hpp>

#include <boost/function.hpp>
#include <boost/format.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/bind/bind.hpp>

#include <core/Log.hpp>
#include <shared_core/Error.hpp>
#include <shared_core/FilePath.hpp>
#include <shared_core/SafeConvert.hpp>
#include <core/FileSerializer.hpp>

#define R_INTERNAL_FUNCTIONS
#include <r/RInternal.hpp>
#include <r/RExec.hpp>
#include <r/RInterface.hpp>

#include <r/session/RSessionUtils.hpp>

using namespace rstudio::core;
using namespace boost::placeholders;

namespace rstudio {
namespace r {
   
using namespace exec;
   
namespace session {
namespace search_path {

namespace {   

const char * const kEnvironmentFile = "environment";
const char * const kSearchPathDir = "search_path";
   
const char * const kSearchPathElementsDir = "search_path_elements";
const char * const kPackagePaths = "package_paths";
const char * const kEnvDataDir = "environment_data";

Error saveGlobalEnvironmentToFile(const FilePath& environmentFile)
{
   std::string envPath =
            string_utils::utf8ToSystem(environmentFile.getAbsolutePath());
   return executeSafely(boost::bind(R_SaveGlobalEnvToFile, envPath.c_str()));
}
   
Error restoreGlobalEnvironment(const core::FilePath& environmentFile)
{
   // tolerate no environment saved
   if (!environmentFile.exists())
      return Success();

   return RFunction("base:::load", environmentFile.getAbsolutePath()).call();
}

bool hasEnvironmentData(const std::string& elementName)
{
   if (boost::algorithm::starts_with(elementName, "package:"))
      return false;
   else if (elementName == "Autoloads")
      return false;
   else if (elementName == "tools:rstudio")
      return false;
   else if (elementName == ".GlobalEnv") // saved and restored separately
      return false;
   else if (elementName == "org:r-lib")
      return false;
   else 
      return true;
}

} // anonymous namespace
   

Error save(const FilePath& statePath)
{
   // save the global environment
   FilePath environmentFile = statePath.completePath(kEnvironmentFile);
   Error error = saveGlobalEnvironmentToFile(environmentFile);
   if (error)
      return error;
   
   // reset the contents of the search path dir
   FilePath searchPathDir = statePath.completePath(kSearchPathDir);
   error = searchPathDir.resetDirectory();
   if (error)
      return error;
   
   // create environment data subdirectory
   FilePath environmentDataPath = searchPathDir.completePath(kEnvDataDir);
   error = environmentDataPath.ensureDirectory();
   if (error)
      return error;
   
   // iterate through the search path (build a list as we go). set 
   // .GlobalEnv and package:base as bookends of the list (note this code
   // is based on the implementation of do_search)
   std::vector<std::string> searchPathElements;
   searchPathElements.push_back(".GlobalEnv");
   
   for (SEXP envSEXP = r::sexp::getParentEnv(R_GlobalEnv);
        envSEXP != R_BaseEnv;
        envSEXP = r::sexp::getParentEnv(envSEXP))
   {
      // screen out UserDefinedDatabase elements (attempting to persist
      // a UserDefinedDatabase caused mischief in at least one case (e.g. see
      // RProtoBuf:DescriptorPool) so we exclude it globally.
      if (r::sexp::inherits(envSEXP, "UserDefinedDatabase"))
         continue;

      // get the name of the search path element and add it to our list
      SEXP nameSEXP = Rf_getAttrib(envSEXP, Rf_install("name"));
      std::string elementName;
      if (!Rf_isString(nameSEXP) || Rf_length(nameSEXP) < 1)
         elementName = "(unknown)";
      else
         elementName = r::sexp::asString(nameSEXP);

      // screen out '.conflicts' environment, as generated by the conflicted package
      // (since it will be responsible for re-generating that environment on load)
      if (elementName == ".conflicts")
         continue;

      searchPathElements.push_back(elementName);

      // save the environment's data if necessary
      if (hasEnvironmentData(elementName))
      {
         // determine file path (index of item within list)
         auto index = searchPathElements.size() - 1;
         std::string itemIndex = safe_convert::numberToString(index);
         FilePath dataFilePath = environmentDataPath.completePath(itemIndex);
         
         // save the environment
         Error error = r::exec::RFunction(".rs.saveEnvironment",
                                          envSEXP,
                                          dataFilePath.getAbsolutePath()).call();
         if (error)
            return error;
      }
   }
   searchPathElements.push_back("package:base");
   
   // save the search path list
   FilePath elementsPath = searchPathDir.completePath(kSearchPathElementsDir);
   error =  writeStringVectorToFile(elementsPath, searchPathElements);
   if (error)
      return error;

   // save the package paths list
   std::vector<std::string> packages;
   error = r::exec::RFunction("base:::loadedNamespaces").call(&packages);
   if (error)
      return error;
   
   std::map<std::string, std::string> packagePaths;
   for (auto&& package : packages)
   {
      if (package != "base")
      {
         std::string packagePath;
         error = r::exec::RFunction("base:::getNamespaceInfo")
               .addUtf8Param(package)
               .addUtf8Param("path")
               .call(&packagePath);

         if (error)
            LOG_ERROR(error);
         packagePaths[package] = packagePath;
      }
   }
   
   FilePath packagePathsFile = searchPathDir.completePath(kPackagePaths);
   return writeStringMapToFile(packagePathsFile, packagePaths);
}


Error saveGlobalEnvironment(const FilePath& statePath)
{
   FilePath environmentFile = statePath.completePath(kEnvironmentFile);
   return saveGlobalEnvironmentToFile(environmentFile);
}

bool isBasePackage(const std::string& name)
{
   static const auto basePackages = {
      "package:methods",
      "package:grDevices",
      "package:graphics",
      "package:stats",
      "package:utils",
   };
   
   auto index = std::find(basePackages.begin(), basePackages.end(), name);
   return index != basePackages.end();
}

void repairSearchPath()
{
   // find the 'tools:rstudio' environment on the search path,
   // save a reference to it, and remove it from the search list
   //
   // NOTE: we cannot use 'detach()' and 'attach()' to handle this, as attach
   // will actually create and attach a _copy_ of the environment, which causes
   // trouble if we need to add or modify the 'tools:rstudio' environment in
   // the future
   SEXP toolsSEXP = R_NilValue;
   
   SEXP thisSEXP = R_GlobalEnv;
   while (thisSEXP != R_BaseEnv)
   {
      SEXP prevSEXP = thisSEXP;
      thisSEXP = r::sexp::getParentEnv(thisSEXP);

      SEXP nameSEXP = r::sexp::getAttrib(thisSEXP, "name");
      if (TYPEOF(nameSEXP) != STRSXP)
         continue;

      std::string name = CHAR(STRING_ELT(nameSEXP, 0));
      if (name != "tools:rstudio")
         continue;

      toolsSEXP = thisSEXP;
      r::sexp::sxpinfo::setEnclos(prevSEXP, r::sexp::getParentEnv(thisSEXP));
   }
   
   thisSEXP = R_GlobalEnv;
   while (thisSEXP != R_BaseEnv)
   {
      SEXP prevSEXP = thisSEXP;
      thisSEXP = r::sexp::getParentEnv(thisSEXP);

      SEXP nameSEXP = r::sexp::getAttrib(thisSEXP, "name");
      if (TYPEOF(nameSEXP) != STRSXP)
         continue;
      
      std::string name = CHAR(STRING_ELT(nameSEXP, 0));
      if (isBasePackage(name))
      {
         r::sexp::sxpinfo::setEnclos(prevSEXP, toolsSEXP);
         r::sexp::sxpinfo::setEnclos(toolsSEXP, thisSEXP);
         return;
      }
   }
}

Error restoreSearchPath(
      const FilePath& statePath,
      const std::vector<std::string>& currentSearchPathList)
{
   // attempt to restore the search path if one has been saved
   FilePath searchPathDir = statePath.completePath(kSearchPathDir);
   if (!searchPathDir.exists())
      return Success();
   
   FilePath searchPathsFile = searchPathDir.completePath(kSearchPathElementsDir);
   FilePath packagePathsFile = searchPathDir.completePath(kPackagePaths);
   FilePath environmentDataPath = searchPathDir.completePath(kEnvDataDir);

   // load the required packages
   Error error = r::exec::RFunction(".rs.restoreSearchPath")
         .addUtf8Param(searchPathsFile.getAbsolutePath())
         .addUtf8Param(packagePathsFile.getAbsolutePath())
         .addUtf8Param(environmentDataPath.getAbsolutePath())
         .call();
   if (error)
      LOG_ERROR(error);
         
   // ensure 'tools:rstudio' is placed appropriately in the search list
   // we want to make sure all of the 'base' R packages are visible to
   // 'tools:rstudio', but we also need to make sure packages attached
   // by the user are _not_ visible -- so we need to make sure that
   // the tools environment is attached after any user-defined packages
   // (or other attached environments), but before any base R packages
   repairSearchPath();
   
   return Success();
}

Error restore(
      const FilePath& statePath,
      const std::vector<std::string>& currentSearchPathList,
      bool isCompatibleSessionState)
{
   // restore global environment unless suppressed
   if (utils::restoreEnvironmentOnResume())
   {
      FilePath environmentFile = statePath.completePath(kEnvironmentFile);
      Error error = restoreGlobalEnvironment(environmentFile);
      if (error)
         return error;
   }

   // only restore the search path if we have a compatible R version
   // (guard against attempts to attach incompatible packages to this
   // R session)
   if (isCompatibleSessionState)
   {
      Error error = restoreSearchPath(statePath, currentSearchPathList);
      if (error)
         return error;
   }
   
   return Success();
}
   
} // namespace search_path
} // namespace session
} // namespace r
} // namespace rstudio



