// file      : libbrep/common.hxx -*- C++ -*-
// license   : MIT; see accompanying LICENSE file

#ifndef LIBBREP_COMMON_HXX
#define LIBBREP_COMMON_HXX

#include <ratio>
#include <chrono>
#include <type_traits> // static_assert

#include <odb/query.hxx>

#include <libbpkg/package-name.hxx>

#include <libbrep/types.hxx>
#include <libbrep/utility.hxx>

// The uint16_t value range is not fully covered by SMALLINT PostgreSQL type
// to which uint16_t is mapped by default.
//
#pragma db value(uint16_t) type("INTEGER")

namespace brep
{
  // Use an image type to map bpkg::version to the database since there
  // is no way to modify individual components directly.
  //
  #pragma db value
  struct _version
  {
    uint16_t epoch;
    string canonical_upstream;
    string canonical_release;
    optional<uint16_t> revision;
    string upstream;
    optional<string> release;
  };
}

#include <libbpkg/manifest.hxx>

namespace brep
{
  using optional_version = optional<bpkg::version>;
  using _optional_version = optional<_version>;
}

// Prevent assert() macro expansion in get/set expressions. This should
// appear after all #include directives since the assert() macro is
// redefined in each <assert.h> inclusion.
//
#ifdef ODB_COMPILER
#  undef assert
#  define assert assert
void assert (int);
#endif

// We have to keep these mappings at the global scope instead of inside
// the brep namespace because they need to be also effective in the
// bpkg namespace from which we "borrow" types (and some of them use version).
//
#pragma db map type(bpkg::version) as(brep::_version) \
  to(brep::_version{(?).epoch,                        \
                    (?).canonical_upstream,           \
                    (?).canonical_release,            \
                    (?).revision,                     \
                    (?).upstream,                     \
                    (?).release})                     \
  from(bpkg::version ((?).epoch,                      \
                      std::move ((?).upstream),       \
                      std::move ((?).release),        \
                      (?).revision,                   \
                      0))

#pragma db map type(brep::optional_version) as(brep::_optional_version) \
  to((?)                                                                \
     ? brep::_version{(?)->epoch,                                       \
                      (?)->canonical_upstream,                          \
                      (?)->canonical_release,                           \
                      (?)->revision,                                    \
                      (?)->upstream,                                    \
                      (?)->release}                                     \
     : brep::_optional_version ())                                      \
  from((?)                                                              \
       ? bpkg::version ((?)->epoch,                                     \
                        std::move ((?)->upstream),                      \
                        std::move ((?)->release),                       \
                        (?)->revision,                                  \
                        0)                                              \
       : brep::optional_version ())

namespace brep
{
  // path
  //
  #pragma db map type(path) as(string) to((?).string ()) from(brep::path (?))

  using optional_path = optional<path>;
  using optional_string = optional<string>;

  #pragma db map type(optional_path) as(brep::optional_string) \
    to((?) ? (?)->string () : brep::optional_string ())        \
    from((?) ? brep::path (*(?)) : brep::optional_path ())

  #pragma db map type(dir_path) as(string)     \
    to((?).string ()) from(brep::dir_path (?))

  // Make sure that timestamp can be represented in nonoseconds without loss
  // of accuracy, so the following ODB mapping is adequate.
  //
  static_assert(
    std::ratio_greater_equal<timestamp::period,
                             std::chrono::nanoseconds::period>::value,
    "The following timestamp ODB mapping is invalid");

  // As it pointed out in libbutl/timestamp.mxx we will overflow in year 2262,
  // but by that time some larger basic type will be available for mapping.
  //
  #pragma db map type(timestamp) as(uint64_t)                 \
    to(std::chrono::duration_cast<std::chrono::nanoseconds> ( \
         (?).time_since_epoch ()).count ())                   \
    from(brep::timestamp (                                    \
      std::chrono::duration_cast<brep::timestamp::duration> ( \
        std::chrono::nanoseconds (?))))

  // version
  //
  using bpkg::version;

  // Sometimes we need to split the version into two parts: the part
  // that goes into the object id (epoch, canonical upstream, canonical
  // release, revision) and the original upstream and release. This is what
  // the canonical_version and upstream_version value types are for. Note that
  // upstream_version derives from version and uses it as storage. The idea
  // here is this: when we split the version, we often still want to have the
  // "whole" version object readily accessible and that's exactly what this
  // strange contraption is for. See package for an example on how everything
  // fits together.
  //
  // Note that the object id cannot contain an optional member which is why we
  // make the revision type uint16_t and represent nullopt as zero. This
  // should be ok for package object ids referencing the package manifest
  // version values because an absent revision and zero revision mean the
  // same thing.
  //
  #pragma db value
  struct canonical_version
  {
    uint16_t epoch;
    string   canonical_upstream;
    string   canonical_release;
    uint16_t revision;

