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

#include "mozilla/AppUnits.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/StaticPrefs_print.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/intl/Segmenter.h"
#include "gfxContext.h"
#include "nsDeviceContext.h"
#include "nsFontMetrics.h"
#include "nsIFrame.h"
#include "nsLayoutUtils.h"
#include "nsPresContext.h"
#include "nsGkAtoms.h"
#include "nsFieldSetFrame.h"
#include "nsPageContentFrame.h"
#include "nsDisplayList.h"
#include "nsPageSequenceFrame.h"  // for nsSharedPageData
#include "nsTextFormatter.h"      // for page number localization formatting
#include "nsBidiUtils.h"
#include "nsIPrintSettings.h"
#include "PrintedSheetFrame.h"

#include "mozilla/Logging.h"
extern mozilla::LazyLogModule gLayoutPrintingLog;
#define PR_PL(_p1) MOZ_LOG(gLayoutPrintingLog, mozilla::LogLevel::Debug, _p1)

using namespace mozilla;
using namespace mozilla::gfx;

nsPageFrame* NS_NewPageFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
  return new (aPresShell) nsPageFrame(aStyle, aPresShell->GetPresContext());
}

NS_IMPL_FRAMEARENA_HELPERS(nsPageFrame)

NS_QUERYFRAME_HEAD(nsPageFrame)
  NS_QUERYFRAME_ENTRY(nsPageFrame)
NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)

nsPageFrame::nsPageFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
    : nsContainerFrame(aStyle, aPresContext, kClassID) {}

nsPageFrame::~nsPageFrame() = default;

nsReflowStatus nsPageFrame::ReflowPageContent(
    nsPresContext* aPresContext, const ReflowInput& aPageReflowInput) {
  nsPageContentFrame* const frame = PageContentFrame();
  // If this is the first page, it won't have its page name and computed style
  // set yet. Before reflow, make sure that page name and computed style have
  // been applied.
  frame->EnsurePageName();
  // XXX Pay attention to the page's border and padding...
  //
  // Reflow our ::-moz-page-content frame, allowing it only to be as big as we
  // are (minus margins).
  const nsSize pageSize = ComputePageSize();
  // Scaling applied to the page in the single page-per-sheet case (used for
  // down-scaling when the page is too large to fit on the sheet we are printing
  // on). In the single page-per-sheet case, we need this here to preemptively
  // increase the margins by the same amount that the scaling will reduce them
  // in order to make sure that their physical size is unchanged (particularly
  // important for the unwriteable margins).
  const auto* ppsInfo = GetSharedPageData()->PagesPerSheetInfo();
  const float pageSizeScale =
      ppsInfo->mNumPages == 1 ? ComputeSinglePPSPageSizeScale(pageSize) : 1.0f;
  // Scaling applied to content, as given by the print UI.
  // This is an additional scale factor that is applied to the content in the
  // nsPageContentFrame.
  const float extraContentScale = aPresContext->GetPageScale();
  // Size for the page content. This will be scaled by extraContentScale, and
  // is used to calculate the computed size of the nsPageContentFrame content
  // by subtracting margins.
  nsSize availableSpace = pageSize;

  // When the reflow size is NS_UNCONSTRAINEDSIZE it means we are reflowing
  // a single page to print selection. So this means we want to use
  // NS_UNCONSTRAINEDSIZE without altering it.
  //
  // FIXME(emilio): Is this still true?
  availableSpace.width =
      NSToCoordCeil(availableSpace.width / extraContentScale);
  if (availableSpace.height != NS_UNCONSTRAINEDSIZE) {
    availableSpace.height =
        NSToCoordCeil(availableSpace.height / extraContentScale);
  }

  // Get the number of Twips per pixel from the PresContext
  const nscoord onePixel = AppUnitsPerCSSPixel();

  // insurance against infinite reflow, when reflowing less than a pixel
  // XXX Shouldn't we do something more friendly when invalid margins
  //     are set?
  if (availableSpace.width < onePixel || availableSpace.height < onePixel) {
    NS_WARNING("Reflow aborted; no space for content");
    return {};
  }

  ReflowInput kidReflowInput(
      aPresContext, aPageReflowInput, frame,
      LogicalSize(frame->GetWritingMode(), availableSpace));
  kidReflowInput.mFlags.mIsTopOfPage = true;
  kidReflowInput.mFlags.mTableIsSplittable = true;

  nsMargin defaultMargins = aPresContext->GetDefaultPageMargin();
  // The default margins are in the coordinate space of the physical paper.
  // Scale them by the pageSizeScale to convert them to the content coordinate
  // space.
  for (const auto side : mozilla::AllPhysicalSides()) {
    defaultMargins.Side(side) =
        NSToCoordRound((float)defaultMargins.Side(side) / pageSizeScale);
  }
  mPageContentMargin = defaultMargins;

  // Use the margins given in the @page rule if told to do so.
  // We clamp to the paper's unwriteable margins to avoid clipping, *except*
  // that we will respect a margin of zero if specified, assuming this means
  // the document is intended to fit the paper size exactly, and the client is
  // taking full responsibility for what happens around the edges.
  if (mPD->mPrintSettings->GetHonorPageRuleMargins()) {
    const auto& margin = kidReflowInput.mStyleMargin->mMargin;
    for (const auto side : mozilla::AllPhysicalSides()) {
      if (!margin.Get(side).IsAuto()) {
        // Computed margins are already in the coordinate space of the content,
        // do not scale.
        const nscoord computed =
            kidReflowInput.ComputedPhysicalMargin().Side(side);
        // Respecting a zero margin is particularly important when the client
        // is PDF.js where the PDF already contains the margins.
        // User could also be asking to ignore unwriteable margins (Though
        // currently, it is impossible through the print UI to set both
        // `HonorPageRuleMargins` and `IgnoreUnwriteableMargins`).
        if (computed == 0 ||
            mPD->mPrintSettings->GetIgnoreUnwriteableMargins()) {
          mPageContentMargin.Side(side) = computed;
        } else {
          // Unwriteable margins are in the coordinate space of the physical
          // paper. Scale them by the pageSizeScale to convert them to the
          // content coordinate space.
          const int32_t unwriteableTwips =
              mPD->mPrintSettings->GetUnwriteableMarginInTwips().Side(side);
          const nscoord unwriteable = nsPresContext::CSSTwipsToAppUnits(
              (float)unwriteableTwips / pageSizeScale);
          mPageContentMargin.Side(side) = std::max(
              kidReflowInput.ComputedPhysicalMargin().Side(side), unwriteable);
        }
      }
    }
  }

  // TODO: This seems odd that we need to scale the margins by the extra
  // scale factor, but this is needed for correct margins.
  // Why are the margins already scaled? Shouldn't they be stored so that this
  // scaling factor would be redundant?
  nscoord computedWidth =
      availableSpace.width - mPageContentMargin.LeftRight() / extraContentScale;
  nscoord computedHeight;
  if (availableSpace.height == NS_UNCONSTRAINEDSIZE) {
    computedHeight = NS_UNCONSTRAINEDSIZE;
  } else {
    computedHeight = availableSpace.height -
                     mPageContentMargin.TopBottom() / extraContentScale;
  }

  // Check the width and height, if they're too small we reset the margins
  // back to the default.
  if (computedWidth < onePixel || computedHeight < onePixel) {
    mPageContentMargin = defaultMargins;
    computedWidth = availableSpace.width -
                    mPageContentMargin.LeftRight() / extraContentScale;
    if (computedHeight != NS_UNCONSTRAINEDSIZE) {
      computedHeight = availableSpace.height -
                       mPageContentMargin.TopBottom() / extraContentScale;
    }
    // And if they're still too small, we give up.
    if (computedWidth < onePixel || computedHeight < onePixel) {
      NS_WARNING("Reflow aborted; no space for content");
      return {};
    }
  }

  kidReflowInput.SetComputedWidth(computedWidth);
  kidReflowInput.SetComputedHeight(computedHeight);

  // calc location of frame
  const nscoord xc = mPageContentMargin.left;
  const nscoord yc = mPageContentMargin.top;

  // Get the child's desired size
  ReflowOutput kidOutput(kidReflowInput);
  nsReflowStatus kidStatus;
  ReflowChild(frame, aPresContext, kidOutput, kidReflowInput, xc, yc,
              ReflowChildFlags::Default, kidStatus);

  // Place and size the child
  FinishReflowChild(frame, aPresContext, kidOutput, &kidReflowInput, xc, yc,
                    ReflowChildFlags::Default);

  NS_ASSERTION(!kidStatus.IsFullyComplete() || !frame->GetNextInFlow(),
               "bad child flow list");
  return kidStatus;
}

