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

#include "cmWindowsRegistry.h"

#include <cctype>
#include <cstddef>
#include <type_traits>
#include <unordered_map>
#include <utility>

#include <cmext/string_view>

#include "cmsys/RegularExpression.hxx"

#if defined(_WIN32) && !defined(__CYGWIN__)
#  include <algorithm>
#  include <cstring>
#  include <exception>
#  include <iterator>
#  include <vector>

#  include <cm/memory>

#  include <windows.h>

#  include "cmMakefile.h"
#  include "cmStringAlgorithms.h"
#  include "cmValue.h"
#endif

namespace {
//  Case-independent string comparison
int Strucmp(cm::string_view l, cm::string_view r)
{
  if (l.empty() && r.empty()) {
    return 0;
  }
  if (l.empty() || r.empty()) {
    return static_cast<int>(l.size() - r.size());
  }

  int lc;
  int rc;
  cm::string_view::size_type li = 0;
  cm::string_view::size_type ri = 0;

  do {
    lc = std::tolower(l[li++]);
    rc = std::tolower(r[ri++]);
  } while (lc == rc && li < l.size() && ri < r.size());

  return lc == rc ? static_cast<int>(l.size() - r.size()) : lc - rc;
}

#if defined(_WIN32) && !defined(__CYGWIN__)
bool Is64BitWindows()
{
#  if defined(_WIN64)
  // 64-bit programs run only on Win64
  return true;
#  else
  // 32-bit programs run on both 32-bit and 64-bit Windows, so we must check.
  BOOL isWow64 = false;
  return IsWow64Process(GetCurrentProcess(), &isWow64) && isWow64;
#  endif
}

// Helper to translate Windows registry value type to enum ValueType
cm::optional<cmWindowsRegistry::ValueType> ToValueType(DWORD type)
{
  using ValueType = cmWindowsRegistry::ValueType;

  static std::unordered_map<DWORD, ValueType> ValueTypes{
    { REG_SZ, ValueType::Reg_SZ },
    { REG_EXPAND_SZ, ValueType::Reg_EXPAND_SZ },
    { REG_MULTI_SZ, ValueType::Reg_MULTI_SZ },
    { REG_DWORD, ValueType::Reg_DWORD },
    { REG_QWORD, ValueType::Reg_QWORD }
  };

  auto it = ValueTypes.find(type);

  return it == ValueTypes.end()
    ? cm::nullopt
    : cm::optional<cmWindowsRegistry::ValueType>{ it->second };
}

// class registry_exception
class registry_error : public std::exception
{
public:
  registry_error(std::string msg)
    : What(std::move(msg))
  {
  }
  ~registry_error() override = default;

  char const* what() const noexcept override { return What.c_str(); }

private:
  std::string What;
};

// Class KeyHandler
class KeyHandler
{
public:
  using View = cmWindowsRegistry::View;
  using ValueTypeSet = cmWindowsRegistry::ValueTypeSet;

  KeyHandler(HKEY hkey)
    : Handler(hkey)
  {
  }
  ~KeyHandler() { RegCloseKey(this->Handler); }

  static KeyHandler OpenKey(cm::string_view rootKey, cm::string_view subKey,
                            View view);
  static KeyHandler OpenKey(cm::string_view key, View view);

  std::string ReadValue(
    cm::string_view name,
    ValueTypeSet supportedTypes = cmWindowsRegistry::AllTypes,
    cm::string_view separator = "\0"_s);

  std::vector<std::string> GetValueNames();
  std::vector<std::string> GetSubKeys();

private:
  static std::string FormatSystemError(LSTATUS status);
  static std::wstring ToWide(cm::string_view str);
  static std::string ToNarrow(wchar_t const* str, int size = -1);

