/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "nsFontMetrics.h"
#include <math.h>                // for floor, ceil
#include <algorithm>             // for max
#include "gfxContext.h"          // for gfxContext
#include "gfxFontConstants.h"    // for NS_FONT_{SUB,SUPER}SCRIPT_OFFSET_RATIO
#include "gfxPlatform.h"         // for gfxPlatform
#include "gfxPoint.h"            // for gfxPoint
#include "gfxRect.h"             // for gfxRect
#include "gfxTextRun.h"          // for gfxFontGroup
#include "gfxTypes.h"            // for gfxFloat
#include "nsAtom.h"              // for nsAtom
#include "nsBoundingMetrics.h"   // for nsBoundingMetrics
#include "nsDebug.h"             // for NS_ERROR
#include "nsDeviceContext.h"     // for nsDeviceContext
#include "nsMathUtils.h"         // for NS_round
#include "nsPresContext.h"       // for nsPresContext
#include "nsString.h"            // for nsString
#include "nsStyleConsts.h"       // for StyleHyphens::None
#include "mozilla/Assertions.h"  // for MOZ_ASSERT
#include "mozilla/UniquePtr.h"   // for UniquePtr

class gfxUserFontSet;
using namespace mozilla;

namespace {

class AutoTextRun {
 public:
  typedef mozilla::gfx::DrawTarget DrawTarget;

  AutoTextRun(const nsFontMetrics* aMetrics, DrawTarget* aDrawTarget,
              const char* aString, uint32_t aLength) {
    mTextRun = aMetrics->GetThebesFontGroup()->MakeTextRun(
        reinterpret_cast<const uint8_t*>(aString), aLength, aDrawTarget,
        aMetrics->AppUnitsPerDevPixel(), ComputeFlags(aMetrics),
        nsTextFrameUtils::Flags(), nullptr);
  }

  AutoTextRun(const nsFontMetrics* aMetrics, DrawTarget* aDrawTarget,
              const char16_t* aString, uint32_t aLength) {
    mTextRun = aMetrics->GetThebesFontGroup()->MakeTextRun(
        aString, aLength, aDrawTarget, aMetrics->AppUnitsPerDevPixel(),
        ComputeFlags(aMetrics), nsTextFrameUtils::Flags(), nullptr);
  }

  gfxTextRun* get() const { return mTextRun.get(); }
  gfxTextRun* operator->() const { return mTextRun.get(); }

 private:
  static gfx::ShapedTextFlags ComputeFlags(const nsFontMetrics* aMetrics) {
    gfx::ShapedTextFlags flags = gfx::ShapedTextFlags();
    if (aMetrics->GetTextRunRTL()) {
      flags |= gfx::ShapedTextFlags::TEXT_IS_RTL;
    }
    if (aMetrics->GetVertical()) {
      switch (aMetrics->GetTextOrientation()) {
        case StyleTextOrientation::Mixed:
          flags |= gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED;
          break;
        case StyleTextOrientation::Upright:
          flags |= gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT;
          break;
        case StyleTextOrientation::Sideways:
          flags |= gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_SIDEWAYS_RIGHT;
          break;
      }
    }
    return flags;
  }

  RefPtr<gfxTextRun> mTextRun;
};

class StubPropertyProvider final : public gfxTextRun::PropertyProvider {
 public:
  void GetHyphenationBreaks(
      gfxTextRun::Range aRange,
      gfxTextRun::HyphenType* aBreakBefore) const override {
    NS_ERROR(
        "This shouldn't be called because we never call BreakAndMeasureText");
  }
  mozilla::StyleHyphens GetHyphensOption() const override {
    NS_ERROR(
        "This shouldn't be called because we never call BreakAndMeasureText");
    return mozilla::StyleHyphens::None;
  }
  gfxFloat GetHyphenWidth() const override {
    NS_ERROR("This shouldn't be called because we never enable hyphens");
    return 0;
  }
  already_AddRefed<mozilla::gfx::DrawTarget> GetDrawTarget() const override {
    NS_ERROR("This shouldn't be called because we never enable hyphens");
    return nullptr;
  }
  uint32_t GetAppUnitsPerDevUnit() const override {
    NS_ERROR("This shouldn't be called because we never enable hyphens");
    return 60;
  }
  bool GetSpacing(gfxTextRun::Range aRange, Spacing* aSpacing) const override {
    NS_ERROR("This shouldn't be called because we never enable spacing");
    return false;
  }
  gfx::ShapedTextFlags GetShapedTextFlags() const override {
    NS_ERROR("This shouldn't be called because we never enable hyphens");
    return gfx::ShapedTextFlags();
  }
};

}  // namespace

