/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "WinRegistry.h"
#include "nsThreadUtils.h"

namespace mozilla::widget::WinRegistry {

Key::Key(HKEY aParent, const nsString& aPath, KeyMode aMode, CreateFlag) {
  MOZ_ASSERT(aParent);
  DWORD disposition;
  ::RegCreateKeyExW(aParent, aPath.get(), 0, nullptr, REG_OPTION_NON_VOLATILE,
                    (REGSAM)aMode, nullptr, &mKey, &disposition);
}

Key::Key(HKEY aParent, const nsString& aPath, KeyMode aMode) {
  MOZ_ASSERT(aParent);
  ::RegOpenKeyExW(aParent, aPath.get(), 0, (REGSAM)aMode, &mKey);
}

uint32_t Key::GetChildCount() const {
  MOZ_ASSERT(mKey);
  DWORD result = 0;
  ::RegQueryInfoKeyW(mKey, nullptr, nullptr, nullptr, &result, nullptr, nullptr,
                     nullptr, nullptr, nullptr, nullptr, nullptr);
  return result;
}

bool Key::GetChildName(uint32_t aIndex, nsAString& aResult) const {
  MOZ_ASSERT(mKey);
  FILETIME lastWritten;

  wchar_t nameBuf[kMaxKeyNameLen + 1];
  DWORD nameLen = std::size(nameBuf);

  LSTATUS rv = RegEnumKeyExW(mKey, aIndex, nameBuf, &nameLen, nullptr, nullptr,
                             nullptr, &lastWritten);
  if (LSTATUS_FAILED(rv)) {
    return false;
  }
  aResult.Assign(nameBuf, nameLen);
  return true;
}

bool Key::RemoveChildKey(const nsString& aName) const {
  MOZ_ASSERT(mKey);
  return LSTATUS_SUCCEEDED(RegDeleteKeyW(mKey, aName.get()));
}

uint32_t Key::GetValueCount() const {
  MOZ_ASSERT(mKey);
  DWORD result = 0;
  ::RegQueryInfoKeyW(mKey, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
                     &result, nullptr, nullptr, nullptr, nullptr);
  return result;
}

bool Key::GetValueName(uint32_t aIndex, nsAString& aResult) const {
  MOZ_ASSERT(mKey);
  wchar_t nameBuf[kMaxValueNameLen + 1];
  DWORD nameLen = std::size(nameBuf);

  LSTATUS rv = RegEnumValueW(mKey, aIndex, nameBuf, &nameLen, nullptr, nullptr,
                             nullptr, nullptr);
  if (LSTATUS_FAILED(rv)) {
    return false;
  }
  aResult.Assign(nameBuf, nameLen);
  return true;
}

ValueType Key::GetValueType(const nsString& aName) const {
  MOZ_ASSERT(mKey);
  DWORD result;
  LSTATUS rv =
      RegQueryValueExW(mKey, aName.get(), nullptr, &result, nullptr, nullptr);
  return LSTATUS_SUCCEEDED(rv) ? ValueType(result) : ValueType::None;
}

bool Key::RemoveValue(const nsString& aName) const {
  MOZ_ASSERT(mKey);
  // NOTE: Intentionally not propagating errors from RegDeleteValueW().
  RegDeleteValueW(mKey, aName.get());
  return true;
}

Maybe<uint32_t> Key::GetValueAsDword(const nsString& aName) const {
  MOZ_ASSERT(mKey);
  DWORD type;
  DWORD value = 0;
  DWORD size = sizeof(DWORD);
  LSTATUS rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type,
                                (LPBYTE)&value, &size);
  if (LSTATUS_FAILED(rv) || type != REG_DWORD) {
    return Nothing();
  }
  return Some(value);
}

bool Key::WriteValueAsDword(const nsString& aName, uint32_t aValue) {
  MOZ_ASSERT(mKey);
  return LSTATUS_SUCCEEDED(RegSetValueExW(
      mKey, aName.get(), 0, REG_DWORD, (const BYTE*)&aValue, sizeof(aValue)));
}

