// -*- c++ -*-

// This file is part of the Collective Variables module (Colvars).
// The original version of Colvars and its updates are located at:
// https://github.com/Colvars/colvars
// Please update all Colvars source files before making any changes.
// If you wish to distribute your changes, please submit them to the
// Colvars repository at GitHub.

#if defined(__cplusplus) && __cplusplus >= 202002L
#include <version>
#endif

#ifdef __cpp_lib_filesystem
#include <filesystem>
#else
#ifdef __has_include
# if __has_include(<filesystem>)
#  include <filesystem> // MSVC only defines __cpp_lib_filesystem after include
# endif
#endif // __has_include
#endif // __cpp_lib_filesystem

#if defined(_WIN32)
#include <io.h>
#endif

#if defined(_WIN32) && !defined(__CYGWIN__)
// Using access() to check if a file exists (until we can assume C++14/17)
#include <direct.h>
#else
#include <unistd.h>
#endif

#include <cerrno>
#include <cstdio>

#include <list>
#include <map>
#include <sstream>
#include <fstream>

#include "colvarmodule.h"
#include "colvarproxy_io.h"


colvarproxy_io::colvarproxy_io()
{
  restart_frequency_engine = 0;
  input_stream_error_ = new std::istringstream();
  input_stream_error_->setstate(std::ios::badbit);
  output_stream_error_ = new std::ostringstream();
  output_stream_error_->setstate(std::ios::badbit);
}


colvarproxy_io::~colvarproxy_io()
{
  delete input_stream_error_;
  close_input_streams();
  delete output_stream_error_;
  close_output_streams();
}


bool colvarproxy_io::io_available()
{
  return false;
}


int colvarproxy_io::get_frame(long int&)
{
  return COLVARS_NOT_IMPLEMENTED;
}


int colvarproxy_io::set_frame(long int)
{
  return COLVARS_NOT_IMPLEMENTED;
}


std::string colvarproxy_io::get_current_work_dir() const
{
#ifdef __cpp_lib_filesystem

  return std::filesystem::current_path().string();

#else

  // Legacy code
  size_t constexpr buf_size = 3001;
  char buf[buf_size];

#if defined(_WIN32) && !defined(__CYGWIN__)
  char *getcwd_result = ::_getcwd(buf, buf_size);
#else
  char *getcwd_result = ::getcwd(buf, buf_size);
#endif

  if (getcwd_result == nullptr) {
    cvm::error_static("Error: cannot read the current working directory.\n", COLVARS_INPUT_ERROR);
    return std::string("");
  }

  return std::string(getcwd_result);
#endif
}


std::string colvarproxy_io::join_paths(std::string const &path1, std::string const &path2) const
{
#ifdef __cpp_lib_filesystem

  return (std::filesystem::path(path1) / std::filesystem::path(path2)).string();

#else

  // Legacy code
#if defined(_WIN32) && !defined(__CYGWIN__)
  return (path1 + "\\" + path2);
#else
  return (path1 + "/" + path2);
#endif

#endif
}


int colvarproxy_io::backup_file(char const *filename)
{
  // Simplified version of NAMD_file_exists()
  int exit_code;
  do {
#if defined(_WIN32) && !defined(__CYGWIN__)
    // We could use _access_s here, but it is probably too new
    exit_code = _access(filename, 00);
#else
    exit_code = access(filename, F_OK);
#endif
  } while ((exit_code != 0) && (errno == EINTR));
  if (exit_code != 0) {
    if (errno == ENOENT) {
      // File does not exist
      return COLVARS_OK;
    } else {
      return cvm::error_static("Unknown error while checking if file \""+
                        std::string(filename)+"\" exists.\n", COLVARS_ERROR);
    }
  }

  // The file exists, then rename it
  if (std::string(filename).rfind(std::string(".colvars.state")) !=
      std::string::npos) {
    return rename_file(filename, (std::string(filename)+".old").c_str());
  } else {
    return rename_file(filename, (std::string(filename)+".BAK").c_str());
  }
}


int colvarproxy_io::remove_file(char const *filename)
{
  int error_code = COLVARS_OK;
#if defined(_WIN32) && !defined(__CYGWIN__)
  // Because the file may be open by other processes, rename it to filename.old
  std::string const renamed_file(std::string(filename)+".old");
  // It may still be there from an interrupted run, so remove it to be safe
  std::remove(renamed_file.c_str());
  int rename_exit_code = 0;
  while ((rename_exit_code = std::rename(filename,
                                         renamed_file.c_str())) != 0) {
    if (errno == EINTR) continue;
    error_code |= COLVARS_FILE_ERROR;
    break;
  }
  // Ask to remove filename.old, but ignore any errors raised
  std::remove(renamed_file.c_str());
#else
  if (std::remove(filename)) {
    if (errno != ENOENT) {
      error_code |= COLVARS_FILE_ERROR;
    }
  }
#endif
  if (error_code != COLVARS_OK) {
    return cvm::error_static("Error: in removing file \""+std::string(filename)+
                      "\".\n.",
                      error_code);
  }
  return COLVARS_OK;
}