void nsPageFrame::Reflow(nsPresContext* aPresContext,
                         ReflowOutput& aReflowOutput,
                         const ReflowInput& aReflowInput,
                         nsReflowStatus& aStatus) {
  MarkInReflow();
  DO_GLOBAL_REFLOW_COUNT("nsPageFrame");
  MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
  MOZ_ASSERT(mPD, "Need a pointer to nsSharedPageData before reflow starts");

  // Our status is the same as our child's.
  aStatus = ReflowPageContent(aPresContext, aReflowInput);

  PR_PL(("PageFrame::Reflow %p ", this));
  PR_PL(("[%d,%d][%d,%d]\n", aReflowOutput.Width(), aReflowOutput.Height(),
         aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight()));

  // Return our desired size
  WritingMode wm = aReflowInput.GetWritingMode();
  aReflowOutput.ISize(wm) = aReflowInput.AvailableISize();
  if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) {
    aReflowOutput.BSize(wm) = aReflowInput.AvailableBSize();
  }

  aReflowOutput.SetOverflowAreasToDesiredBounds();
  FinishAndStoreOverflow(&aReflowOutput);

  PR_PL(("PageFrame::Reflow %p ", this));
  PR_PL(("[%d,%d]\n", aReflowInput.AvailableWidth(),
         aReflowInput.AvailableHeight()));
}

#ifdef DEBUG_FRAME_DUMP
nsresult nsPageFrame::GetFrameName(nsAString& aResult) const {
  return MakeFrameName(u"Page"_ns, aResult);
}
#endif

void nsPageFrame::ProcessSpecialCodes(const nsString& aStr, nsString& aNewStr) {
  aNewStr = aStr;

  // Search to see if the &D code is in the string
  // then subst in the current date/time
  constexpr auto kDate = u"&D"_ns;
  if (aStr.Find(kDate) != kNotFound) {
    aNewStr.ReplaceSubstring(kDate, mPD->mDateTimeStr);
  }

  // NOTE: Must search for &PT before searching for &P
  //
  // Search to see if the "page number and page" total code are in the string
  // and replace the page number and page total code with the actual
  // values
  constexpr auto kPageAndTotal = u"&PT"_ns;
  if (aStr.Find(kPageAndTotal) != kNotFound) {
    nsAutoString uStr;
    nsTextFormatter::ssprintf(uStr, mPD->mPageNumAndTotalsFormat.get(),
                              mPageNum, mPD->mRawNumPages);
    aNewStr.ReplaceSubstring(kPageAndTotal, uStr);
  }

  // Search to see if the page number code is in the string
  // and replace the page number code with the actual value
  constexpr auto kPage = u"&P"_ns;
  if (aStr.Find(kPage) != kNotFound) {
    nsAutoString uStr;
    nsTextFormatter::ssprintf(uStr, mPD->mPageNumFormat.get(), mPageNum);
    aNewStr.ReplaceSubstring(kPage, uStr);
  }

  constexpr auto kTitle = u"&T"_ns;
  if (aStr.Find(kTitle) != kNotFound) {
    aNewStr.ReplaceSubstring(kTitle, mPD->mDocTitle);
  }

  constexpr auto kDocURL = u"&U"_ns;
  if (aStr.Find(kDocURL) != kNotFound) {
    aNewStr.ReplaceSubstring(kDocURL, mPD->mDocURL);
  }

  constexpr auto kPageTotal = u"&L"_ns;
  if (aStr.Find(kPageTotal) != kNotFound) {
    nsAutoString uStr;
    nsTextFormatter::ssprintf(uStr, mPD->mPageNumFormat.get(),
                              mPD->mRawNumPages);
    aNewStr.ReplaceSubstring(kPageTotal, uStr);
  }
}