  HKEY Handler;
};

KeyHandler KeyHandler::OpenKey(cm::string_view rootKey, cm::string_view subKey,
                               View view)
{
  if (view == View::Reg64 && !Is64BitWindows()) {
    throw registry_error("No 64bit registry on Windows32.");
  }

  HKEY hRootKey;
  if (rootKey == "HKCU"_s || rootKey == "HKEY_CURRENT_USER"_s) {
    hRootKey = HKEY_CURRENT_USER;
  } else if (rootKey == "HKLM"_s || rootKey == "HKEY_LOCAL_MACHINE"_s) {
    hRootKey = HKEY_LOCAL_MACHINE;
  } else if (rootKey == "HKCR"_s || rootKey == "HKEY_CLASSES_ROOT"_s) {
    hRootKey = HKEY_CLASSES_ROOT;
  } else if (rootKey == "HKCC"_s || rootKey == "HKEY_CURRENT_CONFIG"_s) {
    hRootKey = HKEY_CURRENT_CONFIG;
  } else if (rootKey == "HKU"_s || rootKey == "HKEY_USERS"_s) {
    hRootKey = HKEY_USERS;
  } else {
    throw registry_error(cmStrCat(rootKey, ": invalid root key."));
  }
  // Update path format
  auto key = ToWide(subKey);
  std::replace(key.begin(), key.end(), L'/', L'\\');

  REGSAM options = KEY_READ;
  if (Is64BitWindows()) {
    options |= view == View::Reg64 ? KEY_WOW64_64KEY : KEY_WOW64_32KEY;
  }

  HKEY hKey;
  LSTATUS status;
  if ((status = RegOpenKeyExW(hRootKey, key.c_str(), 0, options, &hKey)) !=
      ERROR_SUCCESS) {
    throw registry_error(FormatSystemError(status));
  }

  return KeyHandler(hKey);
}

KeyHandler KeyHandler::OpenKey(cm::string_view key, View view)
{
  auto start = key.find_first_of("\\/"_s);

  return OpenKey(key.substr(0, start),
                 start == cm::string_view::npos ? cm::string_view{ ""_s }
                                                : key.substr(start + 1),
                 view);
}

std::string KeyHandler::FormatSystemError(LSTATUS status)
{
  std::string formattedMessage{ "Windows Registry: unexpected error." };
  LPWSTR message = nullptr;
  DWORD size = 1024;
  if (FormatMessageW(
        FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, nullptr,
        status, 0, reinterpret_cast<LPWSTR>(&message), size, nullptr) != 0) {
    try {
      formattedMessage = cmTrimWhitespace(ToNarrow(message));
    } catch (...) {
      // ignore any exception because this method can be called
      // as part of the raise of an exception
    }
  }
  LocalFree(message);

  return formattedMessage;
}

std::wstring KeyHandler::ToWide(cm::string_view str)
{
  std::wstring wstr;

  if (str.empty()) {
    return wstr;
  }

  auto const wlength =
    MultiByteToWideChar(KWSYS_ENCODING_DEFAULT_CODEPAGE, 0, str.data(),
                        int(str.size()), nullptr, 0);
  if (wlength > 0) {
    auto wdata = cm::make_unique<wchar_t[]>(wlength);
    auto const r =
      MultiByteToWideChar(KWSYS_ENCODING_DEFAULT_CODEPAGE, 0, str.data(),
                          int(str.size()), wdata.get(), wlength);
    if (r > 0) {
      wstr = std::wstring(wdata.get(), wlength);
    } else {
      throw registry_error(FormatSystemError(GetLastError()));
    }
  } else {
    throw registry_error(FormatSystemError(GetLastError()));
  }

  return wstr;
}

std::string KeyHandler::ToNarrow(wchar_t const* wstr, int size)
{
  std::string str;

  if (size == 0 || (size == -1 && wstr[0] == L'\0')) {
    return str;
  }

  auto const length =
    WideCharToMultiByte(KWSYS_ENCODING_DEFAULT_CODEPAGE, 0, wstr, size,
                        nullptr, 0, nullptr, nullptr);
  if (length > 0) {
    auto data = cm::make_unique<char[]>(length);
    auto const r =
      WideCharToMultiByte(KWSYS_ENCODING_DEFAULT_CODEPAGE, 0, wstr, size,
                          data.get(), length, nullptr, nullptr);
    if (r > 0) {
      if (size == -1) {
        str = std::string(data.get());
      } else {
        str = std::string(data.get(), length);
      }
    } else {
      throw registry_error(FormatSystemError(GetLastError()));
    }
  } else {
    throw registry_error(FormatSystemError(GetLastError()));
  }

  return str;
}

std::string KeyHandler::ReadValue(cm::string_view name,
                                  ValueTypeSet supportedTypes,
                                  cm::string_view separator)
{
  LSTATUS status;
  DWORD size;
  // pick-up maximum size for value
  if ((status = RegQueryInfoKeyW(this->Handler, nullptr, nullptr, nullptr,
                                 nullptr, nullptr, nullptr, nullptr, nullptr,
                                 &size, nullptr, nullptr)) != ERROR_SUCCESS) {
    throw registry_error(this->FormatSystemError(status));
  }
  auto data = cm::make_unique<BYTE[]>(size);
  DWORD type;
  auto valueName = this->ToWide(name);
  if ((status = RegQueryValueExW(this->Handler, valueName.c_str(), nullptr,
                                 &type, data.get(), &size)) != ERROR_SUCCESS) {
    throw registry_error(this->FormatSystemError(status));
  }

  auto valueType = ToValueType(type);
  if (!valueType || !supportedTypes.contains(*valueType)) {
    throw registry_error(cmStrCat(type, ": unsupported type."));
  }

  switch (type) {
    case REG_SZ:
      return this->ToNarrow(reinterpret_cast<wchar_t*>(data.get()));
      break;
    case REG_EXPAND_SZ: {
      auto expandSize = ExpandEnvironmentStringsW(
        reinterpret_cast<wchar_t*>(data.get()), nullptr, 0);
      auto expandData = cm::make_unique<wchar_t[]>(expandSize + 1);
      if (ExpandEnvironmentStringsW(reinterpret_cast<wchar_t*>(data.get()),
                                    expandData.get(), expandSize + 1) == 0) {
        throw registry_error(this->FormatSystemError(GetLastError()));
      } else {
        return this->ToNarrow(expandData.get());
      }
    } break;
    case REG_DWORD:
      return std::to_string(*reinterpret_cast<std::uint32_t*>(data.get()));
      break;
    case REG_QWORD:
      return std::to_string(*reinterpret_cast<std::uint64_t*>(data.get()));
      break;
    case REG_MULTI_SZ: {
      // replace separator with semicolon
      auto sep = this->ToWide(separator)[0];
      std::replace(reinterpret_cast<wchar_t*>(data.get()),
                   reinterpret_cast<wchar_t*>(data.get()) +
                     (size / sizeof(wchar_t)) - 1,
                   sep, L';');
      return this->ToNarrow(reinterpret_cast<wchar_t*>(data.get()));
    } break;
    default:
      throw registry_error(cmStrCat(type, ": unsupported type."));
  }
}

std::vector<std::string> KeyHandler::GetValueNames()
{
  LSTATUS status;
  DWORD maxSize;
  // pick-up maximum size for value names
  if ((status = RegQueryInfoKeyW(
         this->Handler, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
         nullptr, &maxSize, nullptr, nullptr, nullptr)) != ERROR_SUCCESS) {
    throw registry_error(this->FormatSystemError(status));
  }
  // increment size for final null
  auto data = cm::make_unique<wchar_t[]>(++maxSize);
  DWORD index = 0;
  DWORD size = maxSize;

  std::vector<std::string> valueNames;

  while ((status = RegEnumValueW(this->Handler, index, data.get(), &size,
                                 nullptr, nullptr, nullptr, nullptr)) ==
         ERROR_SUCCESS) {
    auto name = this->ToNarrow(data.get());
    valueNames.push_back(name.empty() ? "(default)" : name);
    size = maxSize;
    ++index;
  }

  if (status != ERROR_NO_MORE_ITEMS) {
    throw registry_error(this->FormatSystemError(status));
  }

  return valueNames;
}

std::vector<std::string> KeyHandler::GetSubKeys()
{
  LSTATUS status;
  DWORD size;
  // pick-up maximum size for subkeys
  if ((status = RegQueryInfoKeyW(
         this->Handler, nullptr, nullptr, nullptr, nullptr, &size, nullptr,
         nullptr, nullptr, nullptr, nullptr, nullptr)) != ERROR_SUCCESS) {
    throw registry_error(this->FormatSystemError(status));
  }
  // increment size for final null
  auto data = cm::make_unique<wchar_t[]>(++size);
  DWORD index = 0;
  std::vector<std::string> subKeys;

  while ((status = RegEnumKeyW(this->Handler, index, data.get(), size)) ==
         ERROR_SUCCESS) {
    subKeys.push_back(this->ToNarrow(data.get()));
    ++index;
  }
  if (status != ERROR_NO_MORE_ITEMS) {
    throw registry_error(this->FormatSystemError(status));
  }

  return subKeys;
}
#endif

// ExpressionParser: Helper to parse expression holding multiple
// registry specifications
class ExpressionParser
{
public:
  ExpressionParser(cm::string_view expression)
    : Expression(expression)
    , Separator(";"_s)
    , RegistryFormat{
      "\\[({.+})?(HKCU|HKEY_CURRENT_USER|HKLM|HKEY_LOCAL_MACHINE|HKCR|HKEY_"
      "CLASSES_"
      "ROOT|HKCC|HKEY_CURRENT_CONFIG|HKU|HKEY_USERS)[/\\]?([^]]*)\\]"
    }
  {
  }

