/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ScrollbarDrawing.h"

#include "mozilla/RelativeLuminanceUtils.h"
#include "mozilla/StaticPrefs_widget.h"
#include "nsDeviceContext.h"
#include "nsIFrame.h"
#include "nsLayoutUtils.h"
#include "nsScrollbarFrame.h"
#include "nsNativeTheme.h"

using namespace mozilla::gfx;

namespace mozilla::widget {

using mozilla::RelativeLuminanceUtils;

/* static */
auto ScrollbarDrawing::GetDPIRatioForScrollbarPart(const nsPresContext* aPc)
    -> DPIRatio {
  DPIRatio ratio(
      float(AppUnitsPerCSSPixel()) /
      float(aPc->DeviceContext()->AppUnitsPerDevPixelAtUnitFullZoom()));
  if (aPc->IsPrintPreview()) {
    ratio.scale *= aPc->GetPrintPreviewScaleForSequenceFrameOrScrollbars();
  }
  if (mKind == Kind::Cocoa) {
    return DPIRatio(ratio.scale >= 2.0f ? 2.0f : 1.0f);
  }
  return ratio;
}

/*static*/
nsScrollbarFrame* ScrollbarDrawing::GetParentScrollbarFrame(nsIFrame* aFrame) {
  for (; aFrame; aFrame = aFrame->GetParent()) {
    if (nsScrollbarFrame* f = do_QueryFrame(aFrame)) {
      return f;
    }
  }
  return nullptr;
}

/*static*/
bool ScrollbarDrawing::IsParentScrollbarRolledOver(nsIFrame* aFrame) {
  if (nsScrollbarFrame* f = GetParentScrollbarFrame(aFrame)) {
    if (nsLayoutUtils::UseOverlayScrollbars(f)) {
      return f->HasBeenHovered();
    }
    return f->GetContent()->AsElement()->State().HasState(ElementState::HOVER);
  }
  return false;
}

/*static*/
bool ScrollbarDrawing::IsParentScrollbarHoveredOrActive(nsIFrame* aFrame) {
  nsIFrame* scrollbarFrame = GetParentScrollbarFrame(aFrame);
  return scrollbarFrame &&
         scrollbarFrame->GetContent()
             ->AsElement()
             ->State()
             .HasAtLeastOneOfStates(ElementState::HOVER | ElementState::ACTIVE);
}

/*static*/
bool ScrollbarDrawing::IsScrollbarWidthThin(const nsIFrame* aFrame) {
  auto scrollbarWidth = nsLayoutUtils::ScrollbarWidthFor(aFrame);
  return scrollbarWidth == StyleScrollbarWidth::Thin;
}

CSSIntCoord ScrollbarDrawing::GetCSSScrollbarSize(StyleScrollbarWidth aWidth,
                                                  Overlay aOverlay) const {
  return mScrollbarSize[aWidth == StyleScrollbarWidth::Thin]
                       [aOverlay == Overlay::Yes];
}

void ScrollbarDrawing::ConfigureScrollbarSize(StyleScrollbarWidth aWidth,
                                              Overlay aOverlay,
                                              CSSIntCoord aSize) {
  mScrollbarSize[aWidth == StyleScrollbarWidth::Thin]
                [aOverlay == Overlay::Yes] = aSize;
}

void ScrollbarDrawing::ConfigureScrollbarSize(CSSIntCoord aSize) {
  ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::No, aSize);
  ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::Yes, aSize);
  ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::No, aSize / 2);
  ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::Yes, aSize / 2);
}

LayoutDeviceIntCoord ScrollbarDrawing::GetScrollbarSize(
    const nsPresContext* aPresContext, StyleScrollbarWidth aWidth,
    Overlay aOverlay) {
  return (CSSCoord(GetCSSScrollbarSize(aWidth, aOverlay)) *
          GetDPIRatioForScrollbarPart(aPresContext))
      .Rounded();
}

LayoutDeviceIntCoord ScrollbarDrawing::GetScrollbarSize(
    const nsPresContext* aPresContext, nsIFrame* aFrame) {
  auto width = nsLayoutUtils::ScrollbarWidthFor(aFrame);
  auto overlay =
      nsLayoutUtils::UseOverlayScrollbars(aFrame) ? Overlay::Yes : Overlay::No;
  return GetScrollbarSize(aPresContext, width, overlay);
}

