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

// IMPORTANT NOTE: All QtShim members that use `delegate_` must be decorated
// with DISABLE_CFI_VCALL.

#include "ui/qt/qt_shim.h"

#include <QApplication>
#include <QFont>
#include <QIcon>
#include <QMimeDatabase>
#include <QMimeType>
#include <QPainter>
#include <QPalette>
#include <QScreen>
#include <QStyle>
#include <QStyleOptionTitleBar>
#include <cmath>

#include "base/compiler_specific.h"

namespace qt {

namespace {

bool IsStyleItalic(QFont::Style style) {
  switch (style) {
    case QFont::Style::StyleNormal:
      return false;
    case QFont::Style::StyleItalic:
    // gfx::Font::FontStyle doesn't support oblique, so treat it as italic.
    case QFont::Style::StyleOblique:
      return true;
  }
}

FontHinting QtHintingToFontHinting(QFont::HintingPreference hinting) {
  switch (hinting) {
    case QFont::PreferDefaultHinting:
      return FontHinting::kDefault;
    case QFont::PreferNoHinting:
      return FontHinting::kNone;
    case QFont::PreferVerticalHinting:
      // QT treats vertical hinting as "light" for Freetype:
      // https://doc.qt.io/qt-5/qfont.html#HintingPreference-enum
      return FontHinting::kLight;
    case QFont::PreferFullHinting:
      return FontHinting::kFull;
  }
}

// Obtain the average color of a gradient.
SkColor GradientColor(const QGradient& gradient) {
  QGradientStops stops = gradient.stops();
  if (stops.empty()) {
    return qRgba(0, 0, 0, 0);
  }

  float a = 0;
  float r = 0;
  float g = 0;
  float b = 0;
  for (int i = 0; i < stops.size(); i++) {
    // Determine the extents of this stop.  The whole gradient interval is [0,
    // 1], so extend to the endpoint if this is the first or last stop.
    float left_interval =
        i == 0 ? stops[i].first : (stops[i].first - stops[i - 1].first) / 2;
    float right_interval = i == stops.size() - 1
                               ? 1 - stops[i].first
                               : (stops[i + 1].first - stops[i].first) / 2;
    float length = left_interval + right_interval;

    // alpha() returns a value in [0, 255] and alphaF() returns a value in
    // [0, 1]. The color values are not premultiplied so the RGB channels need
    // to be multiplied by the alpha (in range [0, 1]) before summing.  The
    // alpha doesn't need to be multiplied, so we just sum color.alpha() in
    // range [0, 255] directly.
    const QColor& color = stops[i].second;
    a += color.alpha() * length;
    r += color.alphaF() * color.red() * length;
    g += color.alphaF() * color.green() * length;
    b += color.alphaF() * color.blue() * length;
  }
  return qRgba(r, g, b, a);
}

// Obtain the average color of a texture.
SkColor TextureColor(QImage image) {
  size_t size = image.width() * image.height();
  if (!size) {
    return qRgba(0, 0, 0, 0);
  }

  if (image.format() != QImage::Format_ARGB32_Premultiplied) {
    image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
  }

  size_t a = 0;
  size_t r = 0;
  size_t g = 0;
  size_t b = 0;
  const auto* pixels = reinterpret_cast<QRgb*>(image.bits());
  for (size_t i = 0; i < size; i++) {
    auto color = QColor::fromRgba(UNSAFE_TODO(pixels[i]));
    a += color.alpha();
    r += color.red();
    g += color.green();
    b += color.blue();
  }
  return qRgba(r / size, g / size, b / size, a / size);
}

SkColor BrushColor(const QBrush& brush) {
  QColor color = brush.color();
  auto alpha_blend = [&](uint8_t alpha) {
    QColor blended = color;
    blended.setAlpha(blended.alpha() * alpha / 255);
    return blended.rgba();
  };

  switch (brush.style()) {
    case Qt::SolidPattern:
      return alpha_blend(0xFF);
    case Qt::Dense1Pattern:
      return alpha_blend(0xE0);
    case Qt::Dense2Pattern:
      return alpha_blend(0xC0);
    case Qt::Dense3Pattern:
      return alpha_blend(0xA0);
    case Qt::Dense4Pattern:
      return alpha_blend(0x80);
    case Qt::Dense5Pattern:
      return alpha_blend(0x60);
    case Qt::Dense6Pattern:
      return alpha_blend(0x40);
    case Qt::Dense7Pattern:
      return alpha_blend(0x20);
    case Qt::NoBrush:
      return alpha_blend(0x00);
    case Qt::HorPattern:
    case Qt::VerPattern:
      return alpha_blend(0x20);
    case Qt::CrossPattern:
      return alpha_blend(0x40);
    case Qt::BDiagPattern:
    case Qt::FDiagPattern:
      return alpha_blend(0x20);
    case Qt::DiagCrossPattern:
      return alpha_blend(0x40);
    case Qt::LinearGradientPattern:
    case Qt::RadialGradientPattern:
    case Qt::ConicalGradientPattern:
      return GradientColor(*brush.gradient());
    case Qt::TexturePattern:
      return TextureColor(brush.textureImage());
  }
}

QPalette::ColorRole ColorTypeToColorRole(ColorType type) {
  switch (type) {
    case ColorType::kWindowBg:
      return QPalette::Window;
    case ColorType::kWindowFg:
      return QPalette::WindowText;
    case ColorType::kHighlightBg:
      return QPalette::Highlight;
    case ColorType::kHighlightFg:
      return QPalette::HighlightedText;
    case ColorType::kEntryBg:
      return QPalette::Base;
    case ColorType::kEntryFg:
      return QPalette::Text;
    case ColorType::kButtonBg:
      return QPalette::Button;
    case ColorType::kButtonFg:
      return QPalette::ButtonText;
    case ColorType::kLight:
      return QPalette::Light;
    case ColorType::kMidlight:
      return QPalette::Midlight;
    case ColorType::kMidground:
      return QPalette::Mid;
    case ColorType::kDark:
      return QPalette::Dark;
    case ColorType::kShadow:
      return QPalette::Shadow;
  }
}

QPalette::ColorGroup ColorStateToColorGroup(ColorState state) {
  switch (state) {
    case ColorState::kNormal:
      return QPalette::Normal;
    case ColorState::kDisabled:
      return QPalette::Disabled;
    case ColorState::kInactive:
      return QPalette::Inactive;
  }
}

float GetScreenScale(const QScreen* screen) {
  constexpr double kDefaultPixelDpi = 96.0;
  double scale = screen->devicePixelRatio() * screen->logicalDotsPerInch() /
                 kDefaultPixelDpi;
  // Round to the nearest 1/16th so that UI can losslessly multiply and divide
  // by the scale factor using floating point arithmetic.  GtkUi also rounds
  // in this way, but to 1/64th.  1/16th is chosen here since that's what
  // KDE settings uses.
  scale = std::round(scale * 16) / 16;
  return scale > 0 ? scale : 1.0;
}

bool IsValidIconThemeName(const QString& theme) {
  if (theme.isEmpty() || theme == "." || theme == "..") {
    return false;
  }
  if (theme.contains('/') || theme.contains('\\') || theme.contains("..")) {
    return false;
  }
  return true;
}

}  // namespace

QtShim::QtShim(QtInterface::Delegate* delegate, int* argc, char** argv)
    : delegate_(delegate), app_(*argc, argv) {
  connect(&app_, SIGNAL(fontChanged(const QFont&)), this,
          SLOT(FontChanged(const QFont&)));
  connect(&app_, SIGNAL(paletteChanged(const QPalette&)), this,
          SLOT(PaletteChanged(const QPalette&)));
  connect(&app_, SIGNAL(screenAdded(QScreen*)), this,
          SLOT(ScreenAdded(QScreen*)));
  connect(&app_, SIGNAL(screenRemoved(QScreen*)), this,
          SLOT(ScreenRemoved(QScreen*)));
  for (QScreen* screen : app_.screens()) {
    ScreenAdded(screen);
  }
}

QtShim::~QtShim() = default;

size_t QtShim::GetMonitorConfig(MonitorScale** monitors, float* primary_scale) {
  size_t n_monitors = app_.screens().size();
  monitor_scales_.resize(n_monitors);
  for (size_t i = 0; i < n_monitors; i++) {
    const QScreen* screen = app_.screens()[i];
    MonitorScale monitor = monitor_scales_[i];
    monitor.x_px = screen->geometry().x();
    monitor.y_px = screen->geometry().y();
    monitor.width_px = screen->geometry().width();
    monitor.height_px = screen->geometry().height();
    monitor.scale = GetScreenScale(screen);
  }
  *monitors = monitor_scales_.data();
  *primary_scale = GetScreenScale(app_.primaryScreen());
  return n_monitors;
}

FontRenderParams QtShim::GetFontRenderParams() const {
  QFont font = app_.font();
  auto style = font.styleStrategy();
  return {
      .antialiasing = !(style & QFont::StyleStrategy::NoAntialias),
      .use_bitmaps = !!(style & QFont::StyleStrategy::PreferBitmap),
      .hinting = QtHintingToFontHinting(font.hintingPreference()),
  };
}

FontDescription QtShim::GetFontDescription() const {
  QFont font = app_.font();
  return {
      .family = String(font.family().toStdString().c_str()),
      .size_pixels = font.pixelSize(),
      .size_points = font.pointSize(),
      .is_italic = IsStyleItalic(font.style()),
      .weight = font.weight(),
  };
}

Image QtShim::GetIconForContentType(const String& content_type,
                                    int size) const {
  if (!IsValidIconThemeName(QIcon::themeName())) {
    QIcon::setThemeName("hicolor");
  }

  QMimeDatabase db;
  for (const char* mime : {content_type.c_str(), "application/octet-stream"}) {
    auto mt = db.mimeTypeForName(mime);
    for (const auto& name : {mt.iconName(), mt.genericIconName()}) {
      auto icon = QIcon::fromTheme(name);
      auto pixmap = icon.pixmap(size);
      auto image = pixmap.toImage();
      if (image.format() != QImage::Format_ARGB32_Premultiplied) {
        image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
      }
      if (auto bytes = image.sizeInBytes()) {
        return {image.width(), image.height(),
                static_cast<float>(image.devicePixelRatio()),
                Buffer(image.bits(), bytes)};
      }
    }
  }
  return {};
}

SkColor QtShim::GetColor(ColorType role, ColorState state) const {
  return BrushColor(app_.palette().brush(ColorStateToColorGroup(state),
                                         ColorTypeToColorRole(role)));
}

SkColor QtShim::GetFrameColor(ColorState state, bool use_custom_frame) const {
  constexpr int kSampleSize = 32;
  return TextureColor(DrawHeaderImpl(kSampleSize, kSampleSize,
                                     GetColor(ColorType::kWindowBg, state),
                                     state, use_custom_frame));
}

int QtShim::GetCursorBlinkIntervalMs() const {
  return app_.cursorFlashTime();
}

int QtShim::GetAnimationDurationMs() const {
  return app_.style()->styleHint(QStyle::SH_Widget_Animation_Duration);
}

DISABLE_CFI_VCALL
void QtShim::FontChanged(const QFont& font) {
  delegate_->FontChanged();
}

DISABLE_CFI_VCALL
void QtShim::PaletteChanged(const QPalette& palette) {
  delegate_->ThemeChanged();
}

DISABLE_CFI_VCALL
void QtShim::ScreenAdded(QScreen* screen) {
  connect(screen, SIGNAL(logicalDotsPerInchChanged(qreal)), this,
          SLOT(LogicalDotsPerInchChanged(qreal)));
  connect(screen, SIGNAL(physicalDotsPerInchChanged(qreal)), this,
          SLOT(PhysicalDotsPerInchChanged(qreal)));
  delegate_->ScaleFactorMaybeChanged();
}

DISABLE_CFI_VCALL
void QtShim::ScreenRemoved(QScreen* screen) {
  delegate_->ScaleFactorMaybeChanged();
}

DISABLE_CFI_VCALL
void QtShim::LogicalDotsPerInchChanged(qreal dpi) {
  delegate_->ScaleFactorMaybeChanged();
}

DISABLE_CFI_VCALL
void QtShim::PhysicalDotsPerInchChanged(qreal dpi) {
  delegate_->ScaleFactorMaybeChanged();
}

Image QtShim::DrawHeader(int width,
                         int height,
                         SkColor default_color,
                         ColorState state,
                         bool use_custom_frame) const {
  QImage image =
      DrawHeaderImpl(width, height, default_color, state, use_custom_frame);
  return {width, height, 1.0f, Buffer(image.bits(), image.sizeInBytes())};
}

QImage QtShim::DrawHeaderImpl(int width,
                              int height,
                              SkColor default_color,
                              ColorState state,
                              bool use_custom_frame) const {
  QImage image(width, height, QImage::Format_ARGB32_Premultiplied);
  image.fill(default_color);
  QPainter painter(&image);
  if (use_custom_frame) {
    // Chrome renders it's own window border, so clip the border out by
    // rendering the titlebar larger than the image.
    constexpr int kBorderWidth = 5;

    QStyleOptionTitleBar opt;
    opt.rect = QRect(-kBorderWidth, -kBorderWidth, width + 2 * kBorderWidth,
                     height + 2 * kBorderWidth);
    if (state == ColorState::kNormal) {
      opt.titleBarState = QStyle::State_Active;
    }
    app_.style()->drawComplexControl(QStyle::CC_TitleBar, &opt, &painter,
                                     nullptr);
  } else {
    painter.fillRect(
        0, 0, width, height,
        app_.palette().brush(ColorStateToColorGroup(state), QPalette::Window));
  }
  return image;
}

}  // namespace qt

qt::QtInterface* CreateQtInterface(qt::QtInterface::Delegate* delegate,
                                   int* argc,
                                   char** argv) {
  return new qt::QtShim(delegate, argc, argv);
}