//------------------------------------------------------------------------------
nscoord nsPageFrame::GetXPosition(gfxContext& aRenderingContext,
                                  nsFontMetrics& aFontMetrics,
                                  const nsRect& aRect, int32_t aJust,
                                  const nsString& aStr) {
  nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(
      aStr, this, aFontMetrics, aRenderingContext);
  nscoord x = aRect.x;
  switch (aJust) {
    case nsIPrintSettings::kJustLeft:
      x += mPD->mEdgePaperMargin.left;
      break;

    case nsIPrintSettings::kJustCenter:
      x += (aRect.width - width) / 2;
      break;

    case nsIPrintSettings::kJustRight:
      x += aRect.width - width - mPD->mEdgePaperMargin.right;
      break;
  }  // switch

  return x;
}

// Draw a header or footer
// @param aRenderingContext - rendering content to draw into
// @param aHeaderFooter - indicates whether it is a header or footer
// @param aStrLeft - string for the left header or footer; can be empty
// @param aStrCenter - string for the center header or footer; can be empty
// @param aStrRight - string for the right header or footer; can be empty
// @param aRect - the rect of the page
// @param aAscent - the ascent of the font
// @param aHeight - the height of the font
void nsPageFrame::DrawHeaderFooter(
    gfxContext& aRenderingContext, nsFontMetrics& aFontMetrics,
    nsHeaderFooterEnum aHeaderFooter, const nsString& aStrLeft,
    const nsString& aStrCenter, const nsString& aStrRight, const nsRect& aRect,
    nscoord aAscent, nscoord aHeight) {
  int32_t numStrs = 0;
  if (!aStrLeft.IsEmpty()) numStrs++;
  if (!aStrCenter.IsEmpty()) numStrs++;
  if (!aStrRight.IsEmpty()) numStrs++;

  if (numStrs == 0) return;
  const nscoord contentWidth =
      aRect.width - (mPD->mEdgePaperMargin.left + mPD->mEdgePaperMargin.right);
  const nscoord strSpace = contentWidth / numStrs;

  if (!aStrLeft.IsEmpty()) {
    DrawHeaderFooter(aRenderingContext, aFontMetrics, aHeaderFooter,
                     nsIPrintSettings::kJustLeft, aStrLeft, aRect, aAscent,
                     aHeight, strSpace);
  }
  if (!aStrCenter.IsEmpty()) {
    DrawHeaderFooter(aRenderingContext, aFontMetrics, aHeaderFooter,
                     nsIPrintSettings::kJustCenter, aStrCenter, aRect, aAscent,
                     aHeight, strSpace);
  }
  if (!aStrRight.IsEmpty()) {
    DrawHeaderFooter(aRenderingContext, aFontMetrics, aHeaderFooter,
                     nsIPrintSettings::kJustRight, aStrRight, aRect, aAscent,
                     aHeight, strSpace);
  }
}

// Draw a header or footer string
// @param aRenderingContext - rendering context to draw into
// @param aHeaderFooter - indicates whether it is a header or footer
// @param aJust - indicates where the string is located within the header/footer
// @param aStr - the string to be drawn
// @param aRect - the rect of the page
// @param aHeight - the height of the font
// @param aAscent - the ascent of the font
// @param aWidth - available width for the string
void nsPageFrame::DrawHeaderFooter(gfxContext& aRenderingContext,
                                   nsFontMetrics& aFontMetrics,
                                   nsHeaderFooterEnum aHeaderFooter,
                                   int32_t aJust, const nsString& aStr,
                                   const nsRect& aRect, nscoord aAscent,
                                   nscoord aHeight, nscoord aWidth) {
  DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();

  if ((aHeaderFooter == eHeader && aHeight < mPageContentMargin.top) ||
      (aHeaderFooter == eFooter && aHeight < mPageContentMargin.bottom)) {
    nsAutoString str;
    ProcessSpecialCodes(aStr, str);

    int32_t len = (int32_t)str.Length();
    if (len == 0) {
      return;  // bail is empty string
    }

    int32_t index;
    int32_t textWidth = 0;
    const char16_t* text = str.get();
    // find how much text fits, the "position" is the size of the available area
    if (nsLayoutUtils::BinarySearchForPosition(drawTarget, aFontMetrics, text,
                                               0, 0, 0, len, int32_t(aWidth),
                                               index, textWidth)) {
      if (index < len - 1) {
        // we can't fit in all the text, try to remove 3 glyphs and append
        // three "." charactrers.

        // TODO: This might not actually remove three glyphs in cases where
        // ZWJ sequences, regional indicators, etc are used.
        // We also have guarantee that removing three glyphs will make enough
        // space for the ellipse, if they are zero-width or even just narrower
        // than the "." character.
        // See https://bugzilla.mozilla.org/1765008
        mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16 revIter(str);

        // Start iteration at the point where the text does properly fit.
        revIter.Seek(index);

        // Step backwards 3 times, checking if we have any string left by the
        // end.
        revIter.Next();
        revIter.Next();
        if (const Maybe<uint32_t> maybeIndex = revIter.Next()) {
          // TODO: We should consider checking for the ellipse character, or
          // possibly for another continuation indicator based on
          // localization.
          // See https://bugzilla.mozilla.org/1765007
          str.Truncate(*maybeIndex);
          str.AppendLiteral("...");
        } else {
          // We can only fit 3 or fewer chars.  Just show nothing
          str.Truncate();
        }
      }
    } else {
      return;  // bail if couldn't find the correct length
    }

    if (HasRTLChars(str)) {
      PresContext()->SetBidiEnabled();
    }

    // calc the x and y positions of the text
    nscoord x =
        GetXPosition(aRenderingContext, aFontMetrics, aRect, aJust, str);
    nscoord y;
    if (aHeaderFooter == eHeader) {
      y = aRect.y + mPD->mEdgePaperMargin.top;
    } else {
      y = aRect.YMost() - aHeight - mPD->mEdgePaperMargin.bottom;
    }

    // set up new clip and draw the text
    aRenderingContext.Save();
    aRenderingContext.Clip(NSRectToSnappedRect(
        aRect, PresContext()->AppUnitsPerDevPixel(), *drawTarget));
    aRenderingContext.SetColor(sRGBColor::OpaqueBlack());
    nsLayoutUtils::DrawString(this, aFontMetrics, &aRenderingContext, str.get(),
                              str.Length(), nsPoint(x, y + aAscent), nullptr,
                              DrawStringFlags::ForceHorizontal);
    aRenderingContext.Restore();
  }
}

