/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 2; -*- */
/* vim: set sw=2 ts=8 et tw=80 : */
/* 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 "ScrollbarDrawingCocoa.h"

#include "mozilla/gfx/Helpers.h"
#include "mozilla/RelativeLuminanceUtils.h"
#include "mozilla/StaticPrefs_widget.h"
#include "nsContainerFrame.h"
#include "nsAlgorithm.h"
#include "nsIFrame.h"
#include "nsLayoutUtils.h"
#include "nsLookAndFeel.h"
#include "nsNativeTheme.h"

using namespace mozilla::gfx;
namespace mozilla::widget {

using ScrollbarKind = ScrollbarDrawing::ScrollbarKind;

struct ColoredRect {
  LayoutDeviceRect mRect;
  nscolor mColor = 0;
};

// The caller can draw this rectangle with rounded corners as appropriate.
struct ThumbRect {
  LayoutDeviceRect mRect;
  nscolor mFillColor = 0;
  nscolor mStrokeColor = 0;
  float mStrokeWidth = 0.0f;
  float mStrokeOutset = 0.0f;
};

using ScrollbarTrackRects = Array<ColoredRect, 4>;
using ScrollCornerRects = Array<ColoredRect, 7>;

struct ScrollbarParams {
  bool isOverlay = false;
  bool isRolledOver = false;
  bool isSmall = false;
  bool isHorizontal = false;
  bool isRtl = false;
  bool isDark = false;
  bool isCustom = false;
  // Two colors only used when custom is true.
  nscolor trackColor = NS_RGBA(0, 0, 0, 0);
  nscolor faceColor = NS_RGBA(0, 0, 0, 0);
};

static ScrollbarParams ComputeScrollbarParams(nsIFrame* aFrame,
                                              const ComputedStyle& aStyle,
                                              const ThemeColors& aColors,
                                              ScrollbarKind aScrollbarKind) {
  ScrollbarParams params;
  params.isOverlay =
      nsLookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars) != 0;
  params.isRolledOver = ScrollbarDrawing::IsParentScrollbarRolledOver(aFrame);
  params.isSmall =
      aStyle.StyleUIReset()->ScrollbarWidth() == StyleScrollbarWidth::Thin;
  params.isRtl = aScrollbarKind == ScrollbarKind::VerticalLeft;
  params.isHorizontal = aScrollbarKind == ScrollbarKind::Horizontal;
  params.isDark = aColors.IsDark();

  const nsStyleUI* ui = aStyle.StyleUI();
  if (ui->HasCustomScrollbars()) {
    const auto& colors = ui->mScrollbarColor.AsColors();
    params.isCustom = true;
    params.trackColor = colors.track.CalcColor(aStyle);
    params.faceColor = colors.thumb.CalcColor(aStyle);
  }

  return params;
}

LayoutDeviceIntSize ScrollbarDrawingCocoa::GetMinimumWidgetSize(
    nsPresContext* aPresContext, StyleAppearance aAppearance,
    nsIFrame* aFrame) {
  MOZ_ASSERT(nsNativeTheme::IsWidgetScrollbarPart(aAppearance));

  auto minSize = [&]() -> CSSIntSize {
    switch (aAppearance) {
      case StyleAppearance::ScrollbarthumbHorizontal:
        return {26, 0};
      case StyleAppearance::ScrollbarthumbVertical:
        return {0, 26};
      case StyleAppearance::ScrollbarVertical:
      case StyleAppearance::ScrollbarHorizontal:
      case StyleAppearance::ScrollbartrackVertical:
      case StyleAppearance::ScrollbartrackHorizontal: {
        ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aFrame);
        auto scrollbarWidth = style->StyleUIReset()->ScrollbarWidth();
        auto size = GetCSSScrollbarSize(
            scrollbarWidth, Overlay(aPresContext->UseOverlayScrollbars()));
        return {size, size};
      }
      case StyleAppearance::ScrollbarbuttonUp:
      case StyleAppearance::ScrollbarbuttonDown:
        return {15, 16};
      case StyleAppearance::ScrollbarbuttonLeft:
      case StyleAppearance::ScrollbarbuttonRight:
        return {16, 15};
      default:
        return {};
    }
  }();

  auto dpi = GetDPIRatioForScrollbarPart(aPresContext);
  return LayoutDeviceIntSize::Round(CSSSize(minSize) * dpi);
}

static ThumbRect GetThumbRect(const LayoutDeviceRect& aRect,
                              const ScrollbarParams& aParams, float aScale) {
  // Compute the thumb thickness. This varies based on aParams.small,
  // aParams.overlay and aParams.rolledOver. non-overlay: 6 / 8, overlay
  // non-hovered: 5 / 7, overlay hovered: 9 / 11
  float thickness = aParams.isSmall ? 6.0f : 8.0f;
  if (aParams.isOverlay) {
    thickness -= 1.0f;
    if (aParams.isRolledOver) {
      thickness += 4.0f;
    }
  }
  thickness *= aScale;

  // Compute the thumb rect.
  const float outerSpacing =
      ((aParams.isOverlay || aParams.isSmall) ? 1.0f : 2.0f) * aScale;
  LayoutDeviceRect thumbRect = aRect;
  thumbRect.Deflate(1.0f * aScale);
  if (aParams.isHorizontal) {
    float bottomEdge = thumbRect.YMost() - outerSpacing;
    thumbRect.SetBoxY(bottomEdge - thickness, bottomEdge);
  } else {
    if (aParams.isRtl) {
      float leftEdge = thumbRect.X() + outerSpacing;
      thumbRect.SetBoxX(leftEdge, leftEdge + thickness);
    } else {
      float rightEdge = thumbRect.XMost() - outerSpacing;
      thumbRect.SetBoxX(rightEdge - thickness, rightEdge);
    }
  }

  // Compute the thumb fill color.
  nscolor faceColor;
  if (aParams.isCustom) {
    faceColor = aParams.faceColor;
  } else {
    if (aParams.isOverlay) {
      faceColor =
          aParams.isDark ? NS_RGBA(255, 255, 255, 128) : NS_RGBA(0, 0, 0, 128);
    } else if (aParams.isDark) {
      faceColor = aParams.isRolledOver ? NS_RGBA(158, 158, 158, 255)
                                       : NS_RGBA(117, 117, 117, 255);
    } else {
      faceColor = aParams.isRolledOver ? NS_RGBA(125, 125, 125, 255)
                                       : NS_RGBA(194, 194, 194, 255);
    }
  }

  nscolor strokeColor = 0;
  float strokeOutset = 0.0f;
  float strokeWidth = 0.0f;

  // Overlay scrollbars have an additional stroke around the fill.
  if (aParams.isOverlay) {
    // For the default alpha of 128 we want to end up with 48 in the outline.
    constexpr float kAlphaScaling = 48.0f / 128.0f;
    const uint8_t strokeAlpha =
        uint8_t(clamped(NS_GET_A(faceColor) * kAlphaScaling, 0.0f, 48.0f));
    if (strokeAlpha) {
      strokeOutset = (aParams.isDark ? 0.3f : 0.5f) * aScale;
      strokeWidth = (aParams.isDark ? 0.6f : 0.8f) * aScale;

      strokeColor = aParams.isDark ? NS_RGBA(0, 0, 0, strokeAlpha)
                                   : NS_RGBA(255, 255, 255, strokeAlpha);
    }
  }

  return {thumbRect, faceColor, strokeColor, strokeWidth, strokeOutset};
}

struct ScrollbarTrackDecorationColors {
  nscolor mInnerColor = 0;
  nscolor mShadowColor = 0;
  nscolor mOuterColor = 0;
};

static ScrollbarTrackDecorationColors ComputeScrollbarTrackDecorationColors(
    nscolor aTrackColor) {
  ScrollbarTrackDecorationColors result;
  float luminance = RelativeLuminanceUtils::Compute(aTrackColor);
  if (luminance >= 0.5f) {
    result.mInnerColor =
        RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.836f);
    result.mShadowColor =
        RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.982f);
    result.mOuterColor =
        RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 0.886f);
  } else {
    result.mInnerColor =
        RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.196f);
    result.mShadowColor =
        RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.018f);
    result.mOuterColor =
        RelativeLuminanceUtils::Adjust(aTrackColor, luminance * 1.129f);
  }
  return result;
}

static bool GetScrollbarTrackRects(const LayoutDeviceRect& aRect,
                                   const ScrollbarParams& aParams, float aScale,
                                   ScrollbarTrackRects& aRects) {
  if (aParams.isOverlay && !aParams.isRolledOver) {
    // Non-hovered overlay scrollbars don't have a track. Draw nothing.
    return false;
  }

  nscolor trackColor;
  if (aParams.isCustom) {
    trackColor = aParams.trackColor;
  } else {
    if (aParams.isOverlay) {
      trackColor = aParams.isDark ? NS_RGBA(201, 201, 201, 38)
                                  : NS_RGBA(250, 250, 250, 191);
    } else {
      trackColor = aParams.isDark ? NS_RGBA(46, 46, 46, 255)
                                  : NS_RGBA(250, 250, 250, 255);
    }
  }

  float thickness = aParams.isHorizontal ? aRect.height : aRect.width;

  // The scrollbar track is drawn as multiple non-overlapping segments, which
  // make up lines of different widths and with slightly different shading.
  ScrollbarTrackDecorationColors colors =
      ComputeScrollbarTrackDecorationColors(trackColor);
  struct {
    nscolor color;
    float thickness;
  } segments[] = {
      {colors.mInnerColor, 1.0f * aScale},
      {colors.mShadowColor, 1.0f * aScale},
      {trackColor, thickness - 3.0f * aScale},
      {colors.mOuterColor, 1.0f * aScale},
  };

  // Iterate over the segments "from inside to outside" and fill each segment.
  // For horizontal scrollbars, iterate top to bottom.
  // For vertical scrollbars, iterate left to right or right to left based on
  // aParams.isRtl.
  auto current = aRects.begin();
  float accumulatedThickness = 0.0f;
  for (const auto& segment : segments) {
    LayoutDeviceRect segmentRect = aRect;
    float startThickness = accumulatedThickness;
    float endThickness = startThickness + segment.thickness;
    if (aParams.isHorizontal) {
      segmentRect.SetBoxY(aRect.Y() + startThickness, aRect.Y() + endThickness);
    } else {
      if (aParams.isRtl) {
        segmentRect.SetBoxX(aRect.XMost() - endThickness,
                            aRect.XMost() - startThickness);
      } else {
        segmentRect.SetBoxX(aRect.X() + startThickness,
                            aRect.X() + endThickness);
      }
    }
    accumulatedThickness = endThickness;
    *current++ = {segmentRect, segment.color};
  }

  return true;
}

static bool GetScrollCornerRects(const LayoutDeviceRect& aRect,
                                 const ScrollbarParams& aParams, float aScale,
                                 ScrollCornerRects& aRects) {
  if (aParams.isOverlay && !aParams.isRolledOver) {
    // Non-hovered overlay scrollbars don't have a corner. Draw nothing.
    return false;
  }

  // Draw the following scroll corner.
  //
  //        Output:                      Rectangles:
  // +---+---+----------+---+     +---+---+----------+---+
  // | I | S | T ...  T | O |     | I | S | T ...  T | O |
  // +---+   |          |   |     +---+---+          |   |
  // | S   S | T ...  T |   |     | S   S | T ...  T | . |
  // +-------+          | . |     +-------+----------+ . |
  // | T      ...     T | . |     | T      ...     T | . |
  // | .              . | . |     | .              . |   |
  // | T      ...     T |   |     | T      ...     T | O |
  // +------------------+   |     +------------------+---+
  // | O       ...        O |     | O       ...        O |
  // +----------------------+     +----------------------+

  float width = aRect.width;
  float height = aRect.height;
  nscolor trackColor;
  if (aParams.isCustom) {
    trackColor = aParams.trackColor;
  } else {
    trackColor =
        aParams.isDark ? NS_RGBA(46, 46, 46, 255) : NS_RGBA(250, 250, 250, 255);
  }
  ScrollbarTrackDecorationColors colors =
      ComputeScrollbarTrackDecorationColors(trackColor);
  struct {
    nscolor color;
    LayoutDeviceRect relativeRect;
  } pieces[] = {
      {colors.mInnerColor, {0.0f, 0.0f, 1.0f * aScale, 1.0f * aScale}},
      {colors.mShadowColor,
       {1.0f * aScale, 0.0f, 1.0f * aScale, 1.0f * aScale}},
      {colors.mShadowColor,
       {0.0f, 1.0f * aScale, 2.0f * aScale, 1.0f * aScale}},
      {trackColor, {2.0f * aScale, 0.0f, width - 3.0f * aScale, 2.0f * aScale}},
      {trackColor,
       {0.0f, 2.0f * aScale, width - 1.0f * aScale, height - 3.0f * aScale}},
      {colors.mOuterColor,
       {width - 1.0f * aScale, 0.0f, 1.0f * aScale, height - 1.0f * aScale}},
      {colors.mOuterColor,
       {0.0f, height - 1.0f * aScale, width, 1.0f * aScale}},
  };

  auto current = aRects.begin();
  for (const auto& piece : pieces) {
    LayoutDeviceRect pieceRect = piece.relativeRect + aRect.TopLeft();
    if (aParams.isRtl) {
      pieceRect.x = aRect.XMost() - piece.relativeRect.XMost();
    }
    *current++ = {pieceRect, piece.color};
  }
  return true;
}

template <typename PaintBackendData>
void ScrollbarDrawingCocoa::DoPaintScrollbarThumb(
    PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const ElementState& aElementState, const DocumentState& aDocumentState,
    const Colors& aColors, const DPIRatio& aDpiRatio) {
  ScrollbarParams params =
      ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
  auto thumb = GetThumbRect(aRect, params, aDpiRatio.scale);
  LayoutDeviceCoord radius =
      (params.isHorizontal ? thumb.mRect.Height() : thumb.mRect.Width()) / 2.0f;
  ThemeDrawing::PaintRoundedRectWithRadius(
      aPaintData, thumb.mRect, thumb.mRect,
      sRGBColor::FromABGR(thumb.mFillColor), sRGBColor::White(0.0f), 0.0f,
      radius / aDpiRatio, aDpiRatio);
  if (!thumb.mStrokeColor) {
    return;
  }

  // Paint the stroke if needed.
  auto strokeRect = thumb.mRect;
  strokeRect.Inflate(thumb.mStrokeOutset + thumb.mStrokeWidth);
  radius =
      (params.isHorizontal ? strokeRect.Height() : strokeRect.Width()) / 2.0f;
  ThemeDrawing::PaintRoundedRectWithRadius(
      aPaintData, strokeRect, sRGBColor::White(0.0f),
      sRGBColor::FromABGR(thumb.mStrokeColor), thumb.mStrokeWidth,
      radius / aDpiRatio, aDpiRatio);
}

bool ScrollbarDrawingCocoa::PaintScrollbarThumb(
    DrawTarget& aDt, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const ElementState& aElementState, const DocumentState& aDocumentState,
    const Colors& aColors, const DPIRatio& aDpiRatio) {
  DoPaintScrollbarThumb(aDt, aRect, aScrollbarKind, aFrame, aStyle,
                        aElementState, aDocumentState, aColors, aDpiRatio);
  return true;
}

bool ScrollbarDrawingCocoa::PaintScrollbarThumb(
    WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const ElementState& aElementState, const DocumentState& aDocumentState,
    const Colors& aColors, const DPIRatio& aDpiRatio) {
  DoPaintScrollbarThumb(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
                        aElementState, aDocumentState, aColors, aDpiRatio);
  return true;
}

template <typename PaintBackendData>
void ScrollbarDrawingCocoa::DoPaintScrollbarTrack(
    PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const DocumentState& aDocumentState, const Colors& aColors,
    const DPIRatio& aDpiRatio) {
  ScrollbarParams params =
      ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
  ScrollbarTrackRects rects;
  if (GetScrollbarTrackRects(aRect, params, aDpiRatio.scale, rects)) {
    for (const auto& rect : rects) {
      ThemeDrawing::FillRect(aPaintData, rect.mRect,
                             sRGBColor::FromABGR(rect.mColor));
    }
  }
}

bool ScrollbarDrawingCocoa::PaintScrollbarTrack(
    DrawTarget& aDt, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const DocumentState& aDocumentState, const Colors& aColors,
    const DPIRatio& aDpiRatio) {
  DoPaintScrollbarTrack(aDt, aRect, aScrollbarKind, aFrame, aStyle,
                        aDocumentState, aColors, aDpiRatio);
  return true;
}

bool ScrollbarDrawingCocoa::PaintScrollbarTrack(
    WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const DocumentState& aDocumentState, const Colors& aColors,
    const DPIRatio& aDpiRatio) {
  DoPaintScrollbarTrack(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
                        aDocumentState, aColors, aDpiRatio);
  return true;
}

template <typename PaintBackendData>
void ScrollbarDrawingCocoa::DoPaintScrollCorner(
    PaintBackendData& aPaintData, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const DocumentState& aDocumentState, const Colors& aColors,
    const DPIRatio& aDpiRatio) {
  ScrollbarParams params =
      ComputeScrollbarParams(aFrame, aStyle, aColors, aScrollbarKind);
  ScrollCornerRects rects;
  if (GetScrollCornerRects(aRect, params, aDpiRatio.scale, rects)) {
    for (const auto& rect : rects) {
      ThemeDrawing::FillRect(aPaintData, rect.mRect,
                             sRGBColor::FromABGR(rect.mColor));
    }
  }
}

bool ScrollbarDrawingCocoa::PaintScrollCorner(
    DrawTarget& aDt, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const DocumentState& aDocumentState, const Colors& aColors,
    const DPIRatio& aDpiRatio) {
  DoPaintScrollCorner(aDt, aRect, aScrollbarKind, aFrame, aStyle,
                      aDocumentState, aColors, aDpiRatio);
  return true;
}

bool ScrollbarDrawingCocoa::PaintScrollCorner(
    WebRenderBackendData& aWrData, const LayoutDeviceRect& aRect,
    ScrollbarKind aScrollbarKind, nsIFrame* aFrame, const ComputedStyle& aStyle,
    const DocumentState& aDocumentState, const Colors& aColors,
    const DPIRatio& aDpiRatio) {
  DoPaintScrollCorner(aWrData, aRect, aScrollbarKind, aFrame, aStyle,
                      aDocumentState, aColors, aDpiRatio);
  return true;
}

void ScrollbarDrawingCocoa::RecomputeScrollbarParams() {
  // FIXME(emilio): This doesn't respect the
  // StaticPrefs::widget_non_native_theme_scrollbar_size_override() pref;
  ConfigureScrollbarSize(15);  // Just in case, for future-proofing
  ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::No, 15);
  ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::No, 11);
  ConfigureScrollbarSize(StyleScrollbarWidth::Auto, Overlay::Yes, 16);
  ConfigureScrollbarSize(StyleScrollbarWidth::Thin, Overlay::Yes, 14);
}

}  // namespace mozilla::widget