int colvarproxy_io::rename_file(char const *filename, char const *newfilename)
{
  int error_code = COLVARS_OK;
#if defined(_WIN32) && !defined(__CYGWIN__)
  // On straight Windows, must remove the destination before renaming it
  if (_access(newfilename, 00) == 0) {
    error_code |= remove_file(newfilename);
  }
#endif
  int rename_exit_code = 0;
  while ((rename_exit_code = std::rename(filename, newfilename)) != 0) {
    if (errno == EINTR) continue;
    // Call log() instead of error to allow the next try
    cvm::main()->log("Error: in renaming file \""+std::string(filename)+"\" to \""+
             std::string(newfilename)+"\".\n.");
    error_code |= COLVARS_FILE_ERROR;
    if (errno == EXDEV) continue;
    break;
  }
  return rename_exit_code ? error_code : COLVARS_OK;
}


int colvarproxy_io::set_input_prefix(std::string const &prefix)
{
  // set input restart name and strip the extension, if present
  input_prefix_str = prefix;
  if (input_prefix_str.rfind(".colvars.state") != std::string::npos) {
    input_prefix_str.erase(input_prefix_str.rfind(".colvars.state"),
                           std::string(".colvars.state").size());
  }
  return COLVARS_OK;
}


int colvarproxy_io::set_output_prefix(std::string const &prefix)
{
  // set input restart name and strip the extension, if present
  output_prefix_str = prefix;
  if (output_prefix_str.rfind(".colvars.state") != std::string::npos) {
    output_prefix_str.erase(output_prefix_str.rfind(".colvars.state"),
                            std::string(".colvars.state").size());
  }
  return COLVARS_OK;
}


int colvarproxy_io::set_restart_output_prefix(std::string const &prefix)
{
  // set input restart name and strip the extension, if present
  restart_output_prefix_str = prefix;
  if (restart_output_prefix_str.rfind(".colvars.state") != std::string::npos) {
    restart_output_prefix_str.erase(restart_output_prefix_str.rfind(".colvars.state"),
                                    std::string(".colvars.state").size());
  }
  return COLVARS_OK;
}


int colvarproxy_io::set_default_restart_frequency(int freq)
{
  // TODO check for compatibility with colvarsRestartFrequency
  restart_frequency_engine = freq;
  return COLVARS_OK;
}



std::istream &colvarproxy_io::input_stream(std::string const &input_name,
                                           std::string const description,
                                           bool error_on_fail)
{
  if (!io_available()) {
    cvm::error_static("Error: trying to access an input file/channel "
               "from the wrong thread.\n", COLVARS_BUG_ERROR);
    return *input_stream_error_;
  }

  if (colvarproxy_io::input_stream_exists(input_name)) {
    std::ifstream *ifs =
      dynamic_cast<std::ifstream *>(input_streams_[input_name]);
    if (ifs && !ifs->is_open()) {
      // This file was opened before, re-open it.  Using std::ios::binary to
      // work around differences in line termination conventions
      // See https://github.com/Colvars/colvars/commit/8236879f7de4
      ifs->open(input_name.c_str(), std::ios::binary);
    }
  } else {
    input_streams_[input_name] = new std::ifstream(input_name.c_str(),
                                                   std::ios::binary);
  }

  if (input_streams_[input_name]->fail() && error_on_fail) {
    cvm::error_static("Error: cannot open "+description+" \""+input_name+"\".\n",
               COLVARS_FILE_ERROR);
  }

  return *(input_streams_[input_name]);
}


std::istream &
colvarproxy_io::input_stream_from_string(std::string const &input_name,
                                         std::string const &content,
                                         std::string const description)
{
  if (!io_available()) {
    cvm::error_static("Error: trying to access an input file/channel "
               "from the wrong thread.\n", COLVARS_BUG_ERROR);
    return *input_stream_error_;
  }

  if (colvarproxy_io::input_stream_exists(input_name)) {

    std::istringstream *iss =
      dynamic_cast<std::istringstream *>(input_streams_[input_name]);
    if (iss) {
      // If there is already a stringstream, replace it
      delete iss;
    } else {
      std::ifstream *ifs =
        dynamic_cast<std::ifstream *>(input_streams_[input_name]);
      if (ifs) {
        if (ifs->is_open()) {
          ifs->close();
        }
      }
    }
  }

  input_streams_[input_name] = new std::istringstream(content);

  return *(input_streams_[input_name]);
}


bool colvarproxy_io::input_stream_exists(std::string const &input_name)
{
  return (input_streams_.count(input_name) > 0);
}