bool ScrollbarDrawing::IsScrollbarTrackOpaque(nsIFrame* aFrame) {
  auto trackColor = ComputeScrollbarTrackColor(
      aFrame, *nsLayoutUtils::StyleForScrollbar(aFrame),
      Colors(aFrame, StyleAppearance::ScrollbarVertical));
  return trackColor.a == 1.0f;
}

sRGBColor ScrollbarDrawing::ComputeScrollbarTrackColor(
    nsIFrame* aFrame, const ComputedStyle& aStyle, const Colors& aColors) {
  if (aColors.HighContrast()) {
    return aColors.System(StyleSystemColor::Window);
  }
  const nsStyleUI* ui = aStyle.StyleUI();
  if (ui->mScrollbarColor.IsColors()) {
    return sRGBColor::FromABGR(
        ui->mScrollbarColor.AsColors().track.CalcColor(aStyle));
  }
  static constexpr sRGBColor sDefaultDarkTrackColor =
      sRGBColor::FromU8(20, 20, 25, 77);
  static constexpr sRGBColor sDefaultTrackColor(
      gfx::sRGBColor::UnusualFromARGB(0xfff0f0f0));

  auto systemColor = StyleSystemColor::ThemedScrollbar;
  return aColors.SystemOrElse(systemColor, [&] {
    return aColors.IsDark() ? sDefaultDarkTrackColor : sDefaultTrackColor;
  });
}

// Don't use the theme color for dark scrollbars if it's not a color (if it's
// grey-ish), as that'd either lack enough contrast, or be close to what we'd do
// by default anyways.
sRGBColor ScrollbarDrawing::ComputeScrollbarThumbColor(
    nsIFrame* aFrame, const ComputedStyle& aStyle,
    const ElementState& aElementState, const Colors& aColors) {
  const nsStyleUI* ui = aStyle.StyleUI();
  if (ui->mScrollbarColor.IsColors()) {
    return sRGBColor::FromABGR(ThemeColors::AdjustUnthemedScrollbarThumbColor(
        ui->mScrollbarColor.AsColors().thumb.CalcColor(aStyle), aElementState));
  }

  auto systemColor = [&] {
    if (aElementState.HasState(ElementState::ACTIVE)) {
      if (aColors.HighContrast()) {
        return StyleSystemColor::Selecteditem;
      }
      return StyleSystemColor::ThemedScrollbarThumbActive;
    }
    if (aElementState.HasState(ElementState::HOVER)) {
      if (aColors.HighContrast()) {
        return StyleSystemColor::Selecteditem;
      }
      return StyleSystemColor::ThemedScrollbarThumbHover;
    }
    if (aColors.HighContrast()) {
      return StyleSystemColor::Windowtext;
    }
    return StyleSystemColor::ThemedScrollbarThumb;
  }();

  return aColors.SystemOrElse(systemColor, [&] {
    const nscolor unthemedColor = aColors.IsDark() ? NS_RGBA(249, 249, 250, 102)
                                                   : NS_RGB(0xcd, 0xcd, 0xcd);

    return sRGBColor::FromABGR(ThemeColors::AdjustUnthemedScrollbarThumbColor(
        unthemedColor, aElementState));
  });
}

template <typename PaintBackendData>
bool ScrollbarDrawing::DoPaintDefaultScrollbar(
    PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const ElementState& aElementState, const Colors& aColors,
    const DPIRatio& aDpiRatio) {
  const bool overlay = nsLayoutUtils::UseOverlayScrollbars(aFrame);
  if (overlay && !aElementState.HasAtLeastOneOfStates(ElementState::HOVER |
                                                      ElementState::ACTIVE)) {
    return true;
  }
  const auto backgroundColor =
      ComputeScrollbarTrackColor(aFrame, aStyle, aColors);
  auto radius = [&]() -> CSSCoord {
    if (overlay && mKind == Kind::Win11 &&
        StaticPrefs::widget_non_native_theme_win11_scrollbar_round_track()) {
      LayoutDeviceCoord radius =
          (aScrollbarKind == ScrollbarKind::Horizontal ? aRect.height
                                                       : aRect.width) /
          2.0f;
      return radius / aDpiRatio;
    }
    return 0.0f;
  }();
  auto borderColor = aColors.System(StyleSystemColor::Buttontext);
  // Draw an outline around the scrollbar in high contrast mode
  auto borderWidth = aColors.HighContrast() ? CSSCoord(1.0f) : CSSCoord(0.0f);
  ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, backgroundColor,
                                           borderColor, borderWidth, radius,
                                           aDpiRatio);
  return true;
}