nsFontMetrics::nsFontMetrics(const nsFont& aFont, const Params& aParams,
                             nsPresContext* aContext)
    : mFont(aFont),
      mLanguage(aParams.language),
      mPresContext(aContext),
      mP2A(aContext->DeviceContext()->AppUnitsPerDevPixel()),
      mOrientation(aParams.orientation),
      mExplicitLanguage(aParams.explicitLanguage),
#ifdef XP_WIN
      mAllowForceGDIClassic(aParams.allowForceGDIClassic),
#endif
      mTextRunRTL(false),
      mVertical(false),
      mTextOrientation(mozilla::StyleTextOrientation::Mixed) {
  gfxFontStyle style(aFont.style, aFont.weight, aFont.stretch,
                     gfxFloat(aFont.size.ToAppUnits()) / mP2A, aFont.sizeAdjust,
                     aFont.family.is_system_font,
                     aContext->DeviceContext()->IsPrinterContext(),
#ifdef XP_WIN
                     mAllowForceGDIClassic,
#endif
                     aFont.synthesisWeight == StyleFontSynthesis::Auto,
                     aFont.synthesisStyle,
                     aFont.synthesisSmallCaps == StyleFontSynthesis::Auto,
                     aFont.synthesisPosition == StyleFontSynthesis::Auto,
                     aFont.languageOverride);

  aFont.AddFontFeaturesToStyle(&style, mOrientation == eVertical);
  style.featureValueLookup = aParams.featureValueLookup;

  aFont.AddFontVariationsToStyle(&style);

  gfxFloat devToCssSize = gfxFloat(mP2A) / gfxFloat(AppUnitsPerCSSPixel());
  mFontGroup = new gfxFontGroup(
      mPresContext, aFont.family.families, &style, mLanguage, mExplicitLanguage,
      aParams.textPerf, aParams.userFontSet, devToCssSize, aFont.variantEmoji);
}

nsFontMetrics::~nsFontMetrics() {
  // Should not be dropped by stylo
  MOZ_ASSERT(NS_IsMainThread());
  if (mPresContext) {
    mPresContext->FontMetricsDeleted(this);
  }
}

void nsFontMetrics::Destroy() { mPresContext = nullptr; }

// XXXTODO get rid of this macro
#define ROUND_TO_TWIPS(x) (nscoord) floor(((x) * mP2A) + 0.5)
#define CEIL_TO_TWIPS(x) (nscoord) ceil((x) * mP2A)

static const gfxFont::Metrics& GetMetrics(
    const nsFontMetrics* aFontMetrics,
    nsFontMetrics::FontOrientation aOrientation) {
  RefPtr<gfxFont> font =
      aFontMetrics->GetThebesFontGroup()->GetFirstValidFont();
  return font->GetMetrics(aOrientation);
}

static const gfxFont::Metrics& GetMetrics(const nsFontMetrics* aFontMetrics) {
  return GetMetrics(aFontMetrics, aFontMetrics->Orientation());
}

nscoord nsFontMetrics::XHeight() const {
  return ROUND_TO_TWIPS(GetMetrics(this).xHeight);
}

nscoord nsFontMetrics::CapHeight() const {
  return ROUND_TO_TWIPS(GetMetrics(this).capHeight);
}

nscoord nsFontMetrics::SuperscriptOffset() const {
  return ROUND_TO_TWIPS(GetMetrics(this).emHeight *
                        NS_FONT_SUPERSCRIPT_OFFSET_RATIO);
}

nscoord nsFontMetrics::SubscriptOffset() const {
  return ROUND_TO_TWIPS(GetMetrics(this).emHeight *
                        NS_FONT_SUBSCRIPT_OFFSET_RATIO);
}

void nsFontMetrics::GetStrikeout(nscoord& aOffset, nscoord& aSize) const {
  aOffset = ROUND_TO_TWIPS(GetMetrics(this).strikeoutOffset);
  aSize = ROUND_TO_TWIPS(GetMetrics(this).strikeoutSize);
}

void nsFontMetrics::GetUnderline(nscoord& aOffset, nscoord& aSize) const {
  aOffset = ROUND_TO_TWIPS(mFontGroup->GetUnderlineOffset());
  aSize = ROUND_TO_TWIPS(GetMetrics(this).underlineSize);
}

// GetMaxAscent/GetMaxDescent/GetMaxHeight must contain the
// text-decoration lines drawable area. See bug 421353.
// BE CAREFUL for rounding each values. The logic MUST be same as
// nsCSSRendering::GetTextDecorationRectInternal's.