  bool Find()
  {
    // reset result members
    this->RootKey = cm::string_view{};
    this->SubKey = cm::string_view{};
    this->ValueName = cm::string_view{};

    auto result = this->RegistryFormat.find(this->Expression);

    if (result) {
      auto separator = cm::string_view{
        this->Expression.data() + this->RegistryFormat.start(1),
        this->RegistryFormat.end(1) - this->RegistryFormat.start(1)
      };
      if (separator.empty()) {
        separator = this->Separator;
      } else {
        separator = separator.substr(1, separator.length() - 2);
      }

      this->RootKey = cm::string_view{
        this->Expression.data() + this->RegistryFormat.start(2),
        this->RegistryFormat.end(2) - this->RegistryFormat.start(2)
      };
      this->SubKey = cm::string_view{
        this->Expression.data() + this->RegistryFormat.start(3),
        this->RegistryFormat.end(3) - this->RegistryFormat.start(3)
      };

      auto pos = this->SubKey.find(separator);
      if (pos != cm::string_view::npos) {
        // split in ValueName and SubKey
        this->ValueName = this->SubKey.substr(pos + separator.size());
        if (Strucmp(this->ValueName, "(default)") == 0) {
          // handle magic name for default value
          this->ValueName = ""_s;
        }
        this->SubKey = this->SubKey.substr(0, pos);
      } else {
        this->ValueName = ""_s;
      }
    }
    return result;
  }

#if defined(_WIN32) && !defined(__CYGWIN__)
  void Replace(std::string const& value)
  {
    this->Expression.replace(
      this->RegistryFormat.start(),
      this->RegistryFormat.end() - this->RegistryFormat.start(), value);
  }

