/*
 * SessionRnwWeave.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 "SessionRnwWeave.hpp"

#include <boost/utility.hpp>
#include <boost/format.hpp>
#include <boost/algorithm/string/split.hpp>

#include <fmt/format.h>

#include <core/FileSerializer.hpp>
#include <core/StringUtils.hpp>
#include <core/tex/TexLogParser.hpp>
#include <core/tex/TexMagicComment.hpp>

#include <r/RExec.hpp>
#include <r/RRoutines.hpp>
#include <r/RJson.hpp>
#include <r/session/RSessionUtils.hpp>

#include <session/projects/SessionProjects.hpp>
#include <session/SessionModuleContext.hpp>
#include <session/prefs/UserPrefs.hpp>

#include "../rmarkdown/SessionRMarkdown.hpp"
#include "SessionRnwConcordance.hpp"
#include "SessionCompilePdfSupervisor.hpp"

using namespace rstudio::core;
using namespace rstudio::session::modules::tex::rnw_concordance;
using namespace boost::placeholders;

namespace rstudio {
namespace session {
namespace modules { 
namespace tex {
namespace rnw_weave {

namespace {

Error rBinDir(FilePath* pRBinDir)
{
   std::string rHomeBin;
   r::exec::RFunction rHomeBinFunc("R.home", "bin");
   Error error = rHomeBinFunc.call(&rHomeBin);
   if (error)
      return error;

   *pRBinDir = FilePath(rHomeBin);
   return Success();
}


class RnwWeave : boost::noncopyable
{
public:
   explicit RnwWeave(const std::string& name,
                     const std::string& packageName = "")
   {
      name_ = name;
      packageName_ = !packageName.empty() ? packageName : name;
   }

   virtual ~RnwWeave()
   {
   }

   // COPYING: noncopyable (to prevent slicing)

   const std::string& name() const { return name_; }
   const std::string& packageName() const { return packageName_; }

   virtual bool injectConcordance() const = 0;

   virtual bool usesCodeForOptions() const = 0;

   virtual bool forceEchoOnExec() const = 0;

   virtual bool isInstalled() const = 0;

   virtual core::json::Value chunkOptions() const = 0;

   // tangle the passed file (note that the implementation can assume
   // that the working directory is already set to that of the file)
   virtual core::Error tangle(const std::string& file,
                              const std::string& encoding) = 0;

   virtual std::vector<std::string> commandArgs(
                                       const std::string& file,
                                       const std::string& encoding,
                                       const std::string& driver) const
   {
      std::vector<std::string> args;
      args.push_back("--no-save");
      args.push_back("--no-restore");
      args.push_back("-s");
      args.push_back("-e");
      std::string cmd = "grDevices::pdf.options(useDingbats = FALSE); "
                        + weaveCommand(file, encoding, driver);
      args.push_back(cmd);
      return args;
   }

   virtual std::string weaveCommand(const std::string& file,
                                    const std::string& encoding,
                                    const std::string& driver) const = 0;

   virtual core::Error parseOutputForErrors(
                                    const std::string& output,
                                    const core::FilePath& rnwFilePath,
                                    core::tex::LogEntries* pLogEntries) const
   {
      // split into lines so we can determine the line numbers for the chunks
      // NOTE: will need to read this using global/project encoding if we
      // want to look for text outside of theh original error parsing
      // scenario (which only required ascii)
      std::string rnwContents;
      Error error = core::readStringFromFile(rnwFilePath, &rnwContents);
      if (error)
         return error;
      std::vector<std::string> lines;
      boost::algorithm::split(lines, rnwContents, boost::is_any_of("\n"));

      // determine line numbers
      boost::regex re("^<<(.*)>>=.*");
      boost::smatch match;
      std::vector<int> chunkLineNumbers;
      for (std::size_t i=0; i<lines.size(); i++)
      {
         if (regex_utils::match(lines[i], match, re))
            chunkLineNumbers.push_back(gsl::narrow_cast<int>(i) + 1);
      }

      // determine chunk number and error message
      boost::regex cre(
         "[\\w]+:[\\s]+chunk[\\s]+(\\d+)[^\n]+\n[^:]+:[\\s]*[\n]?([^\n]+)\n");
      if (regex_utils::search(output, match, cre))
      {
         std::string match1(match[1]);
         std::string match2(match[2]);
         std::size_t chunk = core::safe_convert::stringTo<int>(match1, 0);
         std::string msg = boost::algorithm::trim_copy(match2);
         if (chunk > 0 && chunk <= chunkLineNumbers.size())
         {
            boost::format fmt("(chunk %1%) %2%");
            core::tex::LogEntry logEntry(FilePath(),
                                         -1,
                                         core::tex::LogEntry::Error,
                                         rnwFilePath,
                                         chunkLineNumbers[chunk-1],
                                         boost::str(fmt % chunk % msg));
            pLogEntries->push_back(logEntry);
         }
      }

      return Success();
   }

protected:
   core::json::Value chunkOptions(const std::string& chunkFunction) const
   {
      SEXP optionsSEXP;
      r::sexp::Protect rProtect;
      r::exec::RFunction optionsFunc(chunkFunction);
      Error error = optionsFunc.call(&optionsSEXP, &rProtect);
      if (error)
      {
         LOG_ERROR(error);
         return json::Value();
      }

      core::json::Value optionsJson;
      error = r::json::jsonValueFromList(optionsSEXP, &optionsJson);
      if (error)
         LOG_ERROR(error);

      return optionsJson;
   }

private:
   std::string name_;
   std::string packageName_;
};

class RnwSweave : public RnwWeave
{
public:
   RnwSweave()
      : RnwWeave("Sweave")
   {
   }

   virtual bool isInstalled() const { return true; }

   virtual bool injectConcordance() const { return true; }

   virtual bool usesCodeForOptions() const { return false; }

   virtual bool forceEchoOnExec() const { return false; }

   virtual core::json::Value chunkOptions() const
   {
      return RnwWeave::chunkOptions(".rs.sweaveChunkOptions");
   }

   virtual core::Error tangle(const std::string& file,
                              const std::string& encoding)
   {
      r::exec::RFunction stangle("utils:::Stangle");
      stangle.addParam(file);
      if (!encoding.empty())
         stangle.addParam("encoding", encoding);
      return stangle.call();
   }

   virtual std::string weaveCommand(const std::string& file,
                                    const std::string& encoding,
                                    const std::string& driver) const
   {
      // escape the path and encoding before interpolating them into the R
      // command, so that a filename containing a single quote cannot break out
      // of the string literal and inject arbitrary R code
      std::string cmd =
            fmt::format("utils::Sweave('{}'",
                        string_utils::singleQuotedStrEscape(file));

      if (!driver.empty())
         cmd += fmt::format(", driver = {}", driver);

      if (!encoding.empty())
         cmd += fmt::format(", encoding = '{}'",
                            string_utils::singleQuotedStrEscape(encoding));

      cmd += ")";

      return cmd;
   }
};


class RnwKnitr : public RnwWeave
{
public:
   RnwKnitr()
      : RnwWeave("knitr", "knitr")
   {
   }

   virtual bool injectConcordance() const { return false; }

   virtual bool usesCodeForOptions() const { return true; }

   virtual bool forceEchoOnExec() const { return true; }

   virtual bool isInstalled() const
   {
      return module_context::isPackageInstalled(packageName());
   }

   virtual std::string weaveCommand(const std::string& file,
                                    const std::string& encoding,
                                    const std::string& driver) const
   {
      std::string cmd = "require(knitr); ";
      if (prefs::userPrefs().alwaysEnableRnwConcordance())
         cmd += "opts_knit$set(concordance = TRUE); ";

      // escape the path and encoding before interpolating them into the R
      // command, so that a filename containing a single quote cannot break out
      // of the string literal and inject arbitrary R code
      cmd += fmt::format("knit('{}'", string_utils::singleQuotedStrEscape(file));

      if (!encoding.empty())
         cmd += fmt::format(", encoding = '{}'",
                            string_utils::singleQuotedStrEscape(encoding));

      cmd += ")";

      return cmd;
   }

   virtual core::Error parseOutputForErrors(
                                    const std::string& output,
                                    const core::FilePath& rnwFilePath,
                                    core::tex::LogEntries* pLogEntries) const
   {
      // older error style
      boost::regex errRe("^\\s*Quitting from lines ([0-9]+)-([0-9]+): "
                          "(?:Error in [a-z]+\\([a-z=, ]+\\) : \n?)?"
                          "([^\n]+)$");

      // new error style
      boost::regex newErrRe("^\\s*" kKnitrErrorRegex "\\s*\\n(.*?)\\n");
      boost::smatch match;
      if (regex_utils::search(output, match, errRe))
      {
         // extract error info
         int lineBegin = safe_convert::stringTo<int>(match[1], -1);
         std::string message = match[3];

         // check to see if there is a parse error which provides more
         // precise pinpointing of the line
         boost::regex parseRe("^\\s*<text>:([0-9]+):[0-9]+: (.+)$");
         if (regex_utils::match(message, match, parseRe))
         {
            lineBegin += safe_convert::stringTo<int>(match[1], -1);
            message = match[2];
         }

         core::tex::LogEntry logEntry(FilePath(),
                                      -1,
                                      core::tex::LogEntry::Error,
                                      rnwFilePath,
                                      lineBegin,
                                      message);
         pLogEntries->push_back(logEntry);
      }

      else if (regex_utils::search(output, match, newErrRe))
      {
         // extract error info
         int lineBegin = safe_convert::stringTo<int>(match[1], -1);
         std::string message = match[4];

         core::tex::LogEntry logEntry(FilePath(),
                                      -1,
                                      core::tex::LogEntry::Error,
                                      rnwFilePath,
                                      lineBegin,
                                      message);
         pLogEntries->push_back(logEntry);
      }

      return Success();
   }

   virtual core::json::Value chunkOptions() const
   {
      if (isInstalled())
         return RnwWeave::chunkOptions(".rs.knitrChunkOptions");
      else
         return json::Value();
   }

   virtual core::Error tangle(const std::string& file,
                              const std::string& encoding)
   {
      r::session::utils::SuppressOutputInScope suppressOutput;
      r::exec::RFunction purlFunc("knitr:::purl");
      purlFunc.addParam("input", file);
      purlFunc.addParam("output", file + ".R");
      if (!encoding.empty())
         purlFunc.addParam("encoding", encoding);
      return purlFunc.call();
   }
};


class RnwWeaveRegistry : boost::noncopyable
{
private:
   RnwWeaveRegistry()
   {
      weaveTypes_.push_back(boost::shared_ptr<RnwWeave>(new RnwSweave()));
      weaveTypes_.push_back(boost::shared_ptr<RnwWeave>(new RnwKnitr()));
   }
   friend const RnwWeaveRegistry& weaveRegistry();

public:
   typedef std::vector<boost::shared_ptr<RnwWeave> > RnwWeaveTypes;


public:
   std::string printableTypeNames() const
   {
      std::string str;
      for (std::size_t i=0; i<weaveTypes_.size(); i++)
      {
         str.append(weaveTypes_[i]->name());
         if (i != (weaveTypes_.size() - 1))
            str.append(", ");
         if (i == (weaveTypes_.size() - 2))
            str.append("and ");
      }
      return str;
   }

   RnwWeaveTypes weaveTypes() const { return weaveTypes_; }

   boost::shared_ptr<RnwWeave> findTypeIgnoreCase(const std::string& name)
                                                                        const
   {
      for (boost::shared_ptr<RnwWeave> weaveType : weaveTypes_)
      {
         if (boost::algorithm::iequals(weaveType->name(), name))
            return weaveType;
      }

      return boost::shared_ptr<RnwWeave>();
   }

private:
   RnwWeaveTypes weaveTypes_;
};


const RnwWeaveRegistry& weaveRegistry()
{
   static RnwWeaveRegistry instance;
   return instance;
}

std::string weaveTypeForFile(const core::tex::TexMagicComments& magicComments)
{
   // first see if the file contains an rnw weave magic comment
   for (const core::tex::TexMagicComment& mc : magicComments)
   {
      if (boost::algorithm::iequals(mc.scope(), "rnw") &&
          boost::algorithm::iequals(mc.variable(), "weave"))
      {
         return mc.value();
      }
   }

   // if we didn't find a directive then inspect project & global config
   if (projects::projectContext().hasProject())
      return projects::projectContext().config().defaultSweaveEngine;
   else
      return prefs::userPrefs().defaultSweaveEngine();
}

std::string driverForFile(const core::tex::TexMagicComments& magicComments)
{
   for (const core::tex::TexMagicComment& mc : magicComments)
   {
      if (boost::algorithm::iequals(mc.scope(), "rnw") &&
          boost::algorithm::iequals(mc.variable(), "driver"))
      {
         std::string driver = mc.value();

         // The driver is interpolated into the weave command as a bare R
         // expression (not a quoted string literal), so unlike the file and
         // encoding we cannot escape it. To keep an untrusted document from
         // injecting arbitrary R code via a "% !Rnw driver = ..." magic
         // comment, only accept values that look like a plain, optionally
         // namespaced function reference -- e.g. "RweaveLatex",
         // "cacheSweave::cacheSweaveDriver", or "RweaveLatex()". Anything else
         // is rejected and the default driver is used instead.
         static const boost::regex driverPattern(
            "^\\s*"
            "(?:[a-zA-Z.][a-zA-Z0-9._]*:{2,3})?"   // optional pkg:: or pkg:::
            "[a-zA-Z.][a-zA-Z0-9._]*"              // function name
            "\\s*(?:\\(\\s*\\))?"                  // optional empty arg list
            "\\s*$");

         if (regex_utils::match(driver, driverPattern))
            return driver;

         WLOGF("Ignoring invalid Sweave driver '{}' specified in magic comment",
               driver);
         return std::string();
      }
   }

   return std::string();
}


void onWeaveProcessExit(boost::shared_ptr<RnwWeave> pRnwWeave,
                        int exitCode,
                        const std::string& output,
                        const FilePath& rnwPath,
                        const CompletedFunction& onCompleted)
{
   if (exitCode == EXIT_SUCCESS)
   {
      // pickup concordance if there is any
      rnw_concordance::Concordances concordances;
      Error error = rnw_concordance::readIfExists(rnwPath, &concordances);
      if (error)
         LOG_ERROR(error);

      // return success
      onCompleted(Result::success(concordances));
   }
   else
   {
      // parse for errors
      core::tex::LogEntries entries;
      Error error = pRnwWeave->parseOutputForErrors(output, rnwPath, &entries);
      if (error)
         LOG_ERROR(error);

      if (!entries.empty())
      {
         onCompleted(Result::error(entries));
      }
      else
      {
         // don't return an error message because the process almost
         // certainly already printed something to stderr
         onCompleted(Result::error(std::string()));
      }
   }
}

} // anonymous namespace

void runTangle(const std::string& filePath,
               const std::string& encoding,
               const std::string& rnwWeave)
{
   using namespace module_context;
   boost::shared_ptr<RnwWeave> pWeave =
                         weaveRegistry().findTypeIgnoreCase(rnwWeave);
   if (!pWeave)
   {
      consoleWriteError("Unknown Rnw weave type: " + rnwWeave + "\n");
   }
   else
   {
      Error error = pWeave->tangle(filePath, encoding);
      if (error)
         consoleWriteError(r::endUserErrorMessage(error) + "\n");
   }
}

void runWeave(const core::FilePath& rnwPath,
              const std::string& encoding,
              const core::tex::TexMagicComments& magicComments,
              const boost::function<void(const std::string&)>& onOutput,
              const CompletedFunction& onCompleted)
{
   // remove existing concordance file (if any)
   rnw_concordance::removePrevious(rnwPath);

   // get the R bin dir
   FilePath rBin;
   Error error = rBinDir(&rBin);
   if (error)
   {
      LOG_ERROR(error);
      onCompleted(Result::error(error.getSummary()));
      return;
   }

   // R exe path differs by platform
#ifdef _WIN32
   FilePath rBinPath = rBin.completePath("Rterm.exe");
#else
   FilePath rBinPath = rBin.completePath("R");
#endif

   // determine the active sweave engine
   std::string weaveType = weaveTypeForFile(magicComments);
   boost::shared_ptr<RnwWeave> pRnwWeave = weaveRegistry()
                                             .findTypeIgnoreCase(weaveType);

   // determine the driver (if any)
   std::string driver = driverForFile(magicComments);

   // run the weave
   if (pRnwWeave)
   {
      std::vector<std::string> args = pRnwWeave->commandArgs(
                                                         rnwPath.getFilename(),
                                                         encoding,
                                                         driver);

      // call back-end
      Error error = compile_pdf_supervisor::runProgram(
               rBinPath,
               args,
               core::system::Options(),
               rnwPath.getParent(),
               onOutput,
               boost::bind(onWeaveProcessExit,
                                 pRnwWeave, _1, _2, rnwPath, onCompleted));
      if (error)
      {
         LOG_ERROR(error);
         onCompleted(Result::error(error.getSummary()));
      }
   }
   else
   {
      onCompleted(Result::error(
         "Unknown Rnw weave method '" + weaveType + "' specified (valid " +
         "values are " + weaveRegistry().printableTypeNames() + ")"));
   }
}

core::json::Value chunkOptions(const std::string& weaveType)
{
   boost::shared_ptr<RnwWeave> pRnwWeave = weaveRegistry()
                                             .findTypeIgnoreCase(weaveType);
   if (pRnwWeave)
      return pRnwWeave->chunkOptions();
   else
      return core::json::Value();
}

core::json::Array supportedTypes()
{
   // query for list of supported types
   json::Array array;
   for (boost::shared_ptr<RnwWeave> pRnwWeave : weaveRegistry().weaveTypes())
   {
      json::Object object;
      object["name"] = pRnwWeave->name();
      object["package_name"] = pRnwWeave->packageName();
      object["inject_concordance"] = pRnwWeave->injectConcordance();
      object["uses_code_for_options"] = pRnwWeave->usesCodeForOptions();
      object["force_echo_on_exec"] = pRnwWeave->forceEchoOnExec();
      array.push_back(object);
   }
   return array;
}

void getTypesInstalledStatus(json::Object* pObj)
{
   // query for status of all rnw weave types
   for (boost::shared_ptr<RnwWeave> pRnwWeave : weaveRegistry().weaveTypes())
   {
      std::string n = string_utils::toLower(pRnwWeave->name() + "_installed");
      (*pObj)[n] = pRnwWeave->isInstalled();
   }
}

} // namespace rnw_weave
} // namespace tex
} // namespace modules
} // namespace session
} // namespace rstudio