class nsDisplayHeaderFooter final : public nsPaintedDisplayItem {
 public:
  nsDisplayHeaderFooter(nsDisplayListBuilder* aBuilder, nsPageFrame* aFrame)
      : nsPaintedDisplayItem(aBuilder, aFrame) {
    MOZ_COUNT_CTOR(nsDisplayHeaderFooter);
  }
  MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayHeaderFooter)

  virtual void Paint(nsDisplayListBuilder* aBuilder,
                     gfxContext* aCtx) override {
#ifdef DEBUG
    nsPageFrame* pageFrame = do_QueryFrame(mFrame);
    MOZ_ASSERT(pageFrame, "We should have an nsPageFrame");
#endif
    static_cast<nsPageFrame*>(mFrame)->PaintHeaderFooter(
        *aCtx, ToReferenceFrame(), false);
  }
  NS_DISPLAY_DECL_NAME("HeaderFooter", TYPE_HEADER_FOOTER)

  virtual nsRect GetComponentAlphaBounds(
      nsDisplayListBuilder* aBuilder) const override {
    bool snap;
    return GetBounds(aBuilder, &snap);
  }
};

static void PaintMarginGuides(nsIFrame* aFrame, DrawTarget* aDrawTarget,
                              const nsRect& aDirtyRect, nsPoint aPt) {
  // Set up parameters needed to draw the guides: we draw them in blue,
  // using 2px-long dashes with 2px separation and a line width of 0.5px.
  // Drawing is antialiased, so on a non-hidpi screen where the line width is
  // less than one device pixel, it doesn't disappear but renders fainter
  // than a solid 1px-wide line would be.
  // (In many cases, the entire preview is scaled down so that the guides
  // will be nominally less than 1 dev px even on a hidpi screen, resulting
  // in lighter antialiased rendering so they don't dominate the page.)
  ColorPattern pattern(ToDeviceColor(sRGBColor(0.0f, 0.0f, 1.0f)));
  Float dashes[] = {2.0f, 2.0f};
  StrokeOptions stroke(/* line width (in CSS px) */ 0.5f,
                       JoinStyle::MITER_OR_BEVEL, CapStyle::BUTT,
                       /* mitre limit (default, not used) */ 10.0f,
                       /* set dash pattern of 2px stroke, 2px gap */
                       ArrayLength(dashes), dashes,
                       /* dash offset */ 0.0f);
  DrawOptions options;

  MOZ_RELEASE_ASSERT(aFrame->IsPageFrame());
  const nsMargin& margin =
      static_cast<nsPageFrame*>(aFrame)->GetUsedPageContentMargin();
  int32_t appUnitsPerDevPx = aFrame->PresContext()->AppUnitsPerDevPixel();

  // Get the frame's rect and inset by the margins to get the edges of the
  // content area, where we want to draw the guides.
  // We draw in two stages, first applying the top/bottom margins and drawing
  // the horizontal guides across the full width of the page.
  nsRect rect(aPt, aFrame->GetSize());
  rect.Deflate(nsMargin(margin.top, 0, margin.bottom, 0));
  Rect r = NSRectToRect(rect, appUnitsPerDevPx);
  aDrawTarget->StrokeLine(r.TopLeft(), r.TopRight(), pattern, stroke, options);
  aDrawTarget->StrokeLine(r.BottomLeft(), r.BottomRight(), pattern, stroke,
                          options);

  // Then reset rect, apply the left/right margins, and draw vertical guides
  // extending the full height of the page.
  rect = nsRect(aPt, aFrame->GetSize());
  rect.Deflate(nsMargin(0, margin.right, 0, margin.left));
  r = NSRectToRect(rect, appUnitsPerDevPx);
  aDrawTarget->StrokeLine(r.TopLeft(), r.BottomLeft(), pattern, stroke,
                          options);
  aDrawTarget->StrokeLine(r.TopRight(), r.BottomRight(), pattern, stroke,
                          options);
}

static std::tuple<uint32_t, uint32_t> GetRowAndColFromIdx(uint32_t aIdxOnSheet,
                                                          uint32_t aNumCols) {
  // Compute the row index by *dividing* the item's ordinal position by how
  // many items fit in each row (i.e. the number of columns), and flooring.
  // Compute the column index by getting the remainder of that division:
  // Notably, mNumRows is irrelevant to this computation; that's because
  // we're adding new items column-by-column rather than row-by-row.
  return {aIdxOnSheet / aNumCols, aIdxOnSheet % aNumCols};
}

