/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */
#include "cmUuid.h"

#include <array>
#include <cstring>

#include "cmCryptoHash.h"

static std::array<int, 5> const kUuidGroups = { { 4, 2, 2, 2, 6 } };

std::string cmUuid::FromMd5(std::vector<unsigned char> const& uuidNamespace,
                            cm::string_view name) const
{
  std::vector<unsigned char> hashInput;
  this->CreateHashInput(uuidNamespace, name, hashInput);

  cmCryptoHash md5(cmCryptoHash::AlgoMD5);
  md5.Initialize();
  md5.Append(hashInput.data(), hashInput.size());
  std::vector<unsigned char> digest = md5.Finalize();

  return this->FromDigest(digest.data(), 3);
}

std::string cmUuid::FromSha1(std::vector<unsigned char> const& uuidNamespace,
                             cm::string_view name) const
{
  std::vector<unsigned char> hashInput;
  this->CreateHashInput(uuidNamespace, name, hashInput);

  cmCryptoHash sha1(cmCryptoHash::AlgoSHA1);
  sha1.Initialize();
  sha1.Append(hashInput.data(), hashInput.size());
  std::vector<unsigned char> digest = sha1.Finalize();

  return this->FromDigest(digest.data(), 5);
}

void cmUuid::CreateHashInput(std::vector<unsigned char> const& uuidNamespace,
                             cm::string_view name,
                             std::vector<unsigned char>& output) const
{
  output = uuidNamespace;

  if (!name.empty()) {
    output.resize(output.size() + name.size());

    memcpy(output.data() + uuidNamespace.size(), name.data(), name.size());
  }
}

std::string cmUuid::FromDigest(unsigned char const* digest,
                               unsigned char version) const
{
  using byte_t = unsigned char;

  byte_t uuid[16] = { 0 };
  memcpy(uuid, digest, 16);

  uuid[6] &= 0xF;
  uuid[6] |= static_cast<byte_t>(version << 4);

  uuid[8] &= 0x3F;
  uuid[8] |= 0x80;

  return this->BinaryToString(uuid);
}

bool cmUuid::StringToBinary(cm::string_view input,
                            std::vector<unsigned char>& output) const
{
  output.clear();
  output.reserve(16);

  if (input.length() != 36) {
    return false;
  }
  size_t index = 0;
  for (size_t i = 0; i < kUuidGroups.size(); ++i) {
    if (i != 0 && input[index++] != '-') {
      return false;
    }
    size_t digits = kUuidGroups[i] * 2;
    if (!this->StringToBinaryImpl(input.substr(index, digits), output)) {
      return false;
    }

    index += digits;
  }

  return true;
}

std::string cmUuid::BinaryToString(unsigned char const* input) const
{
  std::string output;

  size_t inputIndex = 0;
  for (size_t i = 0; i < kUuidGroups.size(); ++i) {
    if (i != 0) {
      output += '-';
    }

    size_t bytes = kUuidGroups[i];
    for (size_t j = 0; j < bytes; ++j) {
      unsigned char inputByte = input[inputIndex++];
      output += this->ByteToHex(inputByte);
    }
  }

  return output;
}

std::string cmUuid::ByteToHex(unsigned char inputByte) const
{
  std::string result("  ");
  for (int i = 0; i < 2; ++i) {
    unsigned char rest = inputByte % 16;
    inputByte /= 16;
    char c = (rest < 0xA) ? static_cast<char>('0' + rest)
                          : static_cast<char>('a' + (rest - 0xA));
    result.at(1 - i) = c;
  }

  return result;
}

bool cmUuid::StringToBinaryImpl(cm::string_view input,
                                std::vector<unsigned char>& output) const
{
  if (input.size() % 2) {
    return false;
  }

  for (size_t i = 0; i < input.size(); i += 2) {
    char c1 = 0;
    if (!this->IntFromHexDigit(input[i], c1)) {
      return false;
    }

    char c2 = 0;
    if (!this->IntFromHexDigit(input[i + 1], c2)) {
      return false;
    }

    output.push_back(static_cast<char>(c1 << 4 | c2));
  }

  return true;
}

bool cmUuid::IntFromHexDigit(char input, char& output) const
{
  if (input >= '0' && input <= '9') {
    output = static_cast<char>(input - '0');
    return true;
  }
  if (input >= 'a' && input <= 'f') {
    output = static_cast<char>(input - 'a' + 0xA);
    return true;
  }
  if (input >= 'A' && input <= 'F') {
    output = static_cast<char>(input - 'A' + 0xA);
    return true;
  }
  return false;
}