int colvarproxy_io::close_input_stream(std::string const &input_name)
{
  if (colvarproxy_io::input_stream_exists(input_name)) {
    std::ifstream *ifs = dynamic_cast<std::ifstream *>(input_streams_[input_name]);
    if (ifs) {
      if (ifs->is_open()) {
        ifs->close();
      }
    } else {
      // From a string, just rewind to the begining
      std::istringstream * iss = dynamic_cast<std::istringstream *>(input_streams_[input_name]);
      if (iss) {
        iss->clear();
        iss->seekg(0);
      }
    }
    return COLVARS_OK;
  }
  return cvm::error_static("Error: input file/channel \""+input_name+
                    "\" does not exist.\n", COLVARS_FILE_ERROR);
}


int colvarproxy_io::delete_input_stream(std::string const &input_name)
{
  if (colvarproxy_io::close_input_stream(input_name) == COLVARS_OK) {
    std::ifstream *ifs = dynamic_cast<std::ifstream *>(input_streams_[input_name]);
    if (ifs) {
      delete ifs;
    } else {
      std::istringstream * iss = dynamic_cast<std::istringstream *>(input_streams_[input_name]);
      if (iss) {
        delete iss;
      }
    }
    input_streams_.erase(input_name);
    return COLVARS_OK;
  }
  return cvm::error_static("Error: input file/channel \""+input_name+
                    "\" does not exist.\n", COLVARS_FILE_ERROR);
}


int colvarproxy_io::close_input_streams()
{
  for (std::map<std::string,
         std::istream *>::iterator ii = input_streams_.begin();
       ii != input_streams_.end();
       ii++) {
    delete ii->second;
  }
  input_streams_.clear();
  return COLVARS_OK;
}


std::list<std::string> colvarproxy_io::list_input_stream_names() const
{
  std::list<std::string> result;
  for (std::map<std::string,
         std::istream *>::const_iterator ii = input_streams_.begin();
       ii != input_streams_.end();
       ii++) {
    result.push_back(ii->first);
  }
  return result;
}


std::ostream & colvarproxy_io::output_stream(std::string const &output_name,
                                             std::string const description)
{
  if (cvm::debug()) {
    cvm::main()->log("Using colvarproxy_io::output_stream()\n");
  }

  if (!io_available()) {
    cvm::error_static("Error: trying to access an output file/channel "
               "from the wrong thread.\n", COLVARS_BUG_ERROR);
    return *output_stream_error_;
  }

  if (colvarproxy_io::output_stream_exists(output_name)) {
    return *(output_streams_[output_name]);
  }

  backup_file(output_name.c_str());

  output_streams_[output_name] = new std::ofstream(output_name.c_str(), std::ios::binary);
  if (!*(output_streams_[output_name])) {
    cvm::error_static("Error: cannot write to "+description+" \""+output_name+"\".\n",
               COLVARS_FILE_ERROR);
  }

  return *(output_streams_[output_name]);
}


bool colvarproxy_io::output_stream_exists(std::string const &output_name)
{
  return (output_streams_.count(output_name) > 0);
}


int colvarproxy_io::flush_output_stream(std::string const &output_name)
{
  if (!io_available()) {
    // No-op
    return COLVARS_OK;
  }

  if (colvarproxy_io::output_stream_exists(output_name)) {
    (dynamic_cast<std::ofstream *>(output_streams_[output_name]))->flush();
    return COLVARS_OK;
  }

  return COLVARS_OK;
}


int colvarproxy_io::flush_output_streams()
{
  if (!io_available()) {
    return COLVARS_OK;
  }

  for (std::map<std::string, std::ostream *>::iterator osi = output_streams_.begin();
       osi != output_streams_.end();
       osi++) {
    (dynamic_cast<std::ofstream *>(osi->second))->flush();
  }

  return COLVARS_OK;
}


int colvarproxy_io::close_output_stream(std::string const &output_name)
{
  if (!io_available()) {
    return cvm::error_static("Error: trying to access an output file/channel "
                      "from the wrong thread.\n", COLVARS_BUG_ERROR);
  }

  if (colvarproxy_io::output_stream_exists(output_name)) {
    (dynamic_cast<std::ofstream *>(output_streams_[output_name]))->close();
    delete output_streams_[output_name];
    output_streams_.erase(output_name);
  }

  return COLVARS_OK;
}


int colvarproxy_io::close_output_streams()
{
  if (! io_available()) {
    return COLVARS_OK;
  }

  for (std::map<std::string, std::ostream *>::iterator osi = output_streams_.begin();
       osi != output_streams_.end();
       osi++) {
    (dynamic_cast<std::ofstream *>(osi->second))->close();
    delete osi->second;
  }
  output_streams_.clear();

  return COLVARS_OK;
}