// The minimum ratio for which we will center the page on the sheet when using
// auto-detect logic.
// Note that this ratio is of the content's size to the sheet size scaled to be
// in content space, and so the actual ratio will always be from 0.0 to 1.0,
// with this marking the smallest ratio we consider a near-miss.
// The ratio of A4 on Letter is 0.915034. A threshold of 0.9 will ensure that
// A4 on Letter works, as well as other near-misses.
//
// The ratio is computed as so:
// scale = min(1, sheetHeight / pageHeight, sheetWidth / pageWidth)
//
// Where pageSize is pageWidth or pageHeight, and sheetSize is sheetWidth or
// sheetHeight, respectively:
// scaledPageSize = pageSize * scale
// ratio = scaledPageSize / sheetSize
//
// A4 (210mm x 297mm) on US Letter (215.9mm x 279.4mm) is derived as so:
// scale = min(1, 215.9 / 210, 279.4 / 297) = 0.9407407407..
//
// Using the widths:
// scaledPageSize = (210 * 0.940741) = 197.556
// ratio = 197.556 / 215.9 = 0.915034
//
// See nsPageFrame::ComputeSinglePPSPageSizeScale for scale calculation, and
// OffsetToCenterPage for ratio calculation.
constexpr float kCenterPageRatioThreshold = 0.9f;

// Numeric values for the pref "print.center_page_on_sheet"
enum {
  kPrintCenterPageOnSheetNever = 0,
  kPrintCenterPageOnSheetAlways = 1,
  kPrintCenterPageOnSheetAuto = 2
};

// Returns an offset to center the page on the sheet, with a given scale.
// When no centering can/should happen, this will avoid extra calculations and
// return 0.0f.
// This takes into account the value of the pref "print.center_page_on_sheet".
static float OffsetToCenterPage(nscoord aContentSize, nscoord aSheetSize,
                                float aScale, float aAppUnitsPerPixel) {
  MOZ_ASSERT(aScale <= 1.0f && aScale > 0.0f,
             "Scale must be in the range (0,1]");
  const unsigned centerPagePref = StaticPrefs::print_center_page_on_sheet();
  if (centerPagePref == kPrintCenterPageOnSheetNever) {
    return 0.0f;
  }

  // Determine the ratio of scaled page to the sheet size.
  const float sheetSize =
      NSAppUnitsToFloatPixels(aSheetSize, aAppUnitsPerPixel);
  const float scaledContentSize =
      NSAppUnitsToFloatPixels(aContentSize, aAppUnitsPerPixel) * aScale;
  const float ratio = scaledContentSize / sheetSize;

  // If the ratio is within the threshold, or the pref indicates we should
  // always center the page, return half the difference to form the offset.
  if (centerPagePref == kPrintCenterPageOnSheetAlways ||
      ratio >= kCenterPageRatioThreshold) {
    return (sheetSize - scaledContentSize) * 0.5f;
  }
  return 0.0f;
}

