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

#include "ui/native_theme/native_theme.h"

#include <stddef.h>

#include <algorithm>
#include <cmath>
#include <optional>
#include <utility>

#include "base/callback_list.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/sequence_checker.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/types/pass_key.h"
#include "build/build_config.h"
#include "cc/paint/paint_canvas.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/ui_base_switches.h"
#include "ui/color/color_id.h"
#include "ui/color/color_metrics.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_key.h"
#include "ui/color/color_provider_manager.h"
#include "ui/color/system_theme.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/native_theme/native_theme_observer.h"
#include "ui/native_theme/os_settings_provider.h"

#if BUILDFLAG(IS_MAC)
#include "ui/native_theme/native_theme_aura.h"
#include "ui/native_theme/native_theme_mac.h"
#elif defined(USE_AURA)
#include "ui/native_theme/features/native_theme_features.h"
#include "ui/native_theme/native_theme_aura.h"
#include "ui/native_theme/native_theme_fluent.h"
#if BUILDFLAG(IS_WIN)
#include "ui/native_theme/native_theme_win.h"
#endif
#else
#include "ui/native_theme/native_theme_mobile.h"
#endif

namespace ui {

namespace {

#if BUILDFLAG(IS_MAC)
using NativeUiTheme = NativeThemeMac;
using WebUiTheme = NativeThemeAura;
#elif defined(USE_AURA)
#if BUILDFLAG(IS_WIN)
using NativeUiTheme = NativeThemeWin;
#else
using NativeUiTheme = NativeThemeAura;
#endif
NativeTheme* GetInstanceForWebImpl() {
  static const bool use_fluent = IsFluentScrollbarEnabled();
  if (use_fluent) {
    static base::NoDestructor<NativeThemeFluent> s_web_theme;
    return s_web_theme.get();
  }
  static base::NoDestructor<NativeThemeAura> s_web_theme(
#if BUILDFLAG(IS_CHROMEOS)
      true
#else
      IsOverlayScrollbarEnabledByFeatureFlag()
#endif
  );
  return s_web_theme.get();
}
#else
using NativeUiTheme = NativeThemeMobile;
using WebUiTheme = NativeThemeMobile;
#endif

}  // namespace

NativeTheme::MenuListExtraParams::MenuListExtraParams() = default;

NativeTheme::MenuListExtraParams::MenuListExtraParams(
    const NativeTheme::MenuListExtraParams&) = default;

NativeTheme::MenuListExtraParams& NativeTheme::MenuListExtraParams::operator=(
    const NativeTheme::MenuListExtraParams&) = default;

NativeTheme::TextFieldExtraParams::TextFieldExtraParams() = default;

NativeTheme::TextFieldExtraParams::TextFieldExtraParams(
    const NativeTheme::TextFieldExtraParams&) = default;

NativeTheme::TextFieldExtraParams& NativeTheme::TextFieldExtraParams::operator=(
    const NativeTheme::TextFieldExtraParams&) = default;

NativeTheme::UpdateNotificationDelayScoper::UpdateNotificationDelayScoper() {
  ++num_instances_;
}

NativeTheme::UpdateNotificationDelayScoper::UpdateNotificationDelayScoper(
    const UpdateNotificationDelayScoper&) {
  ++num_instances_;
}

NativeTheme::UpdateNotificationDelayScoper::UpdateNotificationDelayScoper(
    UpdateNotificationDelayScoper&&) {
  ++num_instances_;
}

NativeTheme::UpdateNotificationDelayScoper::~UpdateNotificationDelayScoper() {
  if (--num_instances_ == 0) {
    GetDelayedNotifications().Notify();
  }
}

// static
base::CallbackListSubscription
NativeTheme::UpdateNotificationDelayScoper::RegisterCallback(
    base::PassKey<NativeTheme>,
    base::OnceClosure cb) {
  return GetDelayedNotifications().Add(std::move(cb));
}

// static
base::OnceClosureList&
NativeTheme::UpdateNotificationDelayScoper::GetDelayedNotifications() {
  static base::NoDestructor<base::OnceClosureList> s_delayed_notifications;
  return *s_delayed_notifications;
}

// static
size_t NativeTheme::UpdateNotificationDelayScoper::num_instances_ = 0;

// static
NativeTheme* NativeTheme::GetInstanceForNativeUi() {
  static base::NoDestructor<NativeUiTheme> s_native_theme;
  static bool initialized = false;
  if (!initialized) {
    s_native_theme->BeginObservingOsSettingChanges();
    initialized = true;
  }
  return s_native_theme.get();
}

// static
NativeTheme* NativeTheme::GetInstanceForWeb() {
#if defined(USE_AURA)
  NativeTheme* const native_theme = GetInstanceForWebImpl();
#else
  static base::NoDestructor<WebUiTheme> s_web_theme;
  NativeTheme* const native_theme = s_web_theme.get();
#endif
  static bool initialized = false;
  if (!initialized) {
    GetInstanceForNativeUi()->SetAssociatedWebInstance(native_theme);
    initialized = true;
  }
  return native_theme;
}

// static
float NativeTheme::AdjustBorderWidthByZoom(float border_width,
                                           float zoom_level) {
  return std::max(std::floor(border_width * zoom_level), 1.0f);
}

// static
float NativeTheme::AdjustBorderRadiusByZoom(Part part,
                                            float border_radius,
                                            float zoom) {
  return (part == kCheckbox || part == kTextField || part == kPushButton)
             ? AdjustBorderWidthByZoom(border_radius, zoom)
             : border_radius;
}

gfx::Size NativeTheme::GetPartSize(Part part,
                                   State state,
                                   const ExtraParams& extra_params) const {
  return {};
}

int NativeTheme::GetPaintedScrollbarTrackInset() const {
  return 0;
}

gfx::Insets NativeTheme::GetScrollbarSolidColorThumbInsets(Part part) const {
  return {};
}

float NativeTheme::GetBorderRadiusForPart(Part part,
                                          float width,
                                          float height) const {
  return 0;
}

bool NativeTheme::SupportsNinePatch(Part part) const {
  return false;
}

gfx::Size NativeTheme::GetNinePatchCanvasSize(Part part) const {
  NOTREACHED();
}

gfx::Rect NativeTheme::GetNinePatchAperture(Part part) const {
  NOTREACHED();
}

SkColor NativeTheme::GetScrollbarThumbColor(
    const ColorProvider* color_provider,
    State state,
    const ScrollbarThumbExtraParams& extra_params) const {
  NOTREACHED();
}

SkColor NativeTheme::GetSystemButtonPressedColor(SkColor base_color) const {
  return base_color;
}

void NativeTheme::BeginObservingOsSettingChanges() {
  os_settings_changed_subscription_ =
      OsSettingsProvider::RegisterOsSettingsChangedCallback(base::BindRepeating(
          &NativeTheme::OnToolkitSettingsChanged, base::Unretained(this)));
  UpdateVariablesForToolkitSettings();
}

void NativeTheme::AddObserver(NativeThemeObserver* observer) {
  native_theme_observers_.AddObserver(observer);
}

void NativeTheme::RemoveObserver(NativeThemeObserver* observer) {
  native_theme_observers_.RemoveObserver(observer);
}

void NativeTheme::NotifyOnNativeThemeUpdated() {
  // This specific method is prone to being mistakenly called on the wrong
  // sequence, because it is often invoked from a platform-specific event
  // listener, and those events may be delivered on unexpected sequences.
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (base::PassKey<NativeTheme> pass_key;
      UpdateNotificationDelayScoper::exists(pass_key)) {
    // At least one scoper exists, so delay notifications until it's gone.
    if (!update_delay_subscription_) {
      update_delay_subscription_ =
          UpdateNotificationDelayScoper::RegisterCallback(
              pass_key, base::BindOnce(&NativeTheme::NotifyOnNativeThemeUpdated,
                                       base::Unretained(this)));
    }
    return;
  }

  if (update_delay_subscription_) {
    // No scopers exist, but a subscription does: this is the callback from the
    // last scoper being destroyed. Reset the subscription so it can be
    // recreated in the future when necessary.
    update_delay_subscription_ = {};
  }

  base::ElapsedTimer timer;
  auto& color_provider_manager = ColorProviderManager::Get();
  const size_t initial_providers_initialized =
      color_provider_manager.num_providers_initialized();

  // Reset the ColorProviderManager's cache so that ColorProviders requested
  // from this point onwards incorporate the changes to the system theme.
  color_provider_manager.ResetColorProviderCache();

  NotifyOnNativeThemeUpdatedImpl();

  color_provider_manager.AfterNativeThemeUpdated();

  RecordNumColorProvidersInitializedDuringOnNativeThemeUpdated(
      color_provider_manager.num_providers_initialized() -
      initial_providers_initialized);
  RecordTimeSpentProcessingOnNativeThemeUpdatedEvent(timer.Elapsed());
}

void NativeTheme::NotifyOnCaptionStyleUpdated() {
  // This specific method is prone to being mistakenly called on the wrong
  // sequence, because it is often invoked from a platform-specific event
  // listener, and those events may be delivered on unexpected sequences.
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  native_theme_observers_.Notify(&NativeThemeObserver::OnCaptionStyleUpdated);
}

void NativeTheme::Paint(cc::PaintCanvas* canvas,
                        const ColorProvider* color_provider,
                        Part part,
                        State state,
                        const gfx::Rect& rect,
                        const ExtraParams& extra_params,
                        bool forced_colors,
                        PreferredColorScheme color_scheme,
                        PreferredContrast contrast,
                        std::optional<SkColor> accent_color) const {
  if (rect.IsEmpty()) {
    return;
  }

  // For `color_scheme`, `kNoPreference` means "use current".
  const bool dark_mode =
      color_scheme == PreferredColorScheme::kDark ||
      (color_scheme == PreferredColorScheme::kNoPreference &&
       preferred_color_scheme() == PreferredColorScheme::kDark);

  // Form control accents shouldn't be drawn with any transparency.
  // TODO(C++23): Replace the below with:
  // ```
  //  const std::optional<SkColor> accent_color_opaque = accent_color.transform(
  //      [](SkColor c) { return SkColorSetA(c, SK_AlphaOPAQUE); });
  // ```
  const std::optional<SkColor> accent_color_opaque =
      accent_color.has_value() ? std::make_optional(SkColorSetA(
                                     accent_color.value(), SK_AlphaOPAQUE))
                               : std::nullopt;

  gfx::Canvas gfx_canvas(canvas, 1.0f);
  gfx::ScopedCanvas scoped_canvas(&gfx_canvas);
  gfx_canvas.ClipRect(rect);

  PaintImpl(canvas, color_provider, part, state, rect, extra_params,
            forced_colors, dark_mode, contrast, accent_color_opaque);
}

ColorProviderKey NativeTheme::GetColorProviderKey(
    scoped_refptr<ColorProviderKey::ThemeInitializerSupplier> custom_theme,
    bool use_custom_frame) const {
  ColorProviderKey key;
  key.color_mode = preferred_color_scheme() == PreferredColorScheme::kDark
                       ? ColorProviderKey::ColorMode::kDark
                       : ColorProviderKey::ColorMode::kLight;
  key.contrast_mode = preferred_contrast() == PreferredContrast::kMore
                          ? ColorProviderKey::ContrastMode::kHigh
                          : ColorProviderKey::ContrastMode::kNormal;
  key.forced_colors = forced_colors();
  key.system_theme = system_theme();
  key.frame_type = use_custom_frame ? ColorProviderKey::FrameType::kChromium
                                    : ColorProviderKey::FrameType::kNative;
  key.user_color_source = preferred_color_source_;
  key.user_color = user_color();
  key.scheme_variant = scheme_variant();
  key.custom_theme = std::move(custom_theme);
  return key;
}

NativeTheme::NativeTheme(SystemTheme system_theme)
    : system_theme_(system_theme) {}

NativeTheme::~NativeTheme() = default;

bool NativeTheme::IsForcedDarkMode() {
  static bool kIsForcedDarkMode =
      base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kForceDarkMode);
  return kIsForcedDarkMode;
}