static gfxFloat ComputeMaxDescent(const gfxFont::Metrics& aMetrics,
                                  gfxFontGroup* aFontGroup) {
  gfxFloat offset = floor(-aFontGroup->GetUnderlineOffset() + 0.5);
  gfxFloat size = NS_round(aMetrics.underlineSize);
  gfxFloat minDescent = offset + size;
  return floor(std::max(minDescent, aMetrics.maxDescent) + 0.5);
}

static gfxFloat ComputeMaxAscent(const gfxFont::Metrics& aMetrics) {
  return floor(aMetrics.maxAscent + 0.5);
}

nscoord nsFontMetrics::InternalLeading() const {
  return ROUND_TO_TWIPS(GetMetrics(this).internalLeading);
}

nscoord nsFontMetrics::ExternalLeading() const {
  return ROUND_TO_TWIPS(GetMetrics(this).externalLeading);
}

nscoord nsFontMetrics::EmHeight() const {
  return ROUND_TO_TWIPS(GetMetrics(this).emHeight);
}

nscoord nsFontMetrics::TrimmedAscent() const {
  const auto& m = GetMetrics(this);
  return ROUND_TO_TWIPS(std::max(0.0, m.maxAscent - m.internalLeading / 2));
}

nscoord nsFontMetrics::TrimmedDescent() const {
  const auto& m = GetMetrics(this);
  return ROUND_TO_TWIPS(std::max(0.0, m.maxDescent - m.internalLeading / 2));
}

nscoord nsFontMetrics::MaxHeight() const {
  return CEIL_TO_TWIPS(ComputeMaxAscent(GetMetrics(this))) +
         CEIL_TO_TWIPS(ComputeMaxDescent(GetMetrics(this), mFontGroup));
}

nscoord nsFontMetrics::MaxAscent() const {
  return CEIL_TO_TWIPS(ComputeMaxAscent(GetMetrics(this)));
}

nscoord nsFontMetrics::MaxDescent() const {
  return CEIL_TO_TWIPS(ComputeMaxDescent(GetMetrics(this), mFontGroup));
}

nscoord nsFontMetrics::MaxAdvance() const {
  return CEIL_TO_TWIPS(GetMetrics(this).maxAdvance);
}

nscoord nsFontMetrics::AveCharWidth() const {
  // Use CEIL instead of ROUND for consistency with GetMaxAdvance
  return CEIL_TO_TWIPS(GetMetrics(this).aveCharWidth);
}

nscoord nsFontMetrics::ZeroOrAveCharWidth() const {
  return CEIL_TO_TWIPS(GetMetrics(this).ZeroOrAveCharWidth());
}

nscoord nsFontMetrics::SpaceWidth() const {
  // For vertical text with mixed or sideways orientation, we want the
  // width of a horizontal space (even if we're using vertical line-spacing
  // metrics, as with "writing-mode:vertical-*;text-orientation:mixed").
  return CEIL_TO_TWIPS(
      GetMetrics(this,
                 mVertical && mTextOrientation == StyleTextOrientation::Upright
                     ? eVertical
                     : eHorizontal)
          .spaceWidth);
}

nscoord nsFontMetrics::InterScriptSpacingWidth() const {
  const auto& m = GetMetrics(this);
  // If there is no advance measure of the CJK water ideograph, use 1em instead.
  // https://drafts.csswg.org/css-values-4/#ic
  LayoutDeviceDoubleCoord ic =
      m.ideographicWidth >= 0.0 ? m.ideographicWidth : m.emHeight;

  // The inter-script spacing is defined as 1/8 of the CJK advance measure, i.e.
  // 0.125ic: https://drafts.csswg.org/css-text-4/#inter-script-spacing
  constexpr double kFraction = 0.125;
  return LayoutDevicePixel::ToAppUnits(ic * kFraction, AppUnitsPerDevPixel());
}

int32_t nsFontMetrics::GetMaxStringLength() const {
  const double x = 32767.0 / std::max(1.0, GetMetrics(this).maxAdvance);
  int32_t len = (int32_t)floor(x);
  return std::max(1, len);
}

nscoord nsFontMetrics::GetWidth(const char* aString, uint32_t aLength,
                                DrawTarget* aDrawTarget) const {
  if (aLength == 0) {
    return 0;
  }
  if (aLength == 1 && aString[0] == ' ') {
    return SpaceWidth();
  }
  StubPropertyProvider provider;
  AutoTextRun textRun(this, aDrawTarget, aString, aLength);
  if (textRun.get()) {
    return NSToCoordRound(
        textRun->GetAdvanceWidth(gfxTextRun::Range(0, aLength), &provider));
  }
  return 0;
}