bool ScrollbarDrawing::PaintScrollbar(
    DrawTarget& aDrawTarget, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const ElementState& aElementState, const Colors& aColors,
    const DPIRatio& aDpiRatio) {
  return DoPaintDefaultScrollbar(aDrawTarget, aRect, aScrollbarKind, aFrame,
                                 aStyle, aElementState, aColors, aDpiRatio);
}

bool ScrollbarDrawing::PaintScrollbar(
    WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const ElementState& aElementState, const Colors& aColors,
    const DPIRatio& aDpiRatio) {
  return DoPaintDefaultScrollbar(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
                                 aElementState, aColors, aDpiRatio);
}

template <typename PaintBackendData>
bool ScrollbarDrawing::DoPaintDefaultScrollCorner(
    PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const Colors& aColors, const DPIRatio& aDpiRatio) {
  auto scrollbarColor = ComputeScrollbarTrackColor(aFrame, aStyle, aColors);
  ThemeDrawing::FillRect(aPaintData, aRect, scrollbarColor);
  return true;
}

bool ScrollbarDrawing::PaintScrollCorner(
    DrawTarget& aDrawTarget, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const Colors& aColors, const DPIRatio& aDpiRatio) {
  return DoPaintDefaultScrollCorner(aDrawTarget, aRect, aScrollbarKind, aFrame,
                                    aStyle, aColors, aDpiRatio);
}

bool ScrollbarDrawing::PaintScrollCorner(
    WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const Colors& aColors, const DPIRatio& aDpiRatio) {
  return DoPaintDefaultScrollCorner(aWrData, aRect, aScrollbarKind, aFrame,
                                    aStyle, aColors, aDpiRatio);
}

nscolor ScrollbarDrawing::GetScrollbarButtonColor(nscolor aTrackColor,
                                                  ElementState aStates) {
  // See numbers in GetScrollbarArrowColor.
  // This function is written based on ratios between values listed there.

  bool isActive = aStates.HasState(ElementState::ACTIVE);
  bool isHover = aStates.HasState(ElementState::HOVER);
  if (!isActive && !isHover) {
    return aTrackColor;
  }
  float luminance = RelativeLuminanceUtils::Compute(aTrackColor);
  if (isActive) {
    if (luminance >= 0.18f) {
      luminance *= 0.134f;
    } else {
      luminance /= 0.134f;
      luminance = std::min(luminance, 1.0f);
    }
  } else {
    if (luminance >= 0.18f) {
      luminance *= 0.805f;
    } else {
      luminance /= 0.805f;
    }
  }
  return RelativeLuminanceUtils::Adjust(aTrackColor, luminance);
}

Maybe<nscolor> ScrollbarDrawing::GetScrollbarArrowColor(nscolor aButtonColor) {
  // In Windows 10 scrollbar, there are several gray colors used:
  //
  // State  | Background (lum) | Arrow   | Contrast
  // -------+------------------+---------+---------
  // Normal | Gray 240 (87.1%) | Gray 96 |     5.5
  // Hover  | Gray 218 (70.1%) | Black   |    15.0
  // Active | Gray 96  (11.7%) | White   |     6.3
  //
  // Contrast value is computed based on the definition in
  // https://www.w3.org/TR/WCAG20/#contrast-ratiodef
  //
  // This function is written based on these values.

  if (NS_GET_A(aButtonColor) == 0) {
    // If the button color is transparent, because of e.g.
    // scrollbar-color: <something> transparent, then use
    // the thumb color, which is expected to have enough
    // contrast.
    return Nothing();
  }

  float luminance = RelativeLuminanceUtils::Compute(aButtonColor);
  // Color with luminance larger than 0.72 has contrast ratio over 4.6
  // to color with luminance of gray 96, so this value is chosen for
  // this range. It is the luminance of gray 221.
  if (luminance >= 0.72) {
    // ComputeRelativeLuminanceFromComponents(96). That function cannot
    // be constexpr because of std::pow.
    const float GRAY96_LUMINANCE = 0.117f;
    return Some(RelativeLuminanceUtils::Adjust(aButtonColor, GRAY96_LUMINANCE));
  }
  // The contrast ratio of a color to black equals that to white when its
  // luminance is around 0.18, with a contrast ratio ~4.6 to both sides,
  // thus the value below. It's the lumanince of gray 118.
  //
  // TODO(emilio): Maybe the button alpha is not the best thing to use here and
  // we should use the thumb alpha? It seems weird that the color of the arrow
  // depends on the opacity of the scrollbar thumb...
  if (luminance >= 0.18) {
    return Some(NS_RGBA(0, 0, 0, NS_GET_A(aButtonColor)));
  }
  return Some(NS_RGBA(255, 255, 255, NS_GET_A(aButtonColor)));
}