    canonical_version () = default;

    explicit
    canonical_version (const version& v)
        : epoch (v.epoch),
          canonical_upstream (v.canonical_upstream),
          canonical_release (v.canonical_release),
          revision (v.effective_revision ()) {}

    bool
    empty () const noexcept
    {
      // Note that an empty canonical_upstream doesn't denote an empty
      // canonical_version. Remeber, that canonical_upstream doesn't include
      // rightmost digit-only zero components? So non-empty version("0") has
      // an empty canonical_upstream.
      //
      return epoch == 0                  &&
             canonical_upstream.empty () &&
             canonical_release.empty ()  &&
             revision == 0;
    }

    // Change collation to ensure the proper comparison of the "absent" release
    // with a specified one.
    //
    // The default collation for UTF8-encoded TEXT columns in PostgreSQL is
    // UCA-compliant. This makes the statement 'a' < '~' to be false, which
    // in turn makes the statement 2.1-alpha < 2.1 to be false as well.
    //
    // Unicode Collation Algorithm (UCA): http://unicode.org/reports/tr10/
    //
    #pragma db member(canonical_release) options("COLLATE \"C\"")
  };

  #pragma db value transient
  struct upstream_version: version
  {
    #pragma db member(upstream_) virtual(string)                 \
      get(this.upstream)                                         \
      set(this = brep::version (                                 \
            0, std::move (?), std::string (), brep::nullopt, 0))

    #pragma db member(release_) virtual(optional_string)                    \
      get(this.release)                                                     \
      set(this = brep::version (                                            \
            0, std::move (this.upstream), std::move (?), brep::nullopt, 0))

    upstream_version () = default;
    upstream_version (version v): version (move (v)) {}
    upstream_version&
    operator= (version v) {version& b (*this); b = v; return *this;}

    void
    init (const canonical_version& cv, const upstream_version& uv)
    {
      // Note: revert the zero revision mapping (see above).
      //
      *this = version (cv.epoch,
                       uv.upstream,
                       uv.release,
                       (cv.revision != 0
                        ? optional<uint16_t> (cv.revision)
                        : nullopt),
                       0);

      assert (cv.canonical_upstream == canonical_upstream &&
              cv.canonical_release == canonical_release);
    }
  };

  // Wildcard version. Satisfies any dependency constraint and is represented
  // as 0+0 (which is also the "stub version"; since a real version is always
  // greater than the stub version, we reuse it to signify a special case).
  //
  extern const version wildcard_version;

  // package_name
  //
  using bpkg::package_name;

  #pragma db value(package_name) type("CITEXT")

  #pragma db map type("CITEXT") as("TEXT") to("(?)::CITEXT") from("(?)::TEXT")

  // package_id
  //
  #pragma db value
  struct package_id
  {
    string tenant;
    package_name name;
    canonical_version version;

    package_id () = default;
    package_id (string t, package_name n, const brep::version& v)
        : tenant (move (t)),
          name (move (n)),
          version (v) {}
  };

  // repository_type
  //
  using bpkg::repository_type;
  using bpkg::to_repository_type;

  #pragma db map type(repository_type) as(string) \
    to(to_string (?))                             \
    from(brep::to_repository_type (?))

  // repository_url
  //
  using bpkg::repository_url;

  #pragma db map type(repository_url) as(string)                            \
    to((?).string ())                                                       \
    from((?).empty () ? brep::repository_url () : brep::repository_url (?))

  // repository_location
  //
  using bpkg::repository_location;

  #pragma db value
  struct _repository_location
  {
    repository_url  url;
    repository_type type;
  };

  // Note that the type() call fails for an empty repository location.
  //
  #pragma db map type(repository_location) as(_repository_location) \
    to(brep::_repository_location {(?).url (),                      \
                                   (?).empty ()                     \
                                   ? brep::repository_type::pkg     \
                                   : (?).type ()})                  \
    from(brep::repository_location (std::move ((?).url), (?).type))

  // repository_id
  //
  #pragma db value
  struct repository_id
  {
    string tenant;
    string canonical_name;

    repository_id () = default;
    repository_id (string t, string n)
        : tenant (move (t)), canonical_name (move (n)) {}
  };

  // build_class_expr
  //
  using bpkg::build_class_expr;
  using build_class_exprs = small_vector<build_class_expr, 1>;

  #pragma db value(build_class_expr) definition

  #pragma db member(build_class_expr::expr) transient
  #pragma db member(build_class_expr::underlying_classes) transient

  #pragma db member(build_class_expr::expression) virtual(string) before \
    get(this.string ())                                                  \
    set(this = brep::build_class_expr ((?), "" /* comment */))

  // build_constraints
  //
  using bpkg::build_constraint;
  using build_constraints = vector<build_constraint>;