nscoord nsFontMetrics::GetWidth(const char16_t* aString, uint32_t aLength,
                                DrawTarget* aDrawTarget) const {
  if (aLength == 0) {
    return 0;
  }
  if (aLength == 1 && aString[0] == ' ') {
    return SpaceWidth();
  }
  StubPropertyProvider provider;
  AutoTextRun textRun(this, aDrawTarget, aString, aLength);
  if (textRun.get()) {
    return NSToCoordRound(
        textRun->GetAdvanceWidth(gfxTextRun::Range(0, aLength), &provider));
  }
  return 0;
}

// Draw a string using this font handle on the surface passed in.
void nsFontMetrics::DrawString(const char* aString, uint32_t aLength,
                               nscoord aX, nscoord aY,
                               gfxContext* aContext) const {
  if (aLength == 0) {
    return;
  }
  StubPropertyProvider provider;
  AutoTextRun textRun(this, aContext->GetDrawTarget(), aString, aLength);
  if (!textRun.get()) {
    return;
  }
  gfx::Point pt(aX, aY);
  gfxTextRun::Range range(0, aLength);
  if (mTextRunRTL) {
    if (mVertical) {
      pt.y += textRun->GetAdvanceWidth(range, &provider);
    } else {
      pt.x += textRun->GetAdvanceWidth(range, &provider);
    }
  }
  mozilla::gfx::PaletteCache paletteCache;
  gfxTextRun::DrawParams params(aContext, paletteCache);
  params.provider = &provider;
  textRun->Draw(range, pt, params);
}

void nsFontMetrics::DrawString(
    const char16_t* aString, uint32_t aLength, nscoord aX, nscoord aY,
    gfxContext* aContext, DrawTarget* aTextRunConstructionDrawTarget) const {
  if (aLength == 0) {
    return;
  }
  StubPropertyProvider provider;
  AutoTextRun textRun(this, aTextRunConstructionDrawTarget, aString, aLength);
  if (!textRun.get()) {
    return;
  }
  gfx::Point pt(aX, aY);
  gfxTextRun::Range range(0, aLength);
  if (mTextRunRTL) {
    if (mVertical) {
      pt.y += textRun->GetAdvanceWidth(range, &provider);
    } else {
      pt.x += textRun->GetAdvanceWidth(range, &provider);
    }
  }
  mozilla::gfx::PaletteCache paletteCache;
  gfxTextRun::DrawParams params(aContext, paletteCache);
  params.provider = &provider;
  textRun->Draw(range, pt, params);
}

static nsBoundingMetrics GetTextBoundingMetrics(
    const nsFontMetrics* aMetrics, const char16_t* aString, uint32_t aLength,
    mozilla::gfx::DrawTarget* aDrawTarget, gfxFont::BoundingBoxType aType) {
  if (aLength == 0) {
    return nsBoundingMetrics();
  }
  StubPropertyProvider provider;
  AutoTextRun textRun(aMetrics, aDrawTarget, aString, aLength);
  nsBoundingMetrics m;
  if (textRun.get()) {
    gfxTextRun::Metrics theMetrics = textRun->MeasureText(
        gfxTextRun::Range(0, aLength), aType, aDrawTarget, &provider);

    m.leftBearing = NSToCoordFloor(theMetrics.mBoundingBox.X());
    m.rightBearing = NSToCoordCeil(theMetrics.mBoundingBox.XMost());
    m.ascent = NSToCoordCeil(-theMetrics.mBoundingBox.Y());
    m.descent = NSToCoordCeil(theMetrics.mBoundingBox.YMost());
    m.width = NSToCoordRound(theMetrics.mAdvanceWidth);
  }
  return m;
}

nsBoundingMetrics nsFontMetrics::GetBoundingMetrics(
    const char16_t* aString, uint32_t aLength, DrawTarget* aDrawTarget) const {
  return GetTextBoundingMetrics(this, aString, aLength, aDrawTarget,
                                gfxFont::TIGHT_HINTED_OUTLINE_EXTENTS);
}

nsBoundingMetrics nsFontMetrics::GetInkBoundsForInkOverflow(
    const char16_t* aString, uint32_t aLength, DrawTarget* aDrawTarget) const {
  return GetTextBoundingMetrics(this, aString, aLength, aDrawTarget,
                                gfxFont::LOOSE_INK_EXTENTS);
}

gfxUserFontSet* nsFontMetrics::GetUserFontSet() const {
  return mFontGroup->GetUserFontSet();
}