std::pair<sRGBColor, sRGBColor> ScrollbarDrawing::ComputeScrollbarButtonColors(
    nsIFrame* aFrame, StyleAppearance aAppearance, const ComputedStyle& aStyle,
    const ElementState& aElementState, const Colors& aColors) {
  if (aColors.HighContrast()) {
    if (aElementState.HasAtLeastOneOfStates(ElementState::ACTIVE |
                                            ElementState::HOVER)) {
      return aColors.SystemPair(StyleSystemColor::Selecteditem,
                                StyleSystemColor::Buttonface);
    }
    return aColors.SystemPair(StyleSystemColor::Window,
                              StyleSystemColor::Windowtext);
  }

  auto trackColor = ComputeScrollbarTrackColor(aFrame, aStyle, aColors);
  nscolor buttonColor =
      GetScrollbarButtonColor(trackColor.ToABGR(), aElementState);
  auto arrowColor = GetScrollbarArrowColor(buttonColor)
                        .map(sRGBColor::FromABGR)
                        .valueOrFrom([&] {
                          return ComputeScrollbarThumbColor(
                              aFrame, aStyle, aElementState, aColors);
                        });
  return {sRGBColor::FromABGR(buttonColor), arrowColor};
}

bool ScrollbarDrawing::PaintScrollbarButton(
    DrawTarget& aDrawTarget, StyleAppearance aAppearance,
    const LayoutDeviceRect& aRect, ScrollbarKind aScrollbarKind,
    nsIFrame* aFrame, const ComputedStyle& aStyle,
    const ElementState& aElementState, const Colors& aColors,
    const DPIRatio& aDpiRatio) {
  auto [buttonColor, arrowColor] = ComputeScrollbarButtonColors(
      aFrame, aAppearance, aStyle, aElementState, aColors);
  auto borderColor = aColors.System(StyleSystemColor::Buttontext);
  // Draw an outline around the scrollbar in high contrast mode
  auto borderWidth = aColors.HighContrast() ? CSSCoord(1.0f) : CSSCoord(0.0f);
  ThemeDrawing::PaintRoundedRectWithRadius(
      aDrawTarget, aRect, buttonColor, borderColor, borderWidth, 0, aDpiRatio);
  // Start with Up arrow.
  float arrowPolygonX[] = {-4.0f, 0.0f, 4.0f, 4.0f, 0.0f, -4.0f};
  float arrowPolygonY[] = {0.0f, -4.0f, 0.0f, 3.0f, -1.0f, 3.0f};

  const float kPolygonSize = 17;

  const int32_t arrowNumPoints = std::size(arrowPolygonX);
  switch (aAppearance) {
    case StyleAppearance::ScrollbarbuttonUp:
      break;
    case StyleAppearance::ScrollbarbuttonDown:
      for (float& y : arrowPolygonY) {
        y *= -1;
      }
      break;
    case StyleAppearance::ScrollbarbuttonLeft:
      for (int32_t i = 0; i < arrowNumPoints; i++) {
        float temp = arrowPolygonX[i];
        arrowPolygonX[i] = arrowPolygonY[i];
        arrowPolygonY[i] = temp;
      }
      break;
    case StyleAppearance::ScrollbarbuttonRight:
      for (int32_t i = 0; i < arrowNumPoints; i++) {
        float temp = arrowPolygonX[i];
        arrowPolygonX[i] = arrowPolygonY[i] * -1;
        arrowPolygonY[i] = temp;
      }
      break;
    default:
      return false;
  }
  ThemeDrawing::PaintArrow(aDrawTarget, aRect, arrowPolygonX, arrowPolygonY,
                           kPolygonSize, arrowNumPoints, arrowColor);

  return true;
}

}  // namespace mozilla::widget