  #pragma db value(build_constraint) definition

  // The primary reason why a package is unbuildable by the build bot
  // controller service.
  //
  enum class unbuildable_reason: std::uint8_t
  {
    stub,          // A stub,                                     otherwise
    test,          // A separate test (built as part of primary), otherwise
    external,      // From an external repository,                otherwise
    unbuildable    // From an internal unbuildable repository.
  };

  string
  to_string (unbuildable_reason);

  unbuildable_reason
  to_unbuildable_reason (const string&); // May throw invalid_argument.

  inline ostream&
  operator<< (ostream& os, unbuildable_reason r) {return os << to_string (r);}

  using optional_unbuildable_reason = optional<unbuildable_reason>;

  #pragma db map type(unbuildable_reason) as(string) \
    to(to_string (?))                                \
    from(brep::to_unbuildable_reason (?))

  #pragma db map type(optional_unbuildable_reason) as(brep::optional_string) \
    to((?) ? to_string (*(?)) : brep::optional_string ())                    \
    from((?)                                                                 \
    ? brep::to_unbuildable_reason (*(?))                                     \
    : brep::optional_unbuildable_reason ())                                  \

  // Version comparison operators.
  //
  // They allow comparing objects that have epoch, canonical_upstream,
  // canonical_release, and revision data members. The idea is that this
  // works for both query members of types version and canonical_version.
  // Note, though, that the object revisions should be comparable (both
  // optional, numeric, etc), so to compare version to query member or
  // canonical_version you may need to explicitly convert the version object
  // to canonical_version.
  //
  template <typename T1, typename T2>
  inline auto
  compare_version_eq (const T1& x, const T2& y, bool revision)
    -> decltype (x.revision == y.revision)
  {
    // Since we don't quite know what T1 and T2 are (and where the resulting
    // expression will run), let's not push our luck with something like
    // (!revision || x.revision == y.revision).
    //
    auto r (x.epoch == y.epoch &&
            x.canonical_upstream == y.canonical_upstream &&
            x.canonical_release == y.canonical_release);

    return revision
      ? r && x.revision == y.revision
      : r;
  }

  template <typename T1, typename T2>
  inline auto
  compare_version_ne (const T1& x, const T2& y, bool revision)
    -> decltype (x.revision == y.revision)
  {
    auto r (x.epoch != y.epoch ||
            x.canonical_upstream != y.canonical_upstream ||
            x.canonical_release != y.canonical_release);

    return revision
      ? r || x.revision != y.revision
      : r;
  }

  template <typename T1, typename T2>
  inline auto
  compare_version_lt (const T1& x, const T2& y, bool revision)
    -> decltype (x.revision == y.revision)
  {
    auto r (
      x.epoch < y.epoch ||
      (x.epoch == y.epoch && x.canonical_upstream < y.canonical_upstream) ||
      (x.epoch == y.epoch && x.canonical_upstream == y.canonical_upstream &&
       x.canonical_release < y.canonical_release));

    return revision
      ? r ||
      (x.epoch == y.epoch && x.canonical_upstream == y.canonical_upstream &&
       x.canonical_release == y.canonical_release && x.revision < y.revision)
      : r;
  }

  template <typename T1, typename T2>
  inline auto
  compare_version_le (const T1& x, const T2& y, bool revision)
    -> decltype (x.revision == y.revision)
  {
    auto r (
      x.epoch < y.epoch ||
      (x.epoch == y.epoch && x.canonical_upstream < y.canonical_upstream));

    return revision
      ? r ||
      (x.epoch == y.epoch && x.canonical_upstream == y.canonical_upstream &&
       x.canonical_release < y.canonical_release) ||
      (x.epoch == y.epoch && x.canonical_upstream == y.canonical_upstream &&
       x.canonical_release == y.canonical_release && x.revision <= y.revision)
      : r ||
      (x.epoch == y.epoch && x.canonical_upstream == y.canonical_upstream &&
       x.canonical_release <= y.canonical_release);
  }

  template <typename T1, typename T2>
  inline auto
  compare_version_gt (const T1& x, const T2& y, bool revision)
    -> decltype (x.revision == y.revision)
  {
    auto r (
      x.epoch > y.epoch ||
      (x.epoch == y.epoch && x.canonical_upstream > y.canonical_upstream) ||
      (x.epoch == y.epoch && x.canonical_upstream == y.canonical_upstream &&
       x.canonical_release > y.canonical_release));

    return revision
      ? r ||
      (x.epoch == y.epoch && x.canonical_upstream == y.canonical_upstream &&
       x.canonical_release == y.canonical_release && x.revision > y.revision)
      : r;
  }