Maybe<uint64_t> Key::GetValueAsQword(const nsString& aName) const {
  MOZ_ASSERT(mKey);
  DWORD type;
  uint64_t value = 0;
  DWORD size = sizeof(uint64_t);
  LSTATUS rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type,
                                (LPBYTE)&value, &size);
  if (LSTATUS_FAILED(rv) || type != REG_QWORD) {
    return Nothing();
  }
  return Some(value);
}

bool Key::WriteValueAsQword(const nsString& aName, uint64_t aValue) {
  MOZ_ASSERT(mKey);
  return LSTATUS_SUCCEEDED(RegSetValueExW(
      mKey, aName.get(), 0, REG_QWORD, (const BYTE*)&aValue, sizeof(aValue)));
}

bool Key::GetValueAsBinary(const nsString& aName,
                           nsTArray<uint8_t>& aResult) const {
  MOZ_ASSERT(mKey);
  DWORD type;
  DWORD size;
  LSTATUS rv =
      RegQueryValueExW(mKey, aName.get(), nullptr, &type, nullptr, &size);
  if (LSTATUS_FAILED(rv) || type != REG_BINARY) {
    return false;
  }
  if (!aResult.SetLength(size, fallible)) {
    return false;
  }
  rv = RegQueryValueExW(mKey, aName.get(), nullptr, nullptr, aResult.Elements(),
                        &size);
  return LSTATUS_SUCCEEDED(rv);
}

Maybe<nsTArray<uint8_t>> Key::GetValueAsBinary(const nsString& aName) const {
  nsTArray<uint8_t> value;
  Maybe<nsTArray<uint8_t>> result;
  if (GetValueAsBinary(aName, value)) {
    result.emplace(std::move(value));
  }
  return result;
}

bool Key::WriteValueAsBinary(const nsString& aName,
                             Span<const uint8_t> aValue) {
  MOZ_ASSERT(mKey);
  return LSTATUS_SUCCEEDED(RegSetValueExW(mKey, aName.get(), 0, REG_BINARY,
                                          (const BYTE*)aValue.data(),
                                          aValue.size()));
}

static bool IsStringType(DWORD aType, StringFlags aFlags) {
  switch (aType) {
    case REG_SZ:
      return bool(aFlags & StringFlags::Sz);
    case REG_EXPAND_SZ:
      return bool(aFlags & StringFlags::ExpandSz);
    case REG_MULTI_SZ:
      return bool(aFlags & StringFlags::LegacyMultiSz);
    default:
      return false;
  }
}

bool Key::GetValueAsString(const nsString& aName, nsString& aResult,
                           StringFlags aFlags) const {
  MOZ_ASSERT(mKey);
  DWORD type;
  DWORD size;
  LSTATUS rv =
      RegQueryValueExW(mKey, aName.get(), nullptr, &type, nullptr, &size);
  if (LSTATUS_FAILED(rv) || !IsStringType(type, aFlags)) {
    return false;
  }
  if (!size) {
    aResult.Truncate();
    return true;
  }
  // The buffer size must be a multiple of 2.
  if (NS_WARN_IF(size % 2 != 0)) {
    return false;
  }
  size_t resultLen = size / 2;
  {
    auto handleOrError =
        aResult.BulkWrite(resultLen, 0, /* aAllowShrinking = */ false);
    if (NS_WARN_IF(handleOrError.isErr())) {
      return false;
    }
    auto handle = handleOrError.unwrap();
    auto len = GetValueAsString(aName, {handle.Elements(), handle.Length() + 1},
                                aFlags & ~StringFlags::ExpandEnvironment);
    if (NS_WARN_IF(!len)) {
      return false;
    }
    handle.Finish(*len, /* aAllowShrinking = */ false);
    if (*len && !aResult.CharAt(*len - 1)) {
      // The string passed to us had a null terminator in the final
      // position.
      aResult.Truncate(*len - 1);
    }
  }
  if (type == REG_EXPAND_SZ && (aFlags & StringFlags::ExpandEnvironment)) {
    resultLen = ExpandEnvironmentStringsW(aResult.get(), nullptr, 0);
    if (resultLen > 1) {
      nsString expandedResult;
      // |resultLen| includes the terminating null character
      resultLen--;
      if (!expandedResult.SetLength(resultLen, fallible)) {
        return false;
      }
      resultLen = ExpandEnvironmentStringsW(aResult.get(), expandedResult.get(),
                                            resultLen + 1);
      if (resultLen <= 0) {
        return false;
      }
      aResult = std::move(expandedResult);
    } else if (resultLen == 1) {
      // It apparently expands to nothing (just a null terminator).
      resultLen = 0;
      aResult.Truncate();
    }
  }
  return true;
}