// Helper for BuildDisplayList:
static gfx::Matrix4x4 ComputePagesPerSheetAndPageSizeTransform(
    const nsIFrame* aFrame, float aAppUnitsPerPixel) {
  MOZ_ASSERT(aFrame->IsPageFrame());
  auto* pageFrame = static_cast<const nsPageFrame*>(aFrame);
  const nsSize contentPageSize = pageFrame->ComputePageSize();
  MOZ_ASSERT(contentPageSize.width > 0 && contentPageSize.height > 0);
  nsSharedPageData* pd = pageFrame->GetSharedPageData();
  const auto* ppsInfo = pd->PagesPerSheetInfo();

  const nsContainerFrame* const parentFrame = pageFrame->GetParent();
  MOZ_ASSERT(parentFrame->IsPrintedSheetFrame(),
             "Parent of nsPageFrame should be PrintedSheetFrame");
  const auto* sheetFrame = static_cast<const PrintedSheetFrame*>(parentFrame);

  const double rotation =
      pageFrame->GetPageOrientationRotation(pageFrame->GetSharedPageData());

  gfx::Matrix4x4 transform;

  if (ppsInfo->mNumPages == 1) {
    const nsSize sheetSize = sheetFrame->GetSizeForChildren();
    if (rotation != 0.0) {
      const bool sheetIsPortrait = sheetSize.width < sheetSize.height;
      const bool rotatingClockwise = rotation > 0.0;

      // rotation point:
      int32_t x, y;
      if (rotatingClockwise != sheetIsPortrait) {
        // rotating portrait clockwise, or landscape counterclockwise
        x = y = std::min(sheetSize.width, sheetSize.height) / 2;
      } else {
        // rotating portrait counterclockwise, or landscape clockwise
        x = y = std::max(sheetSize.width, sheetSize.height) / 2;
      }

      transform = gfx::Matrix4x4::Translation(
          NSAppUnitsToFloatPixels(x, aAppUnitsPerPixel),
          NSAppUnitsToFloatPixels(y, aAppUnitsPerPixel), 0);
      transform.RotateZ(rotation);
      transform.PreTranslate(NSAppUnitsToFloatPixels(-x, aAppUnitsPerPixel),
                             NSAppUnitsToFloatPixels(-y, aAppUnitsPerPixel), 0);
    }

    // If the difference in horizontal size, after scaling, is relatively small
    // then center the page on the sheet.
    const float scale =
        pageFrame->ComputeSinglePPSPageSizeScale(contentPageSize);
    const float centeringOffset = OffsetToCenterPage(
        contentPageSize.width, sheetSize.width, scale, aAppUnitsPerPixel);

    // Only bother with the translation if it is at least one pixel.
    // It's possible for a mismatch in the paper size reported by the print
    // server and the paper size from Gecko to lead to small offsets, or
    // even (in combination with floating point error) a very small negative
    // offset. Do not apply an offset in those cases.
    if (centeringOffset >= 1.0f) {
      transform.PreTranslate(centeringOffset, 0, 0);
    }
    transform.PreScale(scale, scale, 1);
    return transform;
  }

  // The multiple pages-per-sheet case.

  // Begin with the translation of the page to its pages-per-sheet grid "cell"
  // (the grid origin accounts for the sheet's unwriteable margins):
  const nsPoint gridOrigin = sheetFrame->GetGridOrigin();
  const nscoord cellWidth = sheetFrame->GetGridCellWidth();
  const nscoord cellHeight = sheetFrame->GetGridCellHeight();
  uint32_t rowIdx, colIdx;
  std::tie(rowIdx, colIdx) = GetRowAndColFromIdx(pageFrame->IndexOnSheet(),
                                                 sheetFrame->GetGridNumCols());
  transform = gfx::Matrix4x4::Translation(
      NSAppUnitsToFloatPixels(gridOrigin.x + nscoord(colIdx) * cellWidth,
                              aAppUnitsPerPixel),
      NSAppUnitsToFloatPixels(gridOrigin.y + nscoord(rowIdx) * cellHeight,
                              aAppUnitsPerPixel),
      0.0f);

  // Scale the page to fit, centered, in the grid cell:
  float scaleX = float(cellWidth) / float(contentPageSize.width);
  float scaleY = float(cellHeight) / float(contentPageSize.height);
  MOZ_ASSERT(scaleX > 0.0f && scaleX <= 1.0f && scaleY > 0.0f &&
             scaleY <= 1.0f);
  float scale;
  float dx = 0.0f, dy = 0.0f;
  if (scaleX < scaleY) {
    scale = scaleX;
    // We need to scale down more for the width than the height, so we'll have
    // some spare space in the page's vertical direction. We offset the page
    // to share that space equally above and below the page to center it.
    nscoord extraSpace =
        cellHeight - NSToCoordRound(float(contentPageSize.height) * scale);
    dy = NSAppUnitsToFloatPixels(extraSpace / 2, aAppUnitsPerPixel);
  } else {
    scale = scaleY;
    nscoord extraSpace =
        cellWidth - NSToCoordRound(float(contentPageSize.width) * scale);
    dx = NSAppUnitsToFloatPixels(extraSpace / 2, aAppUnitsPerPixel);
  }
  transform.PreTranslate(dx, dy, 0.0f);
  transform.PreScale(scale, scale, 1.0f);

  // Apply 'page-orientation' rotation, if applicable:
  if (rotation != 0.0) {
    // We've already translated and scaled the page to fit the cell, ignoring
    // rotation. Here we rotate the page around its center and, if necessary,
    // also scale it to fit it to its cell for its orientation change.

    float fitScale = 1.0f;
    if (MOZ_LIKELY(cellWidth != cellHeight &&
                   contentPageSize.width != contentPageSize.height)) {
      // If neither the cell nor the page are square, the scale must change.
      float cellRatio = float(cellWidth) / float(cellHeight);
      float pageRatio =
          float(contentPageSize.width) / float(contentPageSize.height);
      const bool orientationWillMatchAfterRotation =
          floor(cellRatio) != floor(pageRatio);
      if (cellRatio > 1.0f) {
        cellRatio = 1.0f / cellRatio;  // normalize
      }
      if (pageRatio > 1.0f) {
        pageRatio = 1.0f / pageRatio;  // normalize
      }
      fitScale = std::max(cellRatio, pageRatio);
      if (orientationWillMatchAfterRotation) {
        // Scale up, not down
        fitScale = 1.0f / fitScale;
      }
    }

    transform.PreTranslate(
        NSAppUnitsToFloatPixels(contentPageSize.width / 2, aAppUnitsPerPixel),
        NSAppUnitsToFloatPixels(contentPageSize.height / 2, aAppUnitsPerPixel),
        0);
    if (MOZ_LIKELY(fitScale != 1.0f)) {
      transform.PreScale(fitScale, fitScale, 1.0f);
    }
    transform.RotateZ(rotation);
    transform.PreTranslate(
        NSAppUnitsToFloatPixels(-contentPageSize.width / 2, aAppUnitsPerPixel),
        NSAppUnitsToFloatPixels(-contentPageSize.height / 2, aAppUnitsPerPixel),
        0);
  }

  return transform;
}

nsIFrame::ComputeTransformFunction nsPageFrame::GetTransformGetter() const {
  return ComputePagesPerSheetAndPageSizeTransform;
}

nsPageContentFrame* nsPageFrame::PageContentFrame() const {
  nsIFrame* const frame = mFrames.FirstChild();
  MOZ_ASSERT(frame, "pageFrame must have one child");
  MOZ_ASSERT(frame->IsPageContentFrame(),
             "pageFrame must have pageContentFrame as the first child");
  return static_cast<nsPageContentFrame*>(frame);
}

nsSize nsPageFrame::ComputePageSize() const {
  // Compute the expected page-size.
  const nsPageFrame* const frame =
      StaticPrefs::layout_css_allow_mixed_page_sizes()
          ? this
          : static_cast<nsPageFrame*>(FirstContinuation());
  const StylePageSize& pageSize = frame->PageContentFrame()->StylePage()->mSize;
  nsSize size = PresContext()->GetPageSize();
  if (pageSize.IsSize()) {
    // Use the specified size,
    // ignoring sizes that include a zero width or height.
    // These are also ignored in ServoStyleSet::GetPageSizeForPageName()
    // when getting the paper size.
    nscoord cssPageWidth = pageSize.AsSize().width.ToAppUnits();
    nscoord cssPageHeight = pageSize.AsSize().height.ToAppUnits();
    if (cssPageWidth > 0 && cssPageHeight > 0) {
      return nsSize{cssPageWidth, cssPageHeight};
    }
    // Invalid size; just return the default
    return size;
  }

  if (pageSize.IsOrientation()) {
    // Ensure the correct orientation is applied.
    if (pageSize.AsOrientation() == StylePageSizeOrientation::Portrait) {
      if (size.width > size.height) {
        std::swap(size.width, size.height);
      }
    } else {
      MOZ_ASSERT(pageSize.AsOrientation() ==
                 StylePageSizeOrientation::Landscape);
      if (size.width < size.height) {
        std::swap(size.width, size.height);
      }
    }
  } else {
    MOZ_ASSERT(pageSize.IsAuto(), "Impossible page-size value?");
  }
  return size;
}