  cm::string_view GetRootKey() const { return this->RootKey; }

  cm::string_view GetSubKey() const { return this->SubKey; }
  cm::string_view GetValueName() const { return this->ValueName; }

  std::string const& GetExpression() const { return this->Expression; }
#endif

private:
  std::string Expression;
  cm::string_view Separator;
  cmsys::RegularExpression RegistryFormat;
  cm::string_view RootKey;
  cm::string_view SubKey;
  cm::string_view ValueName;
};
}

// class cmWindowsRegistry
cmWindowsRegistry::ValueTypeSet const cmWindowsRegistry::SimpleTypes =
  cmWindowsRegistry::ValueTypeSet{ cmWindowsRegistry::ValueType::Reg_SZ,
                                   cmWindowsRegistry::ValueType::Reg_EXPAND_SZ,
                                   cmWindowsRegistry::ValueType::Reg_DWORD,
                                   cmWindowsRegistry::ValueType::Reg_QWORD };
cmWindowsRegistry::ValueTypeSet const cmWindowsRegistry::AllTypes =
  cmWindowsRegistry::SimpleTypes + cmWindowsRegistry::ValueType::Reg_MULTI_SZ;

cmWindowsRegistry::cmWindowsRegistry(cmMakefile& makefile,
                                     ValueTypeSet const& supportedTypes)
#if defined(_WIN32) && !defined(__CYGWIN__)
  : SupportedTypes(supportedTypes)
