/*
  uuid.h

  Copyright (c) 2021 Kevin Matz (kevin.matz@gmail.com)

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
*/

#include "uuid.h"
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <sstream>
#include <iomanip>

namespace UUID {

/**
 * @brief uuid::uuid
 */
uuid::uuid()
{
  type_    = NIL;
  version_ = VOID;
  timestamp_ = 0;
  clock_seq_ = 0;
  node_      = 0;
  setBytes_();
}


/**
 * @brief uuid::uuid
 * @param other
 */
uuid::uuid(const uuid& other)
{
  type_      = other.type_;
  version_   = other.version_;
  timestamp_ = other.timestamp_;
  clock_seq_ = other.clock_seq_;
  node_      = other.node_;
  setBytes_();
}


/**
 * @brief uuid::uuid
 * @param raw
 */
uuid::uuid(const uint8_t * raw)
  : uuid()
{
  fromArray_(raw);
}


/**
 * @brief uuid::uuid
 * @param c_str
 */
uuid::uuid(const char * c_str)
  : uuid()
{
  fromCstring_(c_str);
}


/**
 * @brief uuid::uuid
 * @param str
 */
uuid::uuid(std::string str)
  : uuid()
{
  fromString_(str);
}


/**
 * @brief uuid::bytes
 * @return
 */
const uint8_t * uuid::bytes() const
{
  return raw_;
};


/**
 * @brief uuid::type
 * @return
 */
Type uuid::type() const
{
  return type_;
};


/**
 * @brief uuid::version
 * @return
 */
uint16_t uuid::version() const
{
  return version_;
};


/**
 * @brief uuid::time
 * @return
 */
uint64_t uuid::time() const
{
  return timestamp_;
};


/**
 * @brief uuid::sequence
 * @return
 */
uint16_t uuid::sequence() const
{
  return clock_seq_;
};


/**
 * @brief uuid::node
 * @return
 */
uint64_t uuid::node() const
{
  return node_;
};


/**
 * @brief uuid::fromArray
 * @param bytes
 *
 * create this UUID from a byte array
 */
void uuid::fromArray_(const uint8_t * bytes)
{
  std::memcpy(raw_, bytes, UUID_LENGTH);
  setFields_();
}


/**
 * @brief uuid::fromCstring
 * @param cstr
 *
 * create this UUID from a C style char array
 */
void uuid::fromCstring_(const char * cstr)
{
  fromString_(std::string(cstr));
}


/**
 * @brief uuid::fromString
 * @param str
 *
 * create this UUID from a string representation
 */
void uuid::fromString_(std::string str)
{
  // remove formating
  if (str.front() == '{' && str.back() == '}')
    str = str.substr(1, str.size() - 2);
  str.erase(std::remove(str.begin(), str.end(), '-'), str.end());
  if (str.length() != 32) return;
  // fill buffer
  uint8_t buf[UUID_LENGTH];
  for (unsigned int i = 0; i < sizeof(buf); i++)
    buf[i] = std::strtoul(str.substr((i*2), 2).c_str(), NULL, UUID_LENGTH);
  // process buffer
  fromArray_(buf);
};


/**
 * @brief uuid::hex
 * @return
 *
 * output this UUID as a hex string
 */
std::string uuid::hex() const
{
  std::ostringstream oss;
  oss << std::hex;
  for (int i = 0; i < UUID_LENGTH; i++)
    oss << std::setw(2) << std::setfill('0') << (int)raw_[i];
  return oss.str();
}


/**
 * @brief uuid::string
 * @return
 *
 * output this UUID as formated hex string
 */
std::string uuid::string() const
{
  std::string str = hex();
  str.reserve(str.length() + 6);
  for (int idx : {8, 13, 18, 23})
    str.insert(idx, "-");
//  str.insert(0, "{");
//  str.insert(str.length(), "}");
  return str;
};


/**
 * @brief uuid::urn
 * @return
 *
 * output this UUID as a urn specified in RFC 4122
 */
std::string uuid::urn() const
{
  return "urn:uuid:" + string();
}


/**
 * @brief uuid::setBytes
 * make the raw bytes match the field contents
 */
void uuid::setBytes_()
{
  switch (type_) {
    case NIL:
      std::memset(raw_, 0, UUID_LENGTH);
      break;
    case NCS:
      setNCSBytes_();
      break;
    case RFC4122:
      setRFC4122Bytes_();
      break;
    case MS:
      break;
    case RESVERED:
      break;
    }
}


/**
 * @brief uuid::setNCSBytes
 */
void uuid::setNCSBytes_()
{
  NCSFields fields;
  fields.time   = timestamp_;
  fields.res    = 0;
  fields.family = version_;
  fields.node   = node_;

  std::memcpy(raw_, &fields, UUID_LENGTH);
}


/**
 * @brief uuid::setRFC4122Bytes
 * TODO: Check endianness
 */
void uuid::setRFC4122Bytes_()
{
  RFC4122Fields fields;
  fields.time_low = timestamp_;
  fields.time_mid = timestamp_ >> 32;
  fields.time_hi_version = timestamp_ >> 48;

  fields.clock_seq_low = clock_seq_;
  fields.clock_seq_hi_variant = clock_seq_ >> 8;

  fields.node_low = node_;
  fields.node_high = node_ >> 16;

  std::memcpy(raw_, &fields, UUID_LENGTH);

  // version
  raw_[6] = (raw_[6] & 0x0f) | (version_ << 4);
  // type
  raw_[8] = (raw_[8] & 0b00111111) | (0b10 << 6);
}


/**
 * @brief uuid::setFields
 */
void uuid::setFields_()
{
  setType_();
  switch (type_) {
    case NCS:
      setNCSFields_();
      break;
    case RFC4122:
      setRFC4122Fields_();
      break;
    default:
      break;
    }
}


/**
 * @brief uuid::setNCSFields
 */
void uuid::setNCSFields_()
{
  NCSFields fields;
  std::memcpy(&fields, raw_, UUID_LENGTH);

  timestamp_ = fields.time;
  version_   = fields.family;
  node_      = fields.node;
}


/**
 * @brief uuid::setRFC4122Fields
 */
void uuid::setRFC4122Fields_()
{
  RFC4122Fields fields;
  std::memcpy(&fields, raw_, UUID_LENGTH);

  timestamp_  = fields.time_low;
  timestamp_ |= (uint64_t)fields.time_mid << 32;
  timestamp_ |= (uint64_t)(fields.time_hi_version & 0xff0f) << 48;

  switch (raw_[6] >> 4) {
  case TIME:
      version_ = TIME;
      break;
  case DCE:
      version_ = DCE;
      break;
  case MD5:
      version_ = MD5;
      break;
  case RAND:
      version_ = RAND;
      break;
  case SHA1:
      version_ = SHA1;
      break;
  default:
      version_ = VOID;
  }

  clock_seq_  = fields.clock_seq_low;
  clock_seq_ |= (uint16_t)(fields.clock_seq_hi_variant & 0b00111111) << 8;

  node_  = fields.node_low;
  node_ |= (uint64_t)fields.node_high << 16;
}


/**
 * @brief uuid::setType
 *
 *  exctract the type from the raw bytes
 */
void uuid::setType_()
{
  if ((raw_[8] >> 7) == 0b0)
    type_ = NCS;
  else if ((raw_[8] >> 6) == 0b10)
    type_ = RFC4122;
  else if ((raw_[8] >> 5) == 0b110)
    type_ = MS;
  else
    type_ = RESVERED;
}


/**
 * @brief uuid::operator =
 * @param other
 * @return
 */
uuid& uuid::operator=(const uuid& other)
{
  if (this != &other) // protect against invalid self-assignment
    {
      std::memcpy(raw_, other.raw_, UUID_LENGTH);
      setFields_();
    }
  return *this;
}


/**
 * @brief operator ==
 * @param a
 * @param b
 * @return
 */
bool operator== (const uuid& a, const uuid& b)
{
  if (std::memcmp(a.raw_, b.raw_, UUID_LENGTH) == 0)
    return true;
  return false;
}


/**
 * @brief uuid::operator const uint8_t *
 */
uuid::operator const uint8_t *   () const
{
  return raw_;
};


/**
 * @brief uuid::operator const std::string
 */
uuid::operator const std::string () const
{
  return string();
};


/**
 * @brief uuid::setBytes
 * @param raw
 */
void uuid::setBytes(const uint8_t * raw)
{
  fromArray_(raw);
}


/**
 * @brief uuid::uuid1
 * @param node
 * @param clock_seq
 * @return incrimented clock_sequence
 *
 * node ID and clock sequence tracking are the responsibility of the caller
 */
uint16_t uuid::uuid1(uint64_t node, uint16_t clock_seq)
{
  type_ = RFC4122;
  version_ = TIME;
  timestamp_ = now_();
  clock_seq_ = clock_seq + 1;
  node_ = node;
  setBytes_();

  return clock_seq_;
}


/**
 * @brief  Version 4 (RANDOM) UUID
 *
 * a very low quality random
 */
void uuid::uuid4() {
  type_ = RFC4122;
  version_ = RAND;
  std::srand(now_());
  timestamp_ = std::rand();
  timestamp_ |= (uint64_t)std::rand() << 32;
  clock_seq_ = std::rand();
  node_ = std::rand();
  node_ |= (uint64_t)std::rand() << 32;
  setBytes_();
}


/**
 * @brief uuid::time_ 4.1.4. Timestamp
 * @return
 *
 *  The timestamp is a 60-bit value. For UUID version 1, this is represented
 *  by Coordinated Universal Time (UTC) as a count of 100-nanosecond intervals
 *  since 00:00:00.00, 15 October 1582 (the date of Gregorian reform to the
 *  Christian calendar).
 */
uint64_t uuid::now_()
{
  auto now = std::chrono::high_resolution_clock::now();
  std::chrono::duration<uint64_t, std::nano> since = now.time_since_epoch();
  uint64_t periods = since.count() / 100;
  periods += UUID_TICKS_BETWEEK_EPOCH;
  return periods;
}

}; // UUID
