// docker-finance | modern accounting for the power-user
//
// Copyright (C) 2021-2025 Aaron Fiore (Founder, Evergreen Crypto LLC)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! \file
//! \author Aaron Fiore (Founder, Evergreen Crypto LLC)
//! \note File intended to be loaded into ROOT.cern framework / Cling interpreter
//! \since docker-finance 1.1.0

#ifndef CONTAINER_SRC_ROOT_COMMON_UTILITY_HH_
#define CONTAINER_SRC_ROOT_COMMON_UTILITY_HH_

#include <ctime>
#include <filesystem>
#include <initializer_list>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "./type.hh"

//! \namespace dfi
//! \since docker-finance 1.0.0
namespace dfi
{
//! \namespace dfi::common
//! \brief Shared common functionality
//! \since docker-finance 1.1.0
namespace common
{

//! \concept Exception
//! \brief Exception type concept for `dfi` exceptions
//! \ingroup cpp_common_impl
//! \since docker-finance 1.1.0
template <typename t_exception>
concept Exception = std::derived_from<t_exception, type::Exception>;

//! \brief Throw exception using `dfi` exception type
//! \param message Message to pass to exception type
//! \param location Source location of exception
//! \ingroup cpp_common_impl
//! \since docker-finance 1.1.0
template <Exception t_exception>
inline void throw_ex(
    const std::string& message = {},
    const std::source_location location = std::source_location::current())
{
  // TODO(unassigned): when throwing because of heap allocation
  std::string msg;
  msg += std::string("     \n\tFILE = ") + location.file_name();
  msg += std::string("     \n\tFUNC = ") + location.function_name();
  msg += std::string("     \n\tLINE = ") + location.line();
  msg += std::string("     \n\tWHAT = ") + message;
  throw t_exception(msg);
}

//! \brief Throw exception using `dfi` exception type (but only on condition)
//! \param condition If boolean condition is true, throw
//! \param message Message to pass to exception type
//! \param location Source location of exception
//! \ingroup cpp_common_impl
//! \since docker-finance 1.1.0
template <Exception t_exception>
inline void throw_ex_if(
    const bool condition,
    const std::string& message = {},
    const std::source_location location = std::source_location::current())
{
  if (condition)
    {
      throw_ex<t_exception>(message, location);
    }
}

// TODO(afiore): add logger with loglevels

//! \brief Process a line of code or interpreter command
//! \since docker-finance 1.1.0
inline void line(const std::string& line)
{
  using EErrorCode = ::TInterpreter::EErrorCode;
  auto ecode = std::make_unique<EErrorCode>();

  // TODO(afiore): log levels
  std::cout << "Interpreting: '" << line << "'" << std::endl;
  gInterpreter->ProcessLine(line.c_str(), ecode.get());
  throw_ex_if<type::RuntimeError>(*ecode != EErrorCode::kNoError, line);
}

//! \brief Load file with given path into interpreter
//! \details Can load a source file or library file
//! \param path Operating system path (absolute or relative)
//! \since docker-finance 1.1.0
inline void load(const std::string& path)
{
  throw_ex_if<type::RuntimeError>(
      !std::filesystem::is_regular_file(path), "invalid file: '" + path + "'");

  // TODO(afiore): log levels
  std::cout << "Loading: '" << path << "'" << std::endl;
  gInterpreter->LoadFile(path.c_str());
}

//! \brief Load files with given paths into interpreter
//! \details Can load source files and/or library files
//! \param path Operating system paths (absolute or relative)
//! \since docker-finance 1.1.0
inline void load(const std::initializer_list<std::string>& paths)
{
  for (const auto& path : paths)
    load(path);
}

//! \brief Wrappers to commonly used interpreter abstractions
//! \ingroup cpp_common_impl
//! \deprecated This will be removed in the v2 API; use the `dfi::common` free functions instead
//! \since docker-finance 1.0.0
class Command
{
 public:
  Command() = default;
  ~Command() = default;

  Command(const Command&) = default;
  Command& operator=(const Command&) = default;

  Command(Command&&) = default;
  Command& operator=(Command&&) = default;

 public:
  //! \deprecated This will be removed in the v2 API; use the `dfi::common` free function instead
  //! \since docker-finance 1.0.0
  static void load(const std::string& path) { ::dfi::common::load(path); }