float nsPageFrame::ComputeSinglePPSPageSizeScale(
    const nsSize aContentPageSize) const {
  MOZ_ASSERT(GetSharedPageData()->PagesPerSheetInfo()->mNumPages == 1,
             "Only intended for the pps==1 case");
  MOZ_ASSERT(aContentPageSize == ComputePageSize(),
             "Incorrect content page size");

  // Check for the simplest case first, an auto page-size which requires no
  // scaling at all.
  {
    const nsPageFrame* const frame =
        StaticPrefs::layout_css_allow_mixed_page_sizes()
            ? this
            : static_cast<nsPageFrame*>(FirstContinuation());
    const StylePageSize& pageSize =
        frame->PageContentFrame()->StylePage()->mSize;
    if (pageSize.IsAuto()) {
      return 1.0f;
    }
  }

  const nsContainerFrame* const parent = GetParent();
  MOZ_ASSERT(parent && parent->IsPrintedSheetFrame(),
             "Parent of nsPageFrame should be PrintedSheetFrame");
  const auto* sheet = static_cast<const PrintedSheetFrame*>(parent);

  // Compute scaling due to a possible mismatch in the paper size we are
  // printing to (from the pres context) and the specified page size when the
  // content uses "@page {size: ...}" to specify a page size for the content.
  float scale = 1.0f;

  const nsSize sheetSize = sheet->GetSizeForChildren();
  nscoord contentPageHeight = aContentPageSize.height;
  // Scale down if the target is too wide.
  if (aContentPageSize.width > sheetSize.width) {
    scale *= float(sheetSize.width) / float(aContentPageSize.width);
    contentPageHeight = NSToCoordRound(contentPageHeight * scale);
  }
  // Scale down if the target is too tall.
  if (contentPageHeight > sheetSize.height) {
    scale *= float(sheetSize.height) / float(contentPageHeight);
  }
  MOZ_ASSERT(
      scale <= 1.0f,
      "Page-size mismatches should only have caused us to scale down, not up.");
  return scale;
}

double nsPageFrame::GetPageOrientationRotation(nsSharedPageData* aPD) const {
  if (!StaticPrefs::layout_css_page_orientation_enabled()) {
    return 0.0;
  }

  if (aPD->PagesPerSheetInfo()->mNumPages == 1 && !PresContext()->IsScreen() &&
      aPD->mPrintSettings->GetOutputFormat() !=
          nsIPrintSettings::kOutputFormatPDF) {
    // In the single page-per-sheet case we rotate the page by essentially
    // rotating the entire sheet. But we can't do that when the output device
    // doesn't support mixed sheet orientations.
    return 0.0;
  }

  const StylePageOrientation& orientation =
      PageContentFrame()->StylePage()->mPageOrientation;

  if (orientation == StylePageOrientation::RotateLeft) {
    return -M_PI / 2.0;
  }
  if (orientation == StylePageOrientation::RotateRight) {
    return M_PI / 2.0;
  }
  return 0.0;
}

void nsPageFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
                                   const nsDisplayListSet& aLists) {
  nsDisplayList content(aBuilder);
  nsDisplayListSet set(&content, &content, &content, &content, &content,
                       &content);
  {
    DisplayListClipState::AutoSaveRestore clipState(aBuilder);
    clipState.Clear();

    nsPresContext* const pc = PresContext();
    {
      // We need to extend the building rect to include the specified page size
      // (scaled by the print scaling factor), in case it is larger than the
      // physical page size. In that case the nsPageFrame will be the size of
      // the physical page, but the child nsPageContentFrame will be the larger
      // specified page size. The more correct way to do this would be to fully
      // reverse the result of ComputePagesPerSheetAndPageSizeTransform to
      // handle this scaling, but this should have the same result and is
      // easier.
      const float scale = pc->GetPageScale();
      const nsSize pageSize = ComputePageSize();
      const nsRect scaledPageRect{0, 0, NSToCoordCeil(pageSize.width / scale),
                                  NSToCoordCeil(pageSize.height / scale)};
      nsDisplayListBuilder::AutoBuildingDisplayList buildingForPageContentFrame(
          aBuilder, this, scaledPageRect, scaledPageRect);

      nsContainerFrame::BuildDisplayList(aBuilder, set);
    }

    if (pc->IsRootPaginatedDocument()) {
      content.AppendNewToTop<nsDisplayHeaderFooter>(aBuilder, this);

      // For print-preview, show margin guides if requested in the settings.
      if (pc->Type() == nsPresContext::eContext_PrintPreview &&
          mPD->mPrintSettings->GetShowMarginGuides()) {
        content.AppendNewToTop<nsDisplayGeneric>(
            aBuilder, this, PaintMarginGuides, "MarginGuides",
            DisplayItemType::TYPE_MARGIN_GUIDES);
      }
    }
  }

  // We'll be drawing the page with a (usually-trivial)
  // N-pages-per-sheet transform applied, so our passed-in visible rect
  // isn't meaningful while we're drawing our children, because the
  // transform could scale down content whose coordinates are off-screen
  // such that it ends up on-screen. So: we temporarily update the visible
  // rect to be the child nsPageFrame's whole frame-rect (represented in
  // this PrintedSheetFrame's coordinate space.
  content.AppendNewToTop<nsDisplayTransform>(
      aBuilder, this, &content, content.GetBuildingRect(),
      nsDisplayTransform::WithTransformGetter);

  set.MoveTo(aLists);
}

