// Copyright 2010-2020, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "win32/base/imm_registrar.h"

#include <WinNls32.h>
#include <atlbase.h>
#include <strsafe.h>
#include <windows.h>

#include <iomanip>
#include <map>
#include <memory>
#include <sstream>
#include <string>

#include "base/const.h"
#include "base/file_util.h"
#include "base/logging.h"
#include "base/system_util.h"
#include "base/util.h"
#include "base/win_util.h"
#include "win32/base/display_name_resource.h"
#include "win32/base/imm_util.h"
#include "win32/base/immdev.h"
#include "win32/base/keyboard_layout_id.h"

namespace mozc {
namespace win32 {

namespace {

using std::unique_ptr;

const wchar_t kRegKeyboardLayouts[] =
    L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts";
const wchar_t kLayoutDisplayNameKey[] = L"Layout Display Name";
const wchar_t kLayoutDisplayNamePattern[] = L"@%s,-%d";
const wchar_t kPreloadKeyName[] = L"Keyboard Layout\\Preload";
const wchar_t kPreloadTopValueName[] = L"1";

typedef std::map<unsigned int, DWORD> PreloadValueMap;

// Converts an unsigned integer to a wide string.
std::wstring utow(unsigned int i) {
  std::wstringstream ss;
  ss << i;
  return ss.str();
}

std::wstring GetSystemRegKeyName(const KeyboardLayoutID &klid) {
  return std::wstring(kRegKeyboardLayouts) + L"\\" + klid.ToString();
}

// Set the layout display name with the Registry String Redirection format
// to the specified keyboard layout.
// See the following documents for details.
// http://blogs.msdn.com/michkap/archive/2006/05/06/591174.aspx
// http://blogs.msdn.com/michkap/archive/2007/01/05/1387397.aspx
// http://blogs.msdn.com/michkap/archive/2007/08/25/4564548.aspx
// http://msdn.microsoft.com/en-us/library/dd374120.aspx
HRESULT SetLayoutDisplayName(
    const KeyboardLayoutID &klid,
    const std::wstring &layout_display_name_resource_path,
    int layout_display_name_resource_id) {
  if (!klid.has_id()) {
    return E_FAIL;
  }

  const std::wstring &key_name = GetSystemRegKeyName(klid);

  CRegKey keybord_layout_key;
  LRESULT result = keybord_layout_key.Open(HKEY_LOCAL_MACHINE, key_name.c_str(),
                                           KEY_READ | KEY_WRITE);
  if (result != ERROR_SUCCESS) {
    LOG(ERROR) << "Failed to open the registry key"
               << " result = " << result;
    return HRESULT_FROM_WIN32(result);
  }

  wchar_t layout_name[MAX_PATH];
  HRESULT hr = StringCchPrintf(layout_name, arraysize(layout_name),
                               kLayoutDisplayNamePattern,
                               layout_display_name_resource_path.c_str(),
                               layout_display_name_resource_id);
  if (FAILED(hr)) {
    LOG(ERROR) << "StringCchPrintf failed"
               << " hr = " << hr;
    return hr;
  }

  result = keybord_layout_key.SetStringValue(&kLayoutDisplayNameKey[0],
                                             layout_name, REG_SZ);
  if (result != ERROR_SUCCESS) {
    LOG(ERROR) << "Failed to set a registry value"
               << " result = " << result;
    return HRESULT_FROM_WIN32(result);
  }

  return S_OK;
}

// ImmInstallIME has a bug in 64-bit Windows. It can't recoganize SysWOW64
// folder as a system folder, so it will refuse to install our IME. The
// solution here is to combine the 64-bit System32 folder and our filename
// to make ImmInstallIME happy.
std::wstring GetFullPathForSystem(const string &basename) {
  string system_dir;
  if (Util::WideToUTF8(SystemUtil::GetSystemDir(), &system_dir) <= 0) {
    return L"";
  }

  const string fullpath = FileUtil::JoinPath(system_dir, basename);

  std::wstring wfullpath;
  if (Util::UTF8ToWide(fullpath, &wfullpath) <= 0) {
    return L"";
  }

  return wfullpath;
}

// Retrieves values under the preload key and stores the result to |keys|.
// Returns ERROR_SUCCESS if the operation completes successfully.
LONG RetrievePreloadValues(HKEY preload_key, PreloadValueMap *keys) {
  if (nullptr == keys) {
    return ERROR_INVALID_PARAMETER;
  }

  // Registry element size limits are described in the link below.
  // http://msdn.microsoft.com/en-us/library/ms724872(VS.85).aspx
  const DWORD kMaxValueNameLength = 16383;
  wchar_t value_name[kMaxValueNameLength];
  const DWORD kMaxValueLength = 256;
  BYTE value[kMaxValueLength];
  for (DWORD i = 0;; ++i) {
    DWORD value_name_length = kMaxValueNameLength;
    DWORD value_length = kMaxValueLength;
    LONG result = RegEnumValue(preload_key, i, value_name, &value_name_length,
                               nullptr,  // reserved (must be nullptr)
                               nullptr,  // type (optional)
                               value, &value_length);

    if (ERROR_NO_MORE_ITEMS == result) {
      break;
    } else if (ERROR_SUCCESS != result) {
      return result;
    }

    const int ivalue_name = _wtoi(value_name);
    const std::wstring wvalue(reinterpret_cast<wchar_t *>(value),
                              (value_length / sizeof(wchar_t)) - 1);
    KeyboardLayoutID klid(wvalue);
    if (!klid.has_id()) {
      continue;
    }
    (*keys)[ivalue_name] = klid.id();
  }
  return ERROR_SUCCESS;
}

// Returns the index of |klid| in |preload_values|.
// This function works well on 64-bit Windows.
unsigned int GetPreloadIndex(const KeyboardLayoutID &klid,
                             const PreloadValueMap &preload_values) {
  unsigned int index = 0;
  for (PreloadValueMap::const_iterator i = preload_values.begin();
       i != preload_values.end(); ++i) {
    if ((*i).second == klid.id()) {
      index = (*i).first;
    }
  }
  return index;
}

std::wstring ToWideString(const string &str) {
  std::wstring wide;
  if (Util::UTF8ToWide(str, &wide) <= 0) {
    return L"";
  }
  return wide;
}

bool RemoveHotKey(HKL hkl) {
  if (hkl == nullptr) {
    return false;
  }

  bool succeeded = true;
  for (DWORD id = IME_HOTKEY_DSWITCH_FIRST; id <= IME_HOTKEY_DSWITCH_LAST;
       ++id) {
    UINT modifiers = 0;
    UINT virtual_key = 0;
    HKL assigned_hkl = nullptr;
    BOOL result = ::ImmGetHotKey(id, &modifiers, &virtual_key, &assigned_hkl);
    if (result == FALSE) {
      continue;
    }
    if (assigned_hkl != hkl) {
      continue;
    }
    // ImmSetHotKey fails when both 2nd and 3rd arguments are valid while 4th
    // argument is nullptr.  To remove the HotKey, pass 0 to them.
    result = ::ImmSetHotKey(id, 0, 0, nullptr);
    if (result == FALSE) {
      succeeded = false;
    }
  }
  return succeeded;
}
}  // namespace

HRESULT ImmRegistrar::Register(
    const std::wstring &ime_filename, const std::wstring &layout_name,
    const std::wstring &layout_display_name_resource_path,
    int layout_display_name_resource_id, HKL *hkl) {
  HKL dummy_hkl = nullptr;
  if (hkl == nullptr) {
    hkl = &dummy_hkl;
  }

  // If the IME is already registered, return directly. If we install 32-bit
  // and 64-bit IME side-by-side in a 64-bit Windows, the ImmInstallIME
  // function should be called only once, either for the 32-bit or 64-bit
  // version of the IME DLL.
  {
    const KeyboardLayoutID klid = GetKLIDFromFileName(ime_filename);
    if (klid.has_id()) {
      // already registered.
      *hkl = ::LoadKeyboardLayoutW(klid.ToString().c_str(), KLF_ACTIVATE);
      return S_OK;
    }
  }  // |klid| is no longer needed.

  IMEPROW dummy_ime_property = {0};

  const std::wstring &fullpath(std::wstring(SystemUtil::GetSystemDir()) +
                               L"\\" + ime_filename);

  // The path name of IME has hard limit. (http://b/2072809)
  if (fullpath.size() + 1 > arraysize(dummy_ime_property.szName)) {
    // Path name is too long. It will be truncated.
    return E_FAIL;
  }

  // The description of IME has hard limit. (http://b/2072809)
  if (layout_name.size() + 1 > arraysize(dummy_ime_property.szDescription)) {
    // Description is too long. It will be truncated.
    return E_FAIL;
  }

  // On 64-bit Windows, it would better to use native (64-bit) version of
  // ImmInstallIME instead of WOW (32-bit) version.  See b/2931871 for details.
  const HKL installed_hkl =
      ::ImmInstallIME(fullpath.c_str(), layout_name.c_str());

  if (!installed_hkl) {
    return E_FAIL;
  }

  // Remove HotKey (if any), which is likely to be an unregistered HotKey
  // used for the previous IME.
  if (!RemoveHotKey(installed_hkl)) {
    DLOG(ERROR) << "RemoveUnregisteredHotKey failed.";
    // Removing the hotkey is an optional, nice-to-have task so its failure
    // is not critical.  Go to the next step.
  }

  *hkl = installed_hkl;

  const KeyboardLayoutID installed_klid = GetKLIDFromFileName(ime_filename);
  if (!installed_klid.has_id()) {
    // ImmInstallIME returned a HKL but KLID is not found.  Something wrong.
    return E_FAIL;
  }

  // SetLayoutDisplayName is not mandatory so that we do nothing if it fails.
  if (FAILED(SetLayoutDisplayName(installed_klid,
                                  layout_display_name_resource_path,
                                  layout_display_name_resource_id))) {
    DLOG(ERROR) << "SetLayoutDisplayName failed.";
  }

  return S_OK;
}

// Uninstall module by deleting a registry key under kRegKeyboardLayouts.
HRESULT ImmRegistrar::Unregister(const std::wstring &ime_filename) {
  const KeyboardLayoutID &klid = GetKLIDFromFileName(ime_filename);
  if (!klid.has_id()) {
    // already unregistered?
    return S_OK;
  }

  // Ensure the target IME is unloaded.
  {
    const int num_keyboard_layout = ::GetKeyboardLayoutList(0, nullptr);
    unique_ptr<HKL[]> keyboard_layouts(new HKL[num_keyboard_layout]);
    const size_t num_copied =
        ::GetKeyboardLayoutList(num_keyboard_layout, keyboard_layouts.get());
    for (size_t i = 0; i < num_copied; ++i) {
      const HKL hkl = keyboard_layouts[i];
      if (!ImmRegistrar::IsIME(hkl, ime_filename)) {
        continue;
      }
      ::UnloadKeyboardLayout(hkl);
      break;
    }
  }

  // Remove IME registry key.
  {
    CRegKey keyboard_layouts;
    LONG result = keyboard_layouts.Open(HKEY_LOCAL_MACHINE, kRegKeyboardLayouts,
                                        KEY_READ | KEY_WRITE);
    if (ERROR_SUCCESS != result) {
      return HRESULT_FROM_WIN32(result);
    }
    result = keyboard_layouts.RecurseDeleteKey(klid.ToString().c_str());
    if (ERROR_SUCCESS != result) {
      return HRESULT_FROM_WIN32(result);
    }
  }

  return S_OK;
}

bool ImmRegistrar::IsIME(HKL hkl, const std::wstring &ime_filename) {
  if (hkl == nullptr) {
    return false;
  }

  wchar_t buf[MAX_PATH];
  if (!::ImmGetIMEFileNameW(hkl, buf, MAX_PATH)) {
    LOG(WARNING) << "Failed to get ime file name";
    return false;
  }

  // TODO(yukawa): Support short filename.  See b/2977730
  return WinUtil::SystemEqualString(buf, ime_filename, true);
}

std::wstring ImmRegistrar::GetFileNameForIME() {
  return ToWideString(mozc::kIMEFile);
}

KeyboardLayoutID ImmRegistrar::GetKLIDForIME() {
  return GetKLIDFromFileName(GetFileNameForIME());
}

KeyboardLayoutID ImmRegistrar::GetKLIDFromFileName(
    const std::wstring &ime_filename) {
  if (ime_filename.empty()) {
    return KeyboardLayoutID();
  }

  CRegKey keyboard_layouts;
  LONG result =
      keyboard_layouts.Open(HKEY_LOCAL_MACHINE, kRegKeyboardLayouts, KEY_READ);
  if (ERROR_SUCCESS != result) {
    return KeyboardLayoutID();
  }

  // Registry element size limits are described in the link below.
  // http://msdn.microsoft.com/en-us/library/ms724872(VS.85).aspx
  const DWORD kMaxValueNameLength = 16383;
  wchar_t value_name[kMaxValueNameLength];
  for (DWORD enum_reg_index = 0;; ++enum_reg_index) {
    DWORD value_name_length = kMaxValueNameLength;
    result = keyboard_layouts.EnumKey(enum_reg_index, value_name,
                                      &value_name_length);
    if (ERROR_NO_MORE_ITEMS == result) {
      break;
    } else if (ERROR_SUCCESS != result) {
      break;
    }

    // Note that |value_name_length| does not contain NUL character.
    const KeyboardLayoutID klid(
        std::wstring(value_name, value_name + value_name_length));

    if (!klid.has_id()) {
      continue;
    }

    CRegKey subkey;
    result = subkey.Open(keyboard_layouts, klid.ToString().c_str(), KEY_READ);
    if (ERROR_SUCCESS != result) {
      continue;
    }

    wchar_t filename_buffer[kMaxValueNameLength];
    ULONG filename_length_including_null = kMaxValueNameLength;
    result = subkey.QueryStringValue(L"Ime File", filename_buffer,
                                     &filename_length_including_null);

    // Note that |filename_length_including_null| contains NUL terminator.
    if (ERROR_SUCCESS != result || (filename_length_including_null == 0)) {
      continue;
    }

    const ULONG filename_length = (filename_length_including_null - 1);
    // Note that |filename_length| does not contain NUL character.
    const std::wstring target_basename(filename_buffer,
                                       filename_buffer + filename_length);

    // TODO(yukawa): Support short filename.  See b/2977730
    if (WinUtil::SystemEqualString(target_basename, ime_filename, true)) {
      return klid;
    }
  }
  return KeyboardLayoutID();
}

std::wstring ImmRegistrar::GetFullPathForIME() {
  return GetFullPathForSystem(mozc::kIMEFile);
}

std::wstring ImmRegistrar::GetLayoutName() {
  std::wstring layout_name;
  // We use English name here as culture-invariant layout name.
  if (Util::UTF8ToWide(kProductNameInEnglish, &layout_name) <= 0) {
    return L"";
  }
  return layout_name;
}

int ImmRegistrar::GetLayoutDisplayNameResourceId() {
  return IDS_IME_DISPLAYNAME;
}

// Removes a value equal to hkl from HKCU\Keyboard Layout\\Preload and decrement
// value names which are more than hkl.
HRESULT ImmRegistrar::RemoveKeyFromPreload(
    const KeyboardLayoutID &klid, const KeyboardLayoutID &defaultKLID) {
  // Retrieve keys under kPreloadKeyName.
  CRegKey preload_key;
  LONG result = preload_key.Open(HKEY_CURRENT_USER, kPreloadKeyName);
  if (ERROR_SUCCESS != result) {
    return E_FAIL;
  }

  PreloadValueMap preload_values;
  result = RetrievePreloadValues(preload_key, &preload_values);
  if (ERROR_SUCCESS != result) {
    return E_FAIL;
  }

  const unsigned int preload_index = GetPreloadIndex(klid, preload_values);
  if (0 == preload_index) {
    // Not found.  Already removed?
    return S_OK;
  }

  // Write the default HKL if the deleted value was the last one.
  if (preload_values.size() == 1) {
    _ASSERT((*preload_values.begin()).first == preload_index);
    result = preload_key.SetStringValue(kPreloadTopValueName,
                                        defaultKLID.ToString().c_str());
    if (ERROR_SUCCESS != result) {
      return E_FAIL;
    }
  } else {
    // Remove values whose names are less than |preload_index|.
    PreloadValueMap::iterator target_iter = preload_values.find(preload_index);
    preload_values.erase(preload_values.begin(), target_iter);
    for (PreloadValueMap::iterator i = preload_values.begin();
         i != preload_values.end(); ++i) {
      const std::wstring &value_name = utow((*i).first);
      const KeyboardLayoutID target_klid((*i).second);
      result = preload_key.DeleteValue(value_name.c_str());
      if ((*i).first == preload_index) {
        continue;
      }
      const std::wstring &new_value_name = utow((*i).first - 1);
      preload_key.SetStringValue(new_value_name.c_str(),
                                 target_klid.ToString().c_str());
    }
  }
  return S_OK;
}

HRESULT ImmRegistrar::RestorePreload(const KeyboardLayoutID &klid) {
  if (!klid.has_id()) {
    return E_FAIL;
  }

  CRegKey preload_key;
  LONG result = preload_key.Open(HKEY_CURRENT_USER, kPreloadKeyName);
  if (ERROR_SUCCESS != result) {
    return E_FAIL;
  }

  PreloadValueMap preload_values;
  result = RetrievePreloadValues(preload_key, &preload_values);
  if (ERROR_SUCCESS != result ||
      preload_values.find(1) == preload_values.end()) {
    // The value corresponding to |hkl| or the value on the top does not exist.
    return E_FAIL;
  }

  const unsigned int preload_index = GetPreloadIndex(klid, preload_values);
  if (0 != preload_index) {
    // |klid| already exists in the preload list.
    // nothing to do.
    return S_OK;
  }

  if (preload_values.size() == 0) {
    result = preload_key.SetStringValue(kPreloadTopValueName,
                                        klid.ToString().c_str());
    if (ERROR_SUCCESS != result) {
      return E_FAIL;
    }
    return S_OK;
  }

  const unsigned int new_preload_index = (preload_values.rbegin()->first + 1);
  result = preload_key.SetStringValue(utow(new_preload_index).c_str(),
                                      klid.ToString().c_str());
  if (ERROR_SUCCESS != result) {
    return E_FAIL;
  }
  return S_OK;
}

// NOTE: There are several ways to reorder the values other than |klid| after
// |klid| is moved to the top.
// The current implementation just swaps the order of |klid| for the top.
HRESULT ImmRegistrar::MovePreloadValueToTop(const KeyboardLayoutID &klid) {
  CRegKey preload_key;
  LONG result = preload_key.Open(HKEY_CURRENT_USER, kPreloadKeyName);
  if (ERROR_SUCCESS != result) {
    return E_FAIL;
  }

  PreloadValueMap preload_values;
  result = RetrievePreloadValues(preload_key, &preload_values);
  if (ERROR_SUCCESS != result ||
      preload_values.find(1) == preload_values.end()) {
    // The value corresponding to |klid| or the value on the top does not exist.
    return E_FAIL;
  }

  const unsigned int preload_index = GetPreloadIndex(klid, preload_values);
  if (0 == preload_index) {
    if (preload_values.size() == 0) {
      // It is not necessary to move values since there is no preload key.
      result = preload_key.SetStringValue(kPreloadTopValueName,
                                          klid.ToString().c_str());
      if (ERROR_SUCCESS != result) {
        return E_FAIL;
      }
      return S_OK;
    }
    // Duplicate the first entry into the end of the list.
    unsigned int new_preload_index = (preload_values.rbegin()->first + 1);
    result = preload_key.SetStringValue(
        utow(new_preload_index).c_str(),
        KeyboardLayoutID(preload_values[1]).ToString().c_str());
    if (ERROR_SUCCESS != result) {
      return E_FAIL;
    }
    // Overwrite the first entry with the target keyboard layout ID.
    result = preload_key.SetStringValue(kPreloadTopValueName,
                                        klid.ToString().c_str());
    if (ERROR_SUCCESS != result) {
      // Attempt rollback when the second call fails.
      preload_key.DeleteValue(utow(new_preload_index).c_str());
      return E_FAIL;
    }
    return S_OK;
  }

  // It is not necessary to move values since the target hkl is already listed
  // on the top.
  if (1 == preload_index) {
    return S_OK;
  }
  result = preload_key.SetStringValue(
      kPreloadTopValueName,
      KeyboardLayoutID(preload_values[preload_index]).ToString().c_str());
  if (ERROR_SUCCESS != result) {
    return E_FAIL;
  }
  const KeyboardLayoutID first_klid(preload_values[1]);
  result = preload_key.SetStringValue(utow(preload_index).c_str(),
                                      first_klid.ToString().c_str());
  if (ERROR_SUCCESS != result) {
    // Attempt rollback when the second call fails.
    preload_key.SetStringValue(kPreloadTopValueName,
                               first_klid.ToString().c_str());
    return E_FAIL;
  }

  return S_OK;
}

}  // namespace win32
}  // namespace mozc