  //! \deprecated This will be removed in the v2 API; use the `dfi::common` free function instead
  //! \since docker-finance 1.0.0
  static void load(const std::initializer_list<std::string>& paths)
  {
    ::dfi::common::load(paths);
  }
};

//! \brief Unload file with given path out of interpreter
//! \details Can unload a source file or library file
//! \param path Operating system path (absolute or relative)
//! \since docker-finance 1.1.0
inline void unload(const std::string& path)
{
  throw_ex_if<type::RuntimeError>(
      !std::filesystem::is_regular_file(path), "invalid file: '" + path + "'");

  // TODO(afiore): log levels
  std::cout << "Unloading: '" << path << "'" << std::endl;
  gInterpreter->UnloadFile(path.c_str());
}

//! \brief Unload files with given paths out of interpreter
//! \details Can unload source files and/or library files
//! \param path Operating system paths (absolute or relative)
//! \since docker-finance 1.1.0
inline void unload(const std::initializer_list<std::string>& paths)
{
  for (const auto& path : paths)
    unload(path);
}

//! \brief Reload file with given path out of/into interpreter
//! \details Can reload source files and/or library files
//! \param path Operating system paths (absolute or relative)
//! \since docker-finance 1.1.0
inline void reload(const std::string& path)
{
  unload(path);
  load(path);
}

//! \brief Reload files with given paths out of/into interpreter
//! \details Can reload source files and/or library files
//! \param path Operating system paths (absolute or relative)
//! \since docker-finance 1.1.0
inline void reload(const std::initializer_list<std::string>& paths)
{
  unload(paths);
  load(paths);
}

//! \brief Add an include directory to interpreter
//! \param path Path to include (absolute or relative)
//! \warning Only pass the complete path to directory and not "-I"
//! \warning To pass multiple paths, call this function multiple times
//! \exception type::RuntimeError if path doesn't exist or cannot be included
//! \since docker-finance 1.1.0
inline void add_include_dir(const std::string& path)
{
  throw_ex_if<type::RuntimeError>(
      !std::filesystem::is_directory(path),
      "invalid directory: '" + path + "'");

  // TODO(afiore): log levels
  std::cout << "Adding: '" << path << "'" << std::endl;
  gInterpreter->AddIncludePath(path.c_str());
}

//! \brief Add a linked library into interpreter
//! \param path Path of library to add (absolute or relative)
//! \warning Only pass the complete path to library and not "-l"
//! \warning To pass multiple libraries, call this function multiple times
//! \exception type::RuntimeError if library doesn't exist or cannot be linked
//! \since docker-finance 1.1.0
inline void add_linked_lib(const std::string& path)
{
  load(path);
}

//! \brief Remove a linked library from interpreter
//! \param path Path of library to remove (absolute or relative)
//! \warning Only pass the complete path to library and not "-l"
//! \warning To pass multiple libraries, call this function multiple times
//! \exception type::RuntimeError if library doesn't exist or cannot be linked
//! \since docker-finance 1.1.0
inline void remove_linked_lib(const std::string& path)
{
  unload(path);
}

//! \brief Get underlying environment variable
//! \param var Environment variable
//! \return Value of given environment variable
//! \exception type::RuntimeError Throws if env var is not set or unavailable
//! \note ROOT environment variables include shell (and shell caller) environment
//! \since docker-finance 1.0.0
std::string get_env(const std::string& var)
{
  const auto* env = gSystem->Getenv(var.c_str());

  throw_ex_if<type::RuntimeError>(
      !env, "'" + var + "' is not set or is unavailable");

  return std::string{env};
}

//! \brief Execute a command in operating system shell
//! \param cmd Operating system shell command [args]
//! \return Return value of command
//! \since docker-finance 1.0.0
int exec(const std::string& cmd)
{
  return gSystem->Exec(cmd.c_str());
}

//! \brief Make current timestamp
//! \return timestamp in "yyyy-mm-ddThh:mm:ssZ" format
//! \since docker-finance 1.0.0
std::string make_timestamp()
{
  const std::time_t t{std::time({})};
  std::vector<char> time(std::size("yyyy-mm-ddThh:mm:ssZ"));
  std::strftime(time.data(), time.size(), "%FT%TZ", std::gmtime(&t));
  return std::string{time.data()};
}

//! \brief Base pseudo-path handler for various pluggables (plugins and macros)
//!
//! \details Handles pluggable paths, typically used by pluggable
//!   implementations when auto-(un|re)loading.
//!
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
class PluggablePath
{
 public:
  //! \brief Parses (or re-parses) constructed types
  //! \warning Only call this function after constructing if underlying type::PluggablePath has been changed (post-construction)
  //! \return NPI reference
  auto& parse()
  {
    // Parse out pseudo tag
    const std::string type{m_pseudo.substr(0, m_pseudo.find('/'))};

    std::string pruned{m_pseudo};
    pruned.erase(0, pruned.find('/') + 1);

    // Set family group
    m_family = pruned;

    // Set parent directory
    m_parent = pruned.substr(0, pruned.find('/'));

    // Set child (filename)
    m_child = pruned.substr(pruned.find_last_of('/') + 1);

    // Set absolute path
    std::string absolute{pruned};
    if (type == "repo")
      {
        m_is_repo = true;
        absolute.insert(0, m_path.repo());
      }
    else if (type == "custom")
      {
        m_is_custom = true;
        absolute.insert(0, m_path.custom());
      }
    else
      {
        throw_ex<type::RuntimeError>(
            "must be of type 'repo/<relative>' or 'custom/<relative>'");
      }
    m_absolute = std::move(absolute);

    return *this;
  }