//------------------------------------------------------------------------------
void nsPageFrame::DeterminePageNum() {
  // If we have no previous continuation, we're page 1. Otherwise, we're
  // just one more than our previous continuation's page number.
  auto* prevContinuation = static_cast<nsPageFrame*>(GetPrevContinuation());
  mPageNum = prevContinuation ? prevContinuation->GetPageNum() + 1 : 1;
}

void nsPageFrame::PaintHeaderFooter(gfxContext& aRenderingContext, nsPoint aPt,
                                    bool aDisableSubpixelAA) {
  nsPresContext* pc = PresContext();

  nsRect rect(aPt, ComputePageSize());
  aRenderingContext.SetColor(sRGBColor::OpaqueBlack());

  DrawTargetAutoDisableSubpixelAntialiasing disable(
      aRenderingContext.GetDrawTarget(), aDisableSubpixelAA);

  // Get the FontMetrics to determine width.height of strings
  nsFontMetrics::Params params;
  params.userFontSet = pc->GetUserFontSet();
  params.textPerf = pc->GetTextPerfMetrics();
  params.featureValueLookup = pc->GetFontFeatureValuesLookup();
  RefPtr<nsFontMetrics> fontMet = pc->GetMetricsFor(mPD->mHeadFootFont, params);

  nscoord ascent = fontMet->MaxAscent();
  nscoord visibleHeight = fontMet->MaxHeight();

  // print document headers and footers
  nsString headerLeft, headerCenter, headerRight;
  mPD->mPrintSettings->GetHeaderStrLeft(headerLeft);
  mPD->mPrintSettings->GetHeaderStrCenter(headerCenter);
  mPD->mPrintSettings->GetHeaderStrRight(headerRight);
  DrawHeaderFooter(aRenderingContext, *fontMet, eHeader, headerLeft,
                   headerCenter, headerRight, rect, ascent, visibleHeight);

  nsString footerLeft, footerCenter, footerRight;
  mPD->mPrintSettings->GetFooterStrLeft(footerLeft);
  mPD->mPrintSettings->GetFooterStrCenter(footerCenter);
  mPD->mPrintSettings->GetFooterStrRight(footerRight);
  DrawHeaderFooter(aRenderingContext, *fontMet, eFooter, footerLeft,
                   footerCenter, footerRight, rect, ascent, visibleHeight);
}

void nsPageFrame::SetSharedPageData(nsSharedPageData* aPD) {
  mPD = aPD;
  // Set the shared data into the page frame before reflow
  PageContentFrame()->SetSharedPageData(mPD);
}

nsIFrame* NS_NewPageBreakFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
  MOZ_ASSERT(aPresShell, "null PresShell");
  // check that we are only creating page break frames when printing
  NS_ASSERTION(aPresShell->GetPresContext()->IsPaginated(),
               "created a page break frame while not printing");

  return new (aPresShell)
      nsPageBreakFrame(aStyle, aPresShell->GetPresContext());
}

NS_IMPL_FRAMEARENA_HELPERS(nsPageBreakFrame)

nsPageBreakFrame::nsPageBreakFrame(ComputedStyle* aStyle,
                                   nsPresContext* aPresContext)
    : nsLeafFrame(aStyle, aPresContext, kClassID) {}

nsPageBreakFrame::~nsPageBreakFrame() = default;

nscoord nsPageBreakFrame::GetIntrinsicISize() {
  return nsPresContext::CSSPixelsToAppUnits(1);
}

nscoord nsPageBreakFrame::GetIntrinsicBSize() { return 0; }

void nsPageBreakFrame::Reflow(nsPresContext* aPresContext,
                              ReflowOutput& aReflowOutput,
                              const ReflowInput& aReflowInput,
                              nsReflowStatus& aStatus) {
  DO_GLOBAL_REFLOW_COUNT("nsPageBreakFrame");
  MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");

  // Override reflow, since we don't want to deal with what our
  // computed values are.
  const WritingMode wm = aReflowInput.GetWritingMode();
  nscoord bSize = aReflowInput.AvailableBSize();
  if (aReflowInput.AvailableBSize() == NS_UNCONSTRAINEDSIZE) {
    bSize = nscoord(0);
  } else if (GetContent()->IsHTMLElement(nsGkAtoms::legend)) {
    // If this is a page break frame for a _rendered legend_ then it should be
    // ignored since these frames are inserted inside the fieldset's inner
    // frame and thus "misplaced".  nsFieldSetFrame::Reflow deals with these
    // forced breaks explicitly instead.
    const nsContainerFrame* parent = GetParent();
    if (parent &&
        parent->Style()->GetPseudoType() == PseudoStyleType::fieldsetContent) {
      while ((parent = parent->GetParent())) {
        if (const nsFieldSetFrame* const fieldset = do_QueryFrame(parent)) {
          const auto* const legend = fieldset->GetLegend();
          if (legend && legend->GetContent() == GetContent()) {
            bSize = nscoord(0);
          }
          break;
        }
      }
    }
  }
  LogicalSize finalSize(wm, GetIntrinsicISize(), bSize);
  // round the height down to the nearest pixel
  // XXX(mats) why???
  finalSize.BSize(wm) -=
      finalSize.BSize(wm) % nsPresContext::CSSPixelsToAppUnits(1);
  aReflowOutput.SetSize(wm, finalSize);
}

#ifdef DEBUG_FRAME_DUMP
nsresult nsPageBreakFrame::GetFrameName(nsAString& aResult) const {
  return MakeFrameName(u"PageBreak"_ns, aResult);
}
#endif