bool NativeTheme::IsForcedLightMode() {
  static bool kIsForcedLightMode =
      base::CommandLine::ForCurrentProcess()->HasSwitch(
          "force-light-mode");
  return kIsForcedLightMode;
}

bool NativeTheme::IsForcedHighContrast() {
  static bool kIsForcedHighContrast =
      base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kForceHighContrast);
  return kIsForcedHighContrast;
}

void NativeTheme::PaintMenuItemBackground(
    cc::PaintCanvas* canvas,
    const ColorProvider* color_provider,
    State state,
    const gfx::Rect& rect,
    const MenuItemExtraParams& extra_params) const {
  const SkScalar radius = SkIntToScalar(extra_params.corner_radius);
  cc::PaintFlags flags;
  const ColorId id = (state == kHovered)
#if BUILDFLAG(IS_CHROMEOS)
                         ? kColorAshSystemUIMenuItemBackgroundSelected
                         : kColorAshSystemUIMenuBackground;
#else
                         ? kColorMenuItemBackgroundSelected
                         : kColorMenuBackground;
#endif
  flags.setColor(color_provider->GetColor(id));
  canvas->drawRoundRect(gfx::RectToSkRect(rect), radius, radius, flags);
}

void NativeTheme::OnToolkitSettingsChanged(bool force_notify) {
  if (UpdateVariablesForToolkitSettings() || force_notify) {
    NotifyOnNativeThemeUpdated();
  }
}