Maybe<nsString> Key::GetValueAsString(const nsString& aName,
                                      StringFlags aFlags) const {
  nsString value;
  Maybe<nsString> result;
  if (GetValueAsString(aName, value, aFlags)) {
    result.emplace(std::move(value));
  }
  return result;
}

Maybe<uint32_t> Key::GetValueAsString(const nsString& aName,
                                      Span<char16_t> aBuffer,
                                      StringFlags aFlags) const {
  MOZ_ASSERT(mKey);
  MOZ_ASSERT(aBuffer.Length(), "Empty buffer?");
  MOZ_ASSERT(!(aFlags & StringFlags::ExpandEnvironment),
             "Environment expansion not performed on a single buffer");

  DWORD size = aBuffer.LengthBytes();
  DWORD type;
  LSTATUS rv = RegQueryValueExW(mKey, aName.get(), nullptr, &type,
                                (LPBYTE)aBuffer.data(), &size);
  if (LSTATUS_FAILED(rv)) {
    return Nothing();
  }
  if (!IsStringType(type, aFlags)) {
    return Nothing();
  }
  uint32_t len = size ? size / sizeof(char16_t) - 1 : 0;
  aBuffer[len] = 0;
  return Some(len);
}

KeyWatcher::KeyWatcher(Key&& aKey,
                       nsISerialEventTarget* aTargetSerialEventTarget,
                       Callback&& aCallback)
    : mKey(std::move(aKey)),
      mEventTarget(aTargetSerialEventTarget),
      mCallback(std::move(aCallback)) {
  MOZ_ASSERT(mKey);
  MOZ_ASSERT(mEventTarget);
  MOZ_ASSERT(mCallback);
  mEvent = CreateEvent(nullptr, /* bManualReset = */ FALSE,
                       /* bInitialState = */ FALSE, nullptr);
  if (NS_WARN_IF(!mEvent)) {
    return;
  }

  if (NS_WARN_IF(!Register())) {
    return;
  }

  // The callback only dispatches to the relevant event target, so we can use
  // WT_EXECUTEINWAITTHREAD.
  RegisterWaitForSingleObject(&mWaitObject, mEvent, WatchCallback, this,
                              INFINITE, WT_EXECUTEINWAITTHREAD);
}

void KeyWatcher::WatchCallback(void* aContext, BOOLEAN) {
  auto* watcher = static_cast<KeyWatcher*>(aContext);
  watcher->Register();
  watcher->mEventTarget->Dispatch(
      NS_NewRunnableFunction("KeyWatcher callback", watcher->mCallback));
}

// As per the documentation in:
// https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regnotifychangekeyvalue
//
// This function detects a single change. After the caller receives a
// notification event, it should call the function again to receive the next
// notification.
bool KeyWatcher::Register() {
  MOZ_ASSERT(mEvent);
  DWORD flags = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_ATTRIBUTES |
                REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_CHANGE_SECURITY |
                REG_NOTIFY_THREAD_AGNOSTIC;
  LSTATUS rv =
      RegNotifyChangeKeyValue(mKey.RawKey(), /* bWatchSubtree = */ TRUE, flags,
                              mEvent, /* fAsynchronous = */ TRUE);
  return !NS_WARN_IF(LSTATUS_FAILED(rv));
}

KeyWatcher::~KeyWatcher() {
  if (mWaitObject) {
    UnregisterWaitEx(mWaitObject, INVALID_HANDLE_VALUE);
  }
  if (mEvent) {
    CloseHandle(mEvent);
  }
}

}  // namespace mozilla::widget::WinRegistry
