/*
 * RInit.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.
 *
 */

#include <boost/bind/bind.hpp>

#include <core/system/Environment.hpp>

#include <r/RExec.hpp>
#include <r/RRoutines.hpp>
#include <r/RSourceManager.hpp>
#include <r/RVersion.hpp>
#include <r/session/RClientState.hpp>
#include <r/session/RConsoleHistory.hpp>
#include <r/session/RSession.hpp>
#include <r/session/RSessionState.hpp>
#include <r/session/RSuspend.hpp>

#include "REmbedded.hpp"
#include "RInit.hpp"
#include "RStdCallbacks.hpp"
#include "RRestartContext.hpp"

#include "graphics/RGraphicsDevice.hpp"

// constants for graphics scratch subdirectory
#define kGraphicsPath "graphics"

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

namespace rstudio {
namespace r {
namespace session {

namespace {

// is this R 3.0 or greater
bool s_isR3 = false;

// is this R 3.3 or greater
bool s_isR3_3 = false;

// is this R 4.0 or greater
bool s_isR4 = false;

boost::function<void()> s_beforeResumeCallback, s_afterResumeCallback;

// function for deferred deserialization actions. this encapsulates parts of 
// the initialization process that are potentially highly latent. this allows
// clients to bring their UI up and then receive an event indicating that the
// latent deserialization actions are taking place
std::atomic<bool> s_isSessionDeserialized(false);
boost::function<void()> s_deferredDeserializationAction;
   
void reportDeferredDeserializationError(const Error& error)
{
   // log error
   LOG_ERROR(error);

   // report to user
   std::string errMsg = r::endUserErrorMessage(error);
   REprintf("%s\n", errMsg.c_str());
}

std::string createAliasedPath(const FilePath& filePath)
{
   return FilePath::createAliasedPath(filePath, utils::userHomePath());
}
   
Error restoreGlobalEnvFromFile(const std::string& path, std::string* pErrMessage)
{
   return r::exec::RFunction(".rs.restoreGlobalEnvFromFile")
         .addParam(path)
         .call(pErrMessage);
}

void completeDeferredSessionInit(bool newSession)
{
   // suppress output which occurs during restore (packages can sometimes
   // print messages to the console indicating they have conflicts -- the
   // user has already seen these messages and doesn't expect them now so
   // we suppress them
   utils::SuppressOutputInScope suppressOutput;
   
   // always cleanup any restart context here
   restartContext().removeSessionState();

   // call external hook
   if (rCallbacks().deferredInit)
      rCallbacks().deferredInit(newSession);
}


void deferredRestoreSuspendedSession(
                     const boost::function<Error()>& deferredRestoreAction)
{
   // notify client of serialization status
   SerializationCallbackScope cb(kSerializationActionResumeSession);
   
   // suppress interrupts which occur during restore
   r::exec::IgnoreInterruptsScope ignoreInterrupts;
 
   // restore action
   Error error = deferredRestoreAction();
   if (error)
      reportDeferredDeserializationError(error);

   // complete deferred init
   completeDeferredSessionInit(false);
}

void deferredRestoreNewSession()
{
   // restore the default global environment if there is one
   FilePath globalEnvPath = utils::startupEnvironmentFilePath();
   if (utils::restoreWorkspace() && globalEnvPath.exists())
   {
      // notify client of serialization status
      SerializationCallbackScope cb(kSerializationActionLoadDefaultWorkspace,
                                    globalEnvPath);

      // ignore interrupts which occur during restoring of the global env
      // the restoration will run to completion in any case and then the
      // next thing the user does will be "interrupted" -- clearly not
      // what they intended
      r::exec::IgnoreInterruptsScope ignoreInterrupts;

      std::string path = globalEnvPath.getAbsolutePath();
      std::string aliasedPath = createAliasedPath(globalEnvPath);
      
      std::string errMessage;
      Error error = restoreGlobalEnvFromFile(path, &errMessage);
      if (error)
      {
         std::cerr << "WARNING: Failed to restore workspace from "
                   << "'" << aliasedPath << "'"
                   << " (an internal error occurred)"
                   << std::endl;
         LOG_ERROR(error);
      }
      else if (!errMessage.empty())
      {
         std::stringstream ss;
         ss << "WARNING: Failed to restore workspace from "
            << "'" << aliasedPath << "'" << std::endl
            << "Reason: " << errMessage << std::endl;
         std::string message = ss.str();

         std::cerr << message << std::endl;
         LOG_ERROR_MESSAGE(message);
      }
      else
      {
         std::cout << "[Workspace loaded from " << aliasedPath << "]"
                   << std::endl
                   << std::endl;
      }
   }

   // mark image clean (we need to do this due to our delayed handling
   // of workspace restoration)
   setImageDirty(false);

   // complete deferred init
   completeDeferredSessionInit(true);
}

} // anonymous namespace

void restoreSession(const FilePath& suspendedSessionPath,
                    std::string* pErrorMessages)
{
   if (s_beforeResumeCallback)
     s_beforeResumeCallback();

   // don't show output during deserialization (packages loaded
   // during deserialization sometimes print messages)
   utils::SuppressOutputInScope suppressOutput;
   
   // deserialize session. if any part of this fails then the errors
   // will be logged and error messages will be returned in the passed
   // errorMessages buffer (this mechanism is used because we generally
   // suppress output during restore but we need a way for the error
   // messages to make their way back to the user)
   boost::function<Error()> deferredRestoreAction;
   r::session::state::restore(suspendedSessionPath,
                              utils::isServerMode(),
                              &deferredRestoreAction,
                              pErrorMessages);

   if (deferredRestoreAction)
   {
      s_deferredDeserializationAction = boost::bind(
                                          deferredRestoreSuspendedSession,
                                          deferredRestoreAction);
   }

   if (s_afterResumeCallback)
     s_afterResumeCallback();
}

// one-time per session initialization
Error initialize()
{
   // ensure that the utils package is loaded (it might not be loaded
   // if R is attempting to recover from a library loading error which
   // occurs during .Rprofile)
   Error libError = r::exec::RFunction("library", "utils").call();
   if (libError)
      LOG_ERROR(libError);

   // check whether this is R 3.3 or greater
   Version rVersion = r::version();
   s_isR3   = rVersion >= Version("3.0.0");
   s_isR3_3 = rVersion >= Version("3.3.0");
   s_isR4   = rVersion >= Version("4.0.0");

   // initialize console history capacity
   r::session::consoleHistory().setCapacityFromRHistsize();

   // install R tools
   FilePath toolsFilePath = utils::rSourcePath().completePath("Tools.R");
   Error error = r::sourceManager().sourceTools(toolsFilePath);
   if (error)
      return error;

   // install R hooks (depends only on helpers in Tools.R)
   FilePath hooksFilePath = utils::rSourcePath().completePath("Hooks.R");
   error = r::sourceManager().sourceTools(hooksFilePath);
   if (error)
      return error;

   // install RStudio API
   FilePath apiFilePath = utils::rSourcePath().completePath("Api.R");
   error = r::sourceManager().sourceTools(apiFilePath);
   if (error)
      return error;
   
   // install app commands
   FilePath appCommandsFilePath = utils::rSourcePath().completePath("AppCommands.R");
   error = r::sourceManager().sourceTools(appCommandsFilePath);
   if (error)
      return error;

   // install global calling handlers
   FilePath globalCallingHandlersFilePath = utils::rSourcePath().completePath("GlobalCallingHandlers.R");
   error = r::sourceManager().sourceTools(globalCallingHandlersFilePath);
   if (error)
      return error;


   // initialize graphics device -- use a stable directory for server mode
   // and temp directory for desktop mode (so that we can support multiple
   // concurrent processes using the same project)
   FilePath graphicsPath;
   if (utils::isServerMode())
   {
      std::string path = kGraphicsPath;
      if (utils::isR3())
         path += "-r3";
      graphicsPath = utils::sessionScratchPath().completePath(path);
   }
   else
   {
      graphicsPath = r::session::utils::tempDir().completePath(
         "rs-graphics-" + core::system::generateUuid());
   }

   error = graphics::device::initialize(graphicsPath,
                                        rCallbacks().locator);
   if (error) 
      return error;
   
   // restore client state
   session::clientState().restore(utils::clientStatePath(),
                                  utils::projectClientStatePath());
      
   // restore suspended session if we have one
   bool wasResumed = false;
   
   // first check for a pending restart
   if (restartContext().hasSessionState())
   {
      // restore session
      std::string errorMessages;
      restoreSession(restartContext().sessionStatePath(), &errorMessages);

      // show any error messages
      if (!errorMessages.empty())
         REprintf("%s\n", errorMessages.c_str());

      // note we were resumed
      wasResumed = true;
   }
   else if (suspendedSessionPath().exists())
   {  
      // restore session
      std::string errorMessages;
      restoreSession(suspendedSessionPath(), &errorMessages);
      
      // show any error messages
      if (!errorMessages.empty())
         REprintf("%s\n", errorMessages.c_str());

      // note we were resumed
      wasResumed = true;
   }  
   // new session
   else
   {  
      // restore console history
      FilePath historyPath = rHistoryFilePath();
      error = consoleHistory().loadFromFile(historyPath, false);
      if (error)
         reportHistoryAccessError("read history from", historyPath, error);

      // defer loading of global environment
      s_deferredDeserializationAction = deferredRestoreNewSession;
   }
   
   // initialize client
   RInitInfo rInitInfo(wasResumed);
   error = rCallbacks().init(rInitInfo);
   if (error)
      return error;

   // call resume hook if we were resumed
   if (wasResumed)
      rCallbacks().resumed();
   
   // now that all initialization code has had a chance to run we
   // can register all external routines which were added to r::routines
   // during the init sequence
   r::routines::registerAll();

   // set default repository if requested
   if (!utils::rCRANUrl().empty() || !utils::rCRANSecondary().empty())
   {
      error = r::exec::RFunction(".rs.setCRANReposAtStartup",
                                 utils::rCRANUrl(),
                                 utils::rCRANSecondary()).call();
      if (error)
         return error;
   }

   // initialize profile resources
   error = r::exec::RFunction(".rs.profileResources").call();
   if (error)
      return error;

   // complete embedded r initialization
   error = r::session::completeEmbeddedRInitialization();
   if (error)
      return error;

   // set global R options
   FilePath optionsFilePath = utils::rSourcePath().completePath("Options.R");
   error = r::sourceManager().sourceLocal(optionsFilePath);
   if (error)
      return error;
   
   // set server options
#ifdef __linux__
   if (utils::isServerMode())
   {
      FilePath serverOptionsFilePath =
            utils::rSourcePath().completePath("ServerOptions.R");
      
      Error error = r::sourceManager().sourceLocal(serverOptionsFilePath);
      if (error)
         return error;
   }
#endif

   // now run hooks for those waiting for session to be fully initialized
   if (rCallbacks().initComplete)
      rCallbacks().initComplete();
   
   // run tests if configured to do so
   // (note that the callback will exit the process after tests have been run)
   if (rCallbacks().runTests)
      rCallbacks().runTests();
   
   return Success();
}

void ensureDeserialized()
{
   if (s_deferredDeserializationAction)
   {
      // do the deferred action
      s_deferredDeserializationAction();
      s_deferredDeserializationAction.clear();
   }

   // mark session as deserialized
   s_isSessionDeserialized = true;

}

bool isSessionRestored()
{
   return s_isSessionDeserialized;
}
   
FilePath rHistoryFilePath()
{
   std::string histFile = core::system::getenv("R_HISTFILE");
   boost::algorithm::trim(histFile);
   if (histFile.empty())
      histFile = ".Rhistory";

   return utils::rHistoryDir().completePath(histFile);
}

void reportHistoryAccessError(const std::string& context,
                              const FilePath& historyFilePath,
                              const Error& error)
{
   // always log
   LOG_ERROR(error);

   // default summary
   std::string summary = error.getSummary();

   // if the file exists and we still got no such file or directory
   // then it is almost always permission denied. this seems to happen
   // somewhat frequently on linux systems where the user was root for
   // an operation and ended up writing a .Rhistory
   if (historyFilePath.exists() &&
       (error == systemError(boost::system::errc::no_such_file_or_directory, ErrorLocation())))
   {
      summary = "permission denied (is the .Rhistory file owned by root?)";
   }

   // notify the user
   std::string path = createAliasedPath(historyFilePath);
   std::string errmsg = context + " " + path + ": " + summary;
   REprintf("Error attempting to %s\n", errmsg.c_str());
}

void setResumeCallbacks(boost::function<void()> before, boost::function<void()> after)
{
   s_beforeResumeCallback = before;
   s_afterResumeCallback = after;
}
   
namespace utils {

bool isR3()
{
   return s_isR3;
}

bool isR3_3()
{
   return s_isR3_3;
}

bool isR4()
{
   return s_isR4;
}

}

} // namespace session
} // namespace r
} // namespace rstudio