void NativeTheme::SetAssociatedWebInstance(
    NativeTheme* associated_web_instance) {
  if (associated_web_instance_ != associated_web_instance) {
    associated_web_instance_ = associated_web_instance;
    if (UpdateWebInstance()) {
      associated_web_instance_->NotifyOnNativeThemeUpdatedImpl();
    }
  }
}

bool NativeTheme::UpdateWebInstance() const {
  if (!associated_web_instance_) {
    return false;
  }

  // NOTE: Intentionally does not copy the native "overlay scrollbar" setting to
  // the web instance, as the web instance often wants to differ there.
  // TODO(crbug.com/444399080): If we had a notion somewhere about "web wants
  // overlay scrollbars even when native doesn't", we could probably copy the
  // setting fearlessly here (and have that override it on the web instance
  // side), making callers who want to toggle overlay scrollbars on/off globally
  // simpler and safer.

  // TODO(pkasting): The code duplication between this function and
  // `UpdateVariablesForToolkitSettings()` is error-prone; e.g. it's easy to
  // forget to update the web instance properly when adding a new member.
  // Refactor to a settings struct or similar.

  bool updated_web_instance = false;
  if (associated_web_instance_->forced_colors() != forced_colors()) {
    associated_web_instance_->forced_colors_ = forced_colors();
    updated_web_instance = true;
  }
  if (associated_web_instance_->preferred_color_scheme() !=
      preferred_color_scheme()) {
    associated_web_instance_->preferred_color_scheme_ =
        preferred_color_scheme();
    updated_web_instance = true;
  }
  if (associated_web_instance_->preferred_contrast() != preferred_contrast()) {
    associated_web_instance_->preferred_contrast_ = preferred_contrast();
    updated_web_instance = true;
  }
  if (associated_web_instance_->prefers_reduced_transparency() !=
      prefers_reduced_transparency()) {
    associated_web_instance_->prefers_reduced_transparency_ =
        prefers_reduced_transparency();
    updated_web_instance = true;
  }
  if (associated_web_instance_->inverted_colors() != inverted_colors()) {
    associated_web_instance_->inverted_colors_ = inverted_colors();
    updated_web_instance = true;
  }
  if (associated_web_instance_->user_color() != user_color()) {
    associated_web_instance_->user_color_ = user_color();
    updated_web_instance = true;
  }
  if (associated_web_instance_->scheme_variant() != scheme_variant()) {
    associated_web_instance_->scheme_variant_ = scheme_variant();
    updated_web_instance = true;
  }
  if (associated_web_instance_->preferred_color_source_ !=
      preferred_color_source_) {
    associated_web_instance_->preferred_color_source_ = preferred_color_source_;
    updated_web_instance = true;
  }
  if (associated_web_instance_->caret_blink_interval() !=
      caret_blink_interval()) {
    associated_web_instance_->caret_blink_interval_ = caret_blink_interval();
    updated_web_instance = true;
  }
  return updated_web_instance;
}

