// Copyright (c) 2022 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "tests/cefclient/browser/client_prefs.h"

#include <memory>

#include "include/base/cef_logging.h"
#include "include/cef_command_line.h"
#include "include/cef_preference.h"
#include "include/cef_values.h"
#include "include/views/cef_display.h"
#include "include/wrapper/cef_helpers.h"
#include "tests/shared/common/client_switches.h"
#include "tests/shared/common/string_util.h"

namespace client::prefs {

namespace {

constexpr char kPrefWindowRestore[] = "cefclient.window_restore";

constexpr char kWindowRestoreStateKey[] = "state";
constexpr char kWindowRestoreBoundsKey[] = "bounds";
constexpr char kWindowRestoreBoundsKey_X[] = "x";
constexpr char kWindowRestoreBoundsKey_Y[] = "y";
constexpr char kWindowRestoreBoundsKey_W[] = "w";
constexpr char kWindowRestoreBoundsKey_H[] = "h";

static struct {
  const char* str;
  cef_show_state_t state;
} const kWindowRestoreStateValueMap[] = {
    {"normal", CEF_SHOW_STATE_NORMAL},
    {"minimized", CEF_SHOW_STATE_MINIMIZED},
    {"maximized", CEF_SHOW_STATE_MAXIMIZED},
    {"fullscreen", CEF_SHOW_STATE_FULLSCREEN},
#if defined(OS_MAC)
    // Hidden show state is only supported on MacOS.
    {"hidden", CEF_SHOW_STATE_HIDDEN},
#endif
};

std::optional<cef_show_state_t> ShowStateFromString(const std::string& str) {
  const auto strLower = AsciiStrToLower(str);
  for (auto i : kWindowRestoreStateValueMap) {
    if (strLower == i.str) {
      return i.state;
    }
  }
  return std::nullopt;
}

const char* ShowStateToString(cef_show_state_t show_state) {
  for (auto i : kWindowRestoreStateValueMap) {
    if (show_state == i.state) {
      return i.str;
    }
  }
  NOTREACHED();
  return nullptr;
}

// Create the CefValue representation that will be stored in preferences.
CefRefPtr<CefValue> CreateWindowRestoreValue(
    cef_show_state_t show_state,
    std::optional<CefRect> dip_bounds) {
  auto dict = CefDictionaryValue::Create();

  // Show state is required.
  dict->SetString(kWindowRestoreStateKey, ShowStateToString(show_state));

  // Bounds is optional.
  if (dip_bounds) {
    auto bounds_dict = CefDictionaryValue::Create();
    bounds_dict->SetInt(kWindowRestoreBoundsKey_X, dip_bounds->x);
    bounds_dict->SetInt(kWindowRestoreBoundsKey_Y, dip_bounds->y);
    bounds_dict->SetInt(kWindowRestoreBoundsKey_W, dip_bounds->width);
    bounds_dict->SetInt(kWindowRestoreBoundsKey_H, dip_bounds->height);
    dict->SetDictionary(kWindowRestoreBoundsKey, bounds_dict);
  }

  auto value = CefValue::Create();
  value->SetDictionary(dict);
  return value;
}

CefRefPtr<CefValue> CreateDefaultWindowRestoreValue() {
  return CreateWindowRestoreValue(CEF_SHOW_STATE_NORMAL, std::nullopt);
}

// Parse the CefValue representation that was stored in preferences.
bool ParseWindowRestoreValue(CefRefPtr<CefValue> value,
                             cef_show_state_t& show_state,
                             std::optional<CefRect>& dip_bounds) {
  if (!value || value->GetType() != VTYPE_DICTIONARY) {
    return false;
  }

  auto dict = value->GetDictionary();

  bool has_state = false;

  // Show state is required.
  if (dict->GetType(kWindowRestoreStateKey) == VTYPE_STRING) {
    auto result = ShowStateFromString(dict->GetString(kWindowRestoreStateKey));
    if (result) {
      show_state = *result;
      has_state = true;
    }
  }

  // Bounds is optional.
  if (has_state && dict->GetType(kWindowRestoreBoundsKey) == VTYPE_DICTIONARY) {
    auto bounds_dict = dict->GetDictionary(kWindowRestoreBoundsKey);
    if (bounds_dict->GetType(kWindowRestoreBoundsKey_X) == VTYPE_INT &&
        bounds_dict->GetType(kWindowRestoreBoundsKey_Y) == VTYPE_INT &&
        bounds_dict->GetType(kWindowRestoreBoundsKey_W) == VTYPE_INT &&
        bounds_dict->GetType(kWindowRestoreBoundsKey_H) == VTYPE_INT) {
      dip_bounds = CefRect(bounds_dict->GetInt(kWindowRestoreBoundsKey_X),
                           bounds_dict->GetInt(kWindowRestoreBoundsKey_Y),
                           bounds_dict->GetInt(kWindowRestoreBoundsKey_W),
                           bounds_dict->GetInt(kWindowRestoreBoundsKey_H));
    }
  }

  return has_state;
}

// Keep the bounds inside the closest display work area.
CefRect ClampBoundsToDisplay(const CefRect& dip_bounds) {
  auto display = CefDisplay::GetDisplayMatchingBounds(
      dip_bounds, /*input_pixel_coords=*/false);
  const auto work_area = display->GetWorkArea();

  CefRect bounds = dip_bounds;

  if (bounds.width > work_area.width) {
    bounds.width = work_area.width;
  }
  if (bounds.height > work_area.height) {
    bounds.height = work_area.height;
  }

  if (bounds.x < work_area.x) {
    bounds.x = work_area.x;
  } else if (bounds.x + bounds.width >= work_area.x + work_area.width) {
    bounds.x = work_area.x + work_area.width - bounds.width;
  }

  if (bounds.y < work_area.y) {
    bounds.y = work_area.y;
  } else if (bounds.y + bounds.height >= work_area.y + work_area.height) {
    bounds.y = work_area.y + work_area.height - bounds.height;
  }

  return bounds;
}

}  // namespace

void RegisterGlobalPreferences(CefRawPtr<CefPreferenceRegistrar> registrar) {
  registrar->AddPreference(kPrefWindowRestore,
                           CreateDefaultWindowRestoreValue());
}

bool LoadWindowRestorePreferences(cef_show_state_t& show_state,
                                  std::optional<CefRect>& dip_bounds) {
  CEF_REQUIRE_UI_THREAD();

  // Check if show state was specified on the command-line.
  auto command_line = CefCommandLine::GetGlobalCommandLine();
  if (command_line->HasSwitch(switches::kInitialShowState)) {
    auto result = ShowStateFromString(
        command_line->GetSwitchValue(switches::kInitialShowState));
    if (result) {
      show_state = *result;
      return true;
    }
  }

  // Check if show state was saved in global preferences.
  auto manager = CefPreferenceManager::GetGlobalPreferenceManager();
  if (ParseWindowRestoreValue(manager->GetPreference(kPrefWindowRestore),
                              show_state, dip_bounds)) {
    if (dip_bounds) {
      // Keep the bounds inside the closest display.
      dip_bounds = ClampBoundsToDisplay(*dip_bounds);
    }
    return true;
  }

  return false;
}

bool SaveWindowRestorePreferences(cef_show_state_t show_state,
                                  std::optional<CefRect> dip_bounds) {
  CEF_REQUIRE_UI_THREAD();
  auto manager = CefPreferenceManager::GetGlobalPreferenceManager();

  CefString error;
  return manager->SetPreference(
      kPrefWindowRestore, CreateWindowRestoreValue(show_state, dip_bounds),
      error);
}

}  // namespace client::prefs
