#include <cpp4r.hpp>
#include <cstdio>
#include <cstring>
#include <string>

using namespace cpp4r;

namespace {

const uint64_t HASH_PRIME1 = 0x100000001b3ULL;      // FNV-1a 64-bit prime
const uint64_t HASH_PRIME2 = 0xc2b2ae3d27d4eb4fULL; // independent odd prime

inline void hash_feed(uint64_t &h1, uint64_t &h2, const void *data,
                      size_t len) {
  const unsigned char *p = static_cast<const unsigned char *>(data);
  for (size_t i = 0; i < len; ++i) {
    h1 = (h1 ^ p[i]) * HASH_PRIME1;
    h2 = (h2 ^ p[i]) * HASH_PRIME2;
  }
}

inline void hash_feed_int(uint64_t &h1, uint64_t &h2, int x) {
  hash_feed(h1, h2, &x, sizeof(int));
}

// Always feed as 8 bytes so hashes are stable across 32/64-bit platforms.
inline void hash_feed_len(uint64_t &h1, uint64_t &h2, R_xlen_t x) {
  int64_t len = static_cast<int64_t>(x);
  hash_feed(h1, h2, &len, sizeof(len));
}

// Match identical() semantics: all NA variants -> NA_REAL, all NaN variants
// -> R_NaN, -0.0 -> +0.0. Mirrors what rlang::hash() does.
inline double hash_normalise_dbl(double x) {
  if (R_IsNA(x))
    return NA_REAL;
  if (R_IsNaN(x))
    return R_NaN;
  if (x == 0.0)
    return 0.0;
  return x;
}

// Feed the type tag, then user-visible data, then attributes - recursively.
// Only atomic vectors, lists, and their attributes are supported, which
// covers data frames, factors, and nested lists thereof.
void hash_object(uint64_t &h1, uint64_t &h2, SEXP x) {
  int type = TYPEOF(x);
  hash_feed_int(h1, h2, type);

  switch (type) {
  case NILSXP:
    break;

  case LGLSXP: {
    R_xlen_t n = XLENGTH(x);
    hash_feed_len(h1, h2, n);
    hash_feed(h1, h2, LOGICAL(x), (size_t)n * sizeof(int));
    break;
  }

  case INTSXP: {
    R_xlen_t n = XLENGTH(x);
    hash_feed_len(h1, h2, n);
    hash_feed(h1, h2, INTEGER(x), (size_t)n * sizeof(int));
    break;
  }

  case REALSXP: {
    R_xlen_t n = XLENGTH(x);
    hash_feed_len(h1, h2, n);
    const double *p = REAL(x);
    for (R_xlen_t i = 0; i < n; ++i) {
      double v = hash_normalise_dbl(p[i]);
      hash_feed(h1, h2, &v, sizeof(double));
    }
    break;
  }

  case RAWSXP: {
    R_xlen_t n = XLENGTH(x);
    hash_feed_len(h1, h2, n);
    hash_feed(h1, h2, RAW(x), (size_t)n);
    break;
  }

  case STRSXP: {
    R_xlen_t n = XLENGTH(x);
    hash_feed_len(h1, h2, n);
    for (R_xlen_t i = 0; i < n; ++i) {
      SEXP elt = STRING_ELT(x, i);
      if (elt == NA_STRING) {
        hash_feed_int(h1, h2, 1);
      } else {
        hash_feed_int(h1, h2, 0);
        int len = LENGTH(elt);
        hash_feed_int(h1, h2, len);
        hash_feed(h1, h2, CHAR(elt), (size_t)len);
      }
    }
    break;
  }

  case VECSXP: {
    R_xlen_t n = XLENGTH(x);
    hash_feed_len(h1, h2, n);
    for (R_xlen_t i = 0; i < n; ++i) {
      hash_object(h1, h2, VECTOR_ELT(x, i));
    }
    break;
  }

  case SYMSXP: {
    const char *name = CHAR(PRINTNAME(x));
    int len = (int)std::strlen(name);
    hash_feed_int(h1, h2, len);
    hash_feed(h1, h2, name, (size_t)len);
    break;
  }

  default:
    Rf_error("Can't hash objects of type '%s'. hash() only supports atomic "
             "vectors, lists, and their attributes (which covers data frames, "
             "factors, and nested lists thereof).",
             Rf_type2char((SEXPTYPE)type));
  }

  // Hash a fixed, deterministic set of attribute names using only the
  // fully public Rf_getAttrib() API. R_mapAttrib() makes things simpler
  // but it is only available in R 4.6.0+
  static const SEXP known_attrs[] = {R_NamesSymbol,    R_ClassSymbol,
                                      R_RowNamesSymbol, R_LevelsSymbol,
                                      R_DimSymbol,      R_DimNamesSymbol};
  for (SEXP sym : known_attrs) {
    SEXP value = Rf_getAttrib(x, sym);
    if (value != R_NilValue) {
      hash_object(h1, h2, sym);
      hash_object(h1, h2, value);
    }
  }
}

} // namespace

/* roxygen
@title Hash Raw Bytes (C++)
@description Computes a fast, well-mixed 128-bit (32 hex character) hash of
  a raw vector, e.g. the output of \code{serialize()}. For hashing an R object
  directly, without serializing it first, see \code{hash()}.
@param x A raw vector, e.g. the output of \code{serialize()}.
@return A 32-character lowercase hex string.
@export
*/
[[cpp4r::register]]
std::string hash_raw(raws x) {
  uint64_t h1 = 0xcbf29ce484222325ULL; // FNV-1a 64-bit offset basis
  uint64_t h2 = 0x9e3779b97f4a7c15ULL; // 2nd lane seed (golden ratio)

  for (uint8_t byte : x) {
    h1 = (h1 ^ byte) * HASH_PRIME1;
    h2 = (h2 ^ byte) * HASH_PRIME2;
  }

  char buf[33];
  std::snprintf(buf, sizeof(buf), "%016llx%016llx", (unsigned long long)h1,
                (unsigned long long)h2);
  return std::string(buf, 32);
}

/* roxygen
@title Hash an R Object
@description Computes a fast, well-mixed 128-bit (32 hex character) hash of
  an R object, similar to \code{rlang::hash()}. Unlike hashing
  \code{serialize(x, connection = NULL)} (see \code{hash_raw()}), this walks the
  object's type, length, data bytes, and attributes directly, so it never
  allocates a serialized byte buffer. Supports atomic vectors (logical,
  integer, double, character, raw), lists, and their attributes meaning that it
  covers data frames, factors, and nested lists thereof. Other types (e.g.
  closures, environments, S4 objects) are not supported.
@param x An R object to hash.
@return A 32-character lowercase hex string.
@export
*/
[[cpp4r::register]]
SEXP hash(SEXP x) {
  uint64_t h1 = 0xcbf29ce484222325ULL; // FNV-1a 64-bit offset basis
  uint64_t h2 = 0x9e3779b97f4a7c15ULL; // 2nd lane seed (golden ratio)
  hash_object(h1, h2, x);

  char buf[33];
  std::snprintf(buf, sizeof(buf), "%016llx%016llx", (unsigned long long)h1,
                (unsigned long long)h2);
  return Rf_mkString(buf);
}