 protected:
  //! \brief Sets pluggable absolute path from given pseudo path
  //! \param pseudo Pseudo-path ('repo/<relative>' or 'custom/<relative>')
  //! \param base Operating system absolute paths to pluggable locations
  //! \exception type::RuntimeError If not a valid pseudo-path
  PluggablePath(const std::string& pseudo, const type::PluggablePath& path)
      : m_pseudo(pseudo), m_path(path), m_is_repo(false), m_is_custom(false)
  {
    parse();
  }
  ~PluggablePath() = default;

  PluggablePath(const PluggablePath&) = default;
  PluggablePath& operator=(const PluggablePath&) = default;

  PluggablePath(PluggablePath&&) = default;
  PluggablePath& operator=(PluggablePath&&) = default;

 public:
  //! \brief Mutator to underlying type::PluggablePath
  //! \warning Use with caution: underlying implementation may change
  auto& operator()() { return m_path; }

  //! \brief Accessor to underlying type::PluggablePath
  //! \warning Use with caution: underlying implementation may change
  const auto& operator()() const { return m_path; }

 public:
  //! \return The pluggable's pseudo-path
  const std::string& pseudo() const { return m_pseudo; }

  //! \return The pluggable's operating system absolute path
  const std::string& absolute() const { return m_absolute; }

  // TODO(unassigned): relative() to current working dir

  //! \return The pluggable's parent directory
  //! \note This also represents the expected namespace used for pluggable auto-loading
  const std::string& parent() const { return m_parent; }

  //! \return The pluggable's child (filename)
  const std::string& child() const { return m_child; }

  //! \return The pluggable's group of parent member and child filename
  const std::string& family() const { return m_family; }

  //! \return true if pseudo-path describes a repository location
  bool is_repo() const { return m_is_repo; }

  //! \return true if pseudo-path describes a custom location
  bool is_custom() const { return m_is_custom; }

 private:
  type::PluggablePath m_path;
  std::string m_pseudo, m_absolute;
  std::string m_parent, m_family, m_child;
  bool m_is_repo, m_is_custom;
};

//! \brief Base namespace handler for various pluggables (plugins and macros)
//!
//! \details Handles pluggable namespace, typically used by pluggable
//!   implementations when auto-(un|re)loading.
//!
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
class PluggableSpace
{
 protected:
  explicit PluggableSpace(const type::PluggableSpace& space) : m_space(space) {}
  ~PluggableSpace() = default;

  PluggableSpace(const PluggableSpace&) = default;
  PluggableSpace& operator=(const PluggableSpace&) = default;

  PluggableSpace(PluggableSpace&&) = default;
  PluggableSpace& operator=(PluggableSpace&&) = default;

 public:
  //! \brief Mutator to underlying type::PluggableSpace
  //! \warning Use with caution: underlying implementation may change
  auto& operator()() { return m_space; }

  //! \brief Accessor to underlying type::PluggableSpace
  //! \warning Use with caution: underlying implementation may change
  const auto& operator()() const { return m_space; }

 public:
  //! \return The pluggable's outer namespace
  const std::string& outer() const { return m_space.outer(); }

  //! \return The pluggable's inner namespace
  const std::string& inner() const { return m_space.inner(); }

  //! \return true if the outer namespace was set
  bool has_outer() const { return !m_space.outer().empty(); }

  //! \return true if the inner namespace was set
  bool has_inner() const { return !m_space.inner().empty(); }

 private:
  type::PluggableSpace m_space;
};

//! \brief Base argument handler for various pluggables (plugins and macros)
//!
//! \details Handles pluggable arguments, typically used by pluggable
//!   implementations when auto-(un|re)loading.
//!
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
class PluggableArgs
{
 protected:
  explicit PluggableArgs(const type::PluggableArgs& args) : m_args(args) {}
  ~PluggableArgs() = default;