  template <typename T1, typename T2>
  inline auto
  compare_version_ge (const T1& x, const T2& y, bool revision)
    -> decltype (x.revision == y.revision)
  {
    auto r (
      x.epoch > y.epoch ||
      (x.epoch == y.epoch && x.canonical_upstream > y.canonical_upstream));

    return revision
      ? r ||
      (x.epoch == y.epoch && x.canonical_upstream == y.canonical_upstream &&
       x.canonical_release > y.canonical_release) ||
      (x.epoch == y.epoch && x.canonical_upstream == y.canonical_upstream &&
       x.canonical_release == y.canonical_release && x.revision >= y.revision)
      : r ||
      (x.epoch == y.epoch && x.canonical_upstream == y.canonical_upstream &&
       x.canonical_release >= y.canonical_release);
  }

  template <typename T>
  inline auto
  order_by_version_desc (
    const T& x,
    bool first = true) -> //decltype ("ORDER BY" + x.epoch)
                          decltype (x.epoch == 0)
  {
    return (first ? "ORDER BY" : ", ")
      + x.epoch + "DESC,"
      + x.canonical_upstream + "DESC,"
      + x.canonical_release + "DESC,"
      + x.revision + "DESC";
  }

  template <typename T>
  inline auto
  order_by_version (
    const T& x,
    bool first = true) -> //decltype ("ORDER BY" + x.epoch)
                          decltype (x.epoch == 0)
  {
    return (first ? "ORDER BY" : ", ")
      + x.epoch + ","
      + x.canonical_upstream + ","
      + x.canonical_release + ","
      + x.revision;
  }

  // Package id comparison operators.
  //
  inline bool
  operator< (const package_id& x, const package_id& y)
  {
    if (int r = x.tenant.compare (y.tenant))
      return r < 0;

    if (int r = x.name.compare (y.name))
      return r < 0;

    return compare_version_lt (x.version, y.version, true);
  }

  // They allow comparing objects that have tenant, name, and version data
  // members. The idea is that this works for both query members of package id
  // types (in particular in join conditions) as well as for values of
  // package_id type.
  //
  template <typename T1, typename T2>
  inline auto
  operator== (const T1& x, const T2& y)
    -> decltype (x.tenant == y.tenant &&
                 x.name == y.name     &&
                 x.version.epoch == y.version.epoch)
  {
    return x.tenant == y.tenant &&
           x.name == y.name     &&
           compare_version_eq (x.version, y.version, true);
  }

  template <typename T1, typename T2>
  inline auto
  operator!= (const T1& x, const T2& y)
    -> decltype (x.tenant == y.tenant &&
                 x.name == y.name     &&
                 x.version.epoch == y.version.epoch)
  {
    return x.tenant != y.tenant ||
           x.name != y.name     ||
           compare_version_ne (x.version, y.version, true);
  }

  // Allow comparing the query members with the query parameters bound by
  // reference to variables of the package id type (in particular in the
  // prepared queries).
  //
  // Note that it is not operator==() since the query template parameter type
  // can not be deduced from the function parameter types and needs to be
  // specified explicitly.
  //
  template <typename T, typename ID>
  inline auto
  equal (const ID& x, const package_id& y)
    -> decltype (x.tenant == odb::query<T>::_ref (y.tenant) &&
                 x.name == odb::query<T>::_ref (y.name)     &&
                 x.version.epoch == odb::query<T>::_ref (y.version.epoch))
  {
    using query = odb::query<T>;

    const auto& qv (x.version);
    const canonical_version& v (y.version);

    return x.tenant == query::_ref (y.tenant)                          &&
           x.name == query::_ref (y.name)                              &&
           qv.epoch == query::_ref (v.epoch)                           &&
           qv.canonical_upstream == query::_ref (v.canonical_upstream) &&
           qv.canonical_release == query::_ref (v.canonical_release)   &&
           qv.revision == query::_ref (v.revision);
  }

  // Repository id comparison operators.
  //
  inline bool
  operator< (const repository_id& x, const repository_id& y)
  {
    if (int r = x.tenant.compare (y.tenant))
      return r < 0;

    return x.canonical_name.compare (y.canonical_name) < 0;
  }

  // They allow comparing objects that have tenant and canonical_name data
  // members. The idea is that this works for both query members of repository
  // id types (in particular in join conditions) as well as for values of
  // repository_id type.
  //
  template <typename T1, typename T2>
  inline auto
  operator== (const T1& x, const T2& y)
    -> decltype (x.tenant == y.tenant && x.canonical_name == y.canonical_name)
  {
    return x.tenant == y.tenant && x.canonical_name == y.canonical_name;
  }

  template <typename T1, typename T2>
  inline auto
  operator!= (const T1& x, const T2& y)
    -> decltype (x.tenant == y.tenant && x.canonical_name == y.canonical_name)
  {
    return x.tenant != y.tenant || x.canonical_name != y.canonical_name;
  }
}

#endif // LIBBREP_COMMON_HXX