#else
  : LastError("No Registry on this platform.")
#endif
{
#if defined(_WIN32) && !defined(__CYGWIN__)
  if (cmValue targetSize = makefile.GetDefinition("CMAKE_SIZEOF_VOID_P")) {
    this->TargetSize = targetSize == "8" ? 64 : 32;
  }
#else
  (void)makefile;
  (void)supportedTypes;
#endif
}

cm::optional<cmWindowsRegistry::View> cmWindowsRegistry::ToView(
  cm::string_view name)
{
  static std::unordered_map<cm::string_view, cmWindowsRegistry::View>
    ViewDefinitions{
      { "BOTH"_s, View::Both },     { "HOST"_s, View::Host },
      { "TARGET"_s, View::Target }, { "32"_s, View::Reg32 },
      { "64"_s, View::Reg64 },      { "32_64"_s, View::Reg32_64 },
      { "64_32"_s, View::Reg64_32 }
    };

  auto it = ViewDefinitions.find(name);

  return it == ViewDefinitions.end()
    ? cm::nullopt
    : cm::optional<cmWindowsRegistry::View>{ it->second };
}

// define hash structure required by std::unordered_map
namespace std {
template <>
struct hash<cmWindowsRegistry::View>
{
  size_t operator()(cmWindowsRegistry::View v) const noexcept
  {
    return static_cast<
      typename underlying_type<cmWindowsRegistry::View>::type>(v);
  }
};
}

cm::string_view cmWindowsRegistry::FromView(View view)
{
  static std::unordered_map<cmWindowsRegistry::View, cm::string_view>
    ViewDefinitions{
      { View::Both, "BOTH"_s },     { View::Host, "HOST"_s },
      { View::Target, "TARGET"_s }, { View::Reg32, "32"_s },
      { View::Reg64, "64"_s },      { View::Reg32_64, "32_64"_s },
      { View::Reg64_32, "64_32"_s }
    };

  auto it = ViewDefinitions.find(view);

  return it == ViewDefinitions.end() ? ""_s : it->second;
}

cm::string_view cmWindowsRegistry::GetLastError() const
{
  return this->LastError;
}

#if defined(_WIN32) && !defined(__CYGWIN__)
std::vector<cmWindowsRegistry::View> cmWindowsRegistry::ComputeViews(View view)
{
  switch (view) {
    case View::Both:
      switch (this->TargetSize) {
        case 64:
          return std::vector<View>{ View::Reg64, View::Reg32 };
          break;
        case 32:
          return Is64BitWindows()
            ? std::vector<View>{ View::Reg32, View::Reg64 }
            : std::vector<View>{ View::Reg32 };
          break;
        default:
          // No language specified, fallback to host architecture
          return Is64BitWindows()
            ? std::vector<View>{ View::Reg64, View::Reg32 }
            : std::vector<View>{ View::Reg32 };
          break;
      }
      break;
    case View::Target:
      switch (this->TargetSize) {
        case 64:
          return std::vector<View>{ View::Reg64 };
          break;
        case 32:
          return std::vector<View>{ View::Reg32 };
          break;
        default:
          break;
      }
      CM_FALLTHROUGH;
    case View::Host:
      return std::vector<View>{ Is64BitWindows() ? View::Reg64 : View::Reg32 };
      break;
    case View::Reg64_32:
      return Is64BitWindows() ? std::vector<View>{ View::Reg64, View::Reg32 }
                              : std::vector<View>{ View::Reg32 };
      break;
    case View::Reg32_64:
      return Is64BitWindows() ? std::vector<View>{ View::Reg32, View::Reg64 }
                              : std::vector<View>{ View::Reg32 };
      break;
    default:
      return std::vector<View>{ view };
      break;
  }
}
#endif