  PluggableArgs(const PluggableArgs&) = default;
  PluggableArgs& operator=(const PluggableArgs&) = default;

  PluggableArgs(PluggableArgs&&) = default;
  PluggableArgs& operator=(PluggableArgs&&) = default;

 public:
  //! \brief Mutator to underlying type::PluggableArgs
  //! \warning Use with caution: underlying implementation may change
  auto& operator()() { return m_args; }

  //! \brief Accessor to underlying type::PluggableArgs
  //! \warning Use with caution: underlying implementation may change
  const auto& operator()() const { return m_args; }

 public:
  //! \return The pluggable's loader argument
  const std::string& load() const { return m_args.load(); }

  //! \return The pluggable's unloader argument
  const std::string& unload() const { return m_args.unload(); }

  //! \return true if the load argument was set
  bool has_load() const { return !m_args.load().empty(); }

  //! \return true if the unload argument was set
  bool has_unload() const { return !m_args.unload().empty(); }

 private:
  type::PluggableArgs m_args;
};

//! \concept dfi::common::PPath
//! \brief Pluggable base implementation constraint (PluggablePath)
//! \ref dfi::common::PluggablePath
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
template <typename t_path>
concept PPath = std::derived_from<t_path, PluggablePath>;

//! \concept dfi::common::PSpace
//! \brief Pluggable base implementation constraint (PluggableSpace)
//! \ref dfi::common::PluggableSpace
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
template <typename t_space>
concept PSpace = std::derived_from<t_space, PluggableSpace>;

//! \concept dfi::common::PArgs
//! \brief Pluggable base implementation constraint (PluggableArgs)
//! \ref dfi::common::PluggableArgs
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
template <typename t_args>
concept PArgs = std::derived_from<t_args, PluggableArgs>;

//! \brief Base pluggable handler
//! \ingroup cpp_plugin_impl cpp_macro_impl
//! \since docker-finance 1.1.0
template <PPath t_path, PSpace t_space, PArgs t_args>
class Pluggable
{
 protected:
  explicit Pluggable(const type::Pluggable<t_path, t_space, t_args>& plug)
      : m_plug(plug)
  {
  }
  ~Pluggable() = default;

  Pluggable(const Pluggable&) = default;
  Pluggable& operator=(const Pluggable&) = default;

  Pluggable(Pluggable&&) = default;
  Pluggable& operator=(Pluggable&&) = default;

 public:
  //! \brief Mutator to underlying type::Pluggable
  //! \warning Use with caution: underlying implementation may change
  auto& operator()() { return m_plug; }

  //! \brief Accessor to underlying type::Pluggable
  //! \warning Use with caution: underlying implementation may change
  const auto& operator()() const { return m_plug; }

 public:
  //! \brief Loads a single pluggable
  //! \return NPI reference
  //! \ingroup cpp_plugin_impl cpp_macro_impl
  //! \since docker-finance 1.1.0
  const auto& load() const
  {
    // Load pluggable file
    ::dfi::common::load(m_plug.path().absolute());

    // Execute pluggable's loader
    const std::string s{
        "dfi::" + m_plug.space().outer() + "::" + m_plug.space().inner()};
    ::dfi::common::line(s + "::load(\"" + m_plug.args().load() + "\")");

    return *this;
  }

  //! \brief Unloads a single pluggable
  //! \return NPI reference
  //! \ingroup cpp_plugin_impl cpp_macro_impl
  //! \since docker-finance 1.1.0
  const auto& unload() const
  {
    // Execute pluggable's unloader
    const std::string s{
        "dfi::" + m_plug.space().outer() + "::" + m_plug.space().inner()};
    ::dfi::common::line(s + "::unload(\"" + m_plug.args().unload() + "\")");

    // Unload pluggable file
    ::dfi::common::unload(m_plug.path().absolute());

    return *this;
  }

  //! \brief Reloads a single pluggable
  //! \return NPI reference
  //! \ingroup cpp_plugin_impl cpp_macro_impl
  //! \since docker-finance 1.1.0
  const auto& reload() const { return unload().load(); }

 private:
  type::Pluggable<t_path, t_space, t_args> m_plug;
};

}  // namespace common
}  // namespace dfi

#endif  // CONTAINER_SRC_ROOT_COMMON_UTILITY_HH_

// # vim: sw=2 sts=2 si ai et