void NativeTheme::NotifyOnNativeThemeUpdatedImpl() {
  // Update any associated web instance's settings before notifying observers,
  // since those observers may attempt to override the web instance's settings
  // (e.g. to implement web-content-specific forced colors).
  const bool updated_web_instance = UpdateWebInstance();

  native_theme_observers_.Notify(&NativeThemeObserver::OnNativeThemeUpdated,
                                 this);

  // If the web instance was modified above, also notify its observers. This is
  // done last so any of our observers that modify the web instance will have
  // already run.
  //
  // NOTE: If any above observers already called `NotifyOnNativeThemeUpdated()`
  // on the web theme, this is unnecessary jank; however, it's not worth the
  // hassle to try to detect this.
  //
  // TODO(pkasting): Adding a scoping object to batch updates would address
  // this; see comments in header above accessors.
  if (updated_web_instance) {
    // Calling `NotifyOnNativeThemeUpdated()` here would unnecessarily churn the
    // color provider cache.
    associated_web_instance_->NotifyOnNativeThemeUpdatedImpl();
  }
}

bool NativeTheme::UpdateVariablesForToolkitSettings() {
  // This should not be called except in an instance that is monitoring OS
  // setting changes. Otherwise, either:
  //   * This is the associated web instance of another instance, and that
  //     instance will update our members via `UpdateWebInstance()`, so updating
  //     them here is both wasteful and potentially incorrect
  //   * Or, whoever created this instance didn't call
  //     `BeginObservingOsSettingChanges()` and should have
  // Getting this right is important, because calling
  // `OsSettingsProvider::Get()` in the renderer may not return the expected
  // instance (see comments on that function), so we shouldn't be introducing
  // new calls to it carelessly.
  CHECK(os_settings_changed_subscription_);

  // Calculate updated values.
  const auto& os_settings_provider = OsSettingsProvider::Get();
  const auto new_forced_colors = CalculateForcedColors();
  const auto new_preferred_color_scheme = CalculatePreferredColorScheme();
  const auto new_preferred_contrast = CalculatePreferredContrast();
  const auto new_prefers_reduced_transparency =
      os_settings_provider.PrefersReducedTransparency();
  const auto new_inverted_colors = os_settings_provider.PrefersInvertedColors();
  const auto new_user_color = os_settings_provider.AccentColor();
  const auto new_scheme_variant = os_settings_provider.SchemeVariant();
  const auto new_preferred_color_source =
      os_settings_provider.PreferredColorSource();
  const auto new_caret_blink_interval =
      os_settings_provider.CaretBlinkInterval();

  // Set updated values and see if anything changed.
  bool updated = false;
  if (forced_colors() != new_forced_colors) {
    forced_colors_ = new_forced_colors;
    updated = true;
  }
  if (preferred_color_scheme() != new_preferred_color_scheme) {
    preferred_color_scheme_ = new_preferred_color_scheme;
    updated = true;
  }
  if (preferred_contrast() != new_preferred_contrast) {
    preferred_contrast_ = new_preferred_contrast;
    updated = true;
  }
  if (prefers_reduced_transparency() != new_prefers_reduced_transparency) {
    prefers_reduced_transparency_ = new_prefers_reduced_transparency;
    updated = true;
  }
  if (inverted_colors() != new_inverted_colors) {
    inverted_colors_ = new_inverted_colors;
    updated = true;
  }
  if (user_color() != new_user_color) {
    user_color_ = new_user_color;
    updated = true;
  }
  if (scheme_variant() != new_scheme_variant) {
    scheme_variant_ = new_scheme_variant;
    updated = true;
  }
  if (preferred_color_source_ != new_preferred_color_source) {
    preferred_color_source_ = new_preferred_color_source;
    updated = true;
  }
  if (caret_blink_interval() != new_caret_blink_interval) {
    caret_blink_interval_ = new_caret_blink_interval;
    updated = true;
  }

  return updated;
}

ColorProviderKey::ForcedColors NativeTheme::CalculateForcedColors() const {
  return (IsForcedHighContrast() ||
          OsSettingsProvider::Get().ForcedColorsActive())
             ? ColorProviderKey::ForcedColors::kSystem
             : ColorProviderKey::ForcedColors::kNone;
}

NativeTheme::PreferredColorScheme NativeTheme::CalculatePreferredColorScheme()
    const {
  if (IsForcedDarkMode()) {
    return PreferredColorScheme::kDark;
  }
  if (IsForcedLightMode()) {
    return PreferredColorScheme::kLight;
  }
  return OsSettingsProvider::Get().PreferredColorScheme();
}

NativeTheme::PreferredContrast NativeTheme::CalculatePreferredContrast() const {
  return IsForcedHighContrast() ? PreferredContrast::kMore
                                : OsSettingsProvider::Get().PreferredContrast();
}

}  // namespace ui