cm::optional<std::string> cmWindowsRegistry::ReadValue(
  cm::string_view key, cm::string_view name, View view,
  cm::string_view separator)
{
#if defined(_WIN32) && !defined(__CYGWIN__)
  // compute list of registry views
  auto views = this->ComputeViews(view);

  if (Strucmp(name, "(default)") == 0) {
    // handle magic name for default value
    name = ""_s;
  }
  if (separator.empty()) {
    separator = "\0"_s;
  }

  for (auto v : views) {
    try {
      this->LastError.clear();
      auto handler = KeyHandler::OpenKey(key, v);
      return handler.ReadValue(name, this->SupportedTypes, separator);
    } catch (registry_error const& e) {
      this->LastError = e.what();
      continue;
    }
  }
#else
  (void)key;
  (void)name;
  (void)view;
  (void)separator;
#endif
  return cm::nullopt;
}

cm::optional<std::vector<std::string>> cmWindowsRegistry::GetValueNames(
  cm::string_view key, View view)
{
#if defined(_WIN32) && !defined(__CYGWIN__)
  this->LastError.clear();
  // compute list of registry views
  auto views = this->ComputeViews(view);
  std::vector<std::string> valueNames;
  bool querySuccessful = false;

  for (auto v : views) {
    try {
      auto handler = KeyHandler::OpenKey(key, v);
      auto list = handler.GetValueNames();
      std::move(list.begin(), list.end(), std::back_inserter(valueNames));
      querySuccessful = true;
    } catch (registry_error const& e) {
      this->LastError = e.what();
      continue;
    }
  }
  if (!valueNames.empty()) {
    // value names must be unique and sorted
    std::sort(valueNames.begin(), valueNames.end());
    valueNames.erase(std::unique(valueNames.begin(), valueNames.end()),
                     valueNames.end());
  }

  if (querySuccessful) {
    // At least one query was successful, so clean-up any error message
    this->LastError.clear();
    return valueNames;
  }
#else
  (void)key;
  (void)view;
#endif
  return cm::nullopt;
}

cm::optional<std::vector<std::string>> cmWindowsRegistry::GetSubKeys(
  cm::string_view key, View view)
{
#if defined(_WIN32) && !defined(__CYGWIN__)
  this->LastError.clear();
  // compute list of registry views
  auto views = this->ComputeViews(view);
  std::vector<std::string> subKeys;
  bool querySuccessful = false;

  for (auto v : views) {
    try {
      auto handler = KeyHandler::OpenKey(key, v);
      auto list = handler.GetSubKeys();
      std::move(list.begin(), list.end(), std::back_inserter(subKeys));
      querySuccessful = true;
    } catch (registry_error const& e) {
      this->LastError = e.what();
      continue;
    }
  }
  if (!subKeys.empty()) {
    // keys must be unique and sorted
    std::sort(subKeys.begin(), subKeys.end());
    subKeys.erase(std::unique(subKeys.begin(), subKeys.end()), subKeys.end());
  }

  if (querySuccessful) {
    // At least one query was successful, so clean-up any error message
    this->LastError.clear();
    return subKeys;
  }
#else
  (void)key;
  (void)view;
#endif
  return cm::nullopt;
}

cm::optional<std::vector<std::string>> cmWindowsRegistry::ExpandExpression(
  cm::string_view expression, View view, cm::string_view separator)
{
#if defined(_WIN32) && !defined(__CYGWIN__)
  static std::string NOTFOUND{ "/REGISTRY-NOTFOUND" };

  this->LastError.clear();

  // compute list of registry views
  auto views = this->ComputeViews(view);
  std::vector<std::string> result;

  for (auto v : views) {
    ExpressionParser parser(expression);

    while (parser.Find()) {
      try {
        auto handler =
          KeyHandler::OpenKey(parser.GetRootKey(), parser.GetSubKey(), v);
        auto data = handler.ReadValue(parser.GetValueName(),
                                      this->SupportedTypes, separator);
        parser.Replace(data);
      } catch (registry_error const& e) {
        this->LastError = e.what();
        parser.Replace(NOTFOUND);
        continue;
      }
    }
    result.emplace_back(parser.GetExpression());
    if (expression == parser.GetExpression()) {
      // there no substitutions, so can ignore other views
      break;
    }
  }

  return result;
#else
  (void)view;
  (void)separator;

  ExpressionParser parser(expression);
  if (parser.Find()) {
    // expression holds unsupported registry access
    // so the expression cannot be used on this platform
    return cm::nullopt;
  }
  return std::vector<std::string>{ std::string{ expression } };
#endif
}
