// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/dbus/menu/menu_property_list.h"

#include <string>
#include <utility>

#include "base/containers/span.h"
#include "base/memory/ref_counted_memory.h"
#include "base/notimplemented.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/accelerators/menu_label_accelerator_util_linux.h"
#include "ui/base/models/image_model.h"
#include "ui/base/models/menu_model.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/gfx/image/image.h"

namespace {

std::string ToDBusKeySym(ui::KeyboardCode code) {
  const uint16_t c =
      DomCodeToUsLayoutCharacter(UsLayoutKeyboardCodeToDomCode(code), 0);
  if (!c) {
    return std::string();
  }
  return base::UTF16ToUTF8(std::u16string(1, c));
}

std::vector<std::string> GetDbusMenuShortcut(ui::Accelerator accelerator) {
  auto dbus_key_sym = ToDBusKeySym(accelerator.key_code());
  if (dbus_key_sym.empty()) {
    return {};
  }

  std::vector<std::string> parts;
  if (accelerator.IsCtrlDown()) {
    parts.emplace_back("Control");
  }
  if (accelerator.IsAltDown()) {
    parts.emplace_back("Alt");
  }
  if (accelerator.IsShiftDown()) {
    parts.emplace_back("Shift");
  }
  if (accelerator.IsCmdDown()) {
    parts.emplace_back("Super");
  }
  parts.emplace_back(dbus_key_sym);
  return parts;
}

}  // namespace

MenuItemProperties ComputeMenuPropertiesForMenuItem(ui::MenuModel* menu,
                                                    size_t i) {
  // Properties should only be set if they differ from the default values.
  MenuItemProperties properties;

  // The dbusmenu interface has no concept of a "sublabel", "minor text", or
  // "minor icon" like MenuModel has.  Ignore these rather than trying to
  // merge them with the regular label and icon.
  std::u16string label = menu->GetLabelAt(i);
  if (!label.empty()) {
    properties["label"] = dbus_utils::Variant::Wrap<"s">(
        ui::ConvertAcceleratorsFromWindowsStyle(base::UTF16ToUTF8(label)));
  }

  if (!menu->IsEnabledAt(i)) {
    properties["enabled"] = dbus_utils::Variant::Wrap<"b">(false);
  }
  if (!menu->IsVisibleAt(i)) {
    properties["visible"] = dbus_utils::Variant::Wrap<"b">(false);
  }

  ui::ImageModel icon = menu->GetIconAt(i);
  if (icon.IsImage()) {
    auto png_bytes = icon.GetImage().As1xPNGBytes();
    auto span = base::as_byte_span(*png_bytes);
    properties["icon-data"] = dbus_utils::Variant::Wrap<"ay">(
        std::vector<uint8_t>(span.begin(), span.end()));
  }

  ui::Accelerator accelerator;
  if (menu->GetAcceleratorAt(i, &accelerator)) {
    auto parts = GetDbusMenuShortcut(accelerator);
    if (!parts.empty()) {
      properties["shortcut"] = dbus_utils::Variant::Wrap<"aas">(
          std::vector<std::vector<std::string>>{std::move(parts)});
    }
  }

  switch (menu->GetTypeAt(i)) {
    case ui::MenuModel::TYPE_COMMAND:
    case ui::MenuModel::TYPE_HIGHLIGHTED:
    case ui::MenuModel::TYPE_TITLE:
      // Nothing special to do.
      break;
    case ui::MenuModel::TYPE_CHECK:
    case ui::MenuModel::TYPE_RADIO:
      properties["toggle-type"] = dbus_utils::Variant::Wrap<"s">(
          menu->GetTypeAt(i) == ui::MenuModel::TYPE_CHECK ? "checkmark"
                                                          : "radio");
      properties["toggle-state"] =
          dbus_utils::Variant::Wrap<"i">(menu->IsItemCheckedAt(i) ? 1 : 0);
      break;
    case ui::MenuModel::TYPE_SEPARATOR:
      // The dbusmenu interface doesn't have multiple types of separators like
      // MenuModel.  Just use a regular separator in all cases.
      properties["type"] = dbus_utils::Variant::Wrap<"s">("separator");
      break;
    case ui::MenuModel::TYPE_BUTTON_ITEM:
      // This type of menu represents a row of buttons, but the dbusmenu
      // interface has no equivalent of this.  Ignore these items for now
      // since there's currently no uses of it that plumb into this codepath.
      // If there are button menu items in the future, we'd have to fake them
      // with multiple menu items.
      NOTIMPLEMENTED();
      break;
    case ui::MenuModel::TYPE_SUBMENU:
    case ui::MenuModel::TYPE_ACTIONABLE_SUBMENU:
      properties["children-display"] =
          dbus_utils::Variant::Wrap<"s">("submenu");
      break;
  }

  return properties;
}

void ComputeMenuPropertyChanges(const MenuItemProperties& old_properties,
                                const MenuItemProperties& new_properties,
                                MenuPropertyList* item_updated_props,
                                MenuPropertyList* item_removed_props) {
  // Compute updated and removed properties.
  for (const auto& pair : old_properties) {
    const std::string& key = pair.first;
    auto new_it = new_properties.find(key);
    if (new_it != new_properties.end()) {
      if (new_it->second != pair.second) {
        item_updated_props->push_back(key);
      }
    } else {
      item_removed_props->push_back(key);
    }
  }
  // Compute added properties.
  for (const auto& pair : new_properties) {
    const std::string& key = pair.first;
    if (!old_properties.contains(key)) {
      item_updated_props->push_back(key);
    }
  }
}
