// Ported from `vscode/src/vs/editor/common/config/fontInfo.ts` at
// checked-in source. Monaco's
// `FontInfo extends BareFontInfo` inheritance flattens into two structs (a
// `FontInfo` repeats the bare fields and exposes `bare()` for consumers of
// the base shape). The `EditorZoom` multiplier is not ported — the readonly
// viewer has no zoom UI, so the factor is constantly `1` — and neither is
// the `serializeFontInfo`/`restoreFontInfo` persistence channel.

///|
/// Monaco `GOLDEN_LINE_HEIGHT_RATIO`: determined from empirical observations.
let golden_line_height_ratio : Double = golden_line_height_ratio_for_platform(
  current_font_platform(),
)

///|
/// Monaco `MINIMUM_LINE_HEIGHT`.
let minimum_line_height = 8

///|
/// Monaco `EditorFontLigatures.OFF` (`editorOptions.ts`): the
/// `font-feature-settings` value when font ligatures are disabled — the
/// editor default, and the value `FontMeasurements`' monospace check keys on.
pub let editor_font_ligatures_off = "\"liga\" off, \"calt\" off"

///|
/// Monaco `FONT_VARIATION_OFF`.
pub let font_variation_off = "normal"

///|
/// Monaco `FONT_VARIATION_TRANSLATE`.
let font_variation_translate = "translate"

///|
priv enum FontPlatform {
  MacOS
  Windows
  Other
}

///|
fn current_font_platform() -> FontPlatform {
  if @base_common.is_macintosh {
    MacOS
  } else if @base_common.is_windows {
    Windows
  } else {
    Other
  }
}

///|
fn golden_line_height_ratio_for_platform(platform : FontPlatform) -> Double {
  match platform {
    MacOS => 1.5
    Windows | Other => 1.35
  }
}

///|
/// Monaco `EDITOR_FONT_DEFAULTS` (with `DEFAULT_MAC_FONT_FAMILY` /
/// `DEFAULT_WINDOWS_FONT_FAMILY` / `DEFAULT_LINUX_FONT_FAMILY`).
pub struct EditorFontDefaults {
  font_family : String
  font_weight : String
  font_size : Double
  line_height : Double
  letter_spacing : Double
} derive(Eq, Debug)

///|
pub let editor_font_defaults : EditorFontDefaults = editor_font_defaults_for_platform(
  current_font_platform(),
)

///|
fn editor_font_defaults_for_platform(
  platform : FontPlatform,
) -> EditorFontDefaults {
  {
    font_family: match platform {
      MacOS => default_mac_font_family
      Windows => default_windows_font_family
      Other => default_linux_font_family
    },
    font_weight: "normal",
    font_size: match platform {
      MacOS => 12.0
      Windows | Other => 14.0
    },
    line_height: 0.0,
    letter_spacing: 0.0,
  }
}

///|
/// Monaco `BareFontInfo`: the requested font settings, with the line height
/// already resolved through the golden-ratio/em/minimum rules.
pub struct BareFontInfo {
  pixel_ratio : Double
  font_family : String
  font_weight : String
  font_size : Double
  font_feature_settings : String
  font_variation_settings : String
  line_height : Int32
  letter_spacing : Double
} derive(Eq, Debug)

///|
/// Monaco `BareFontInfo._create`: `lineHeight == 0` derives the golden-ratio
/// line height from the font size, values below `MINIMUM_LINE_HEIGHT` are
/// treated as em multipliers, and the result is rounded and floored at the
/// minimum. `fontVariationSettings === 'translate'` translates a numeric
/// font weight into a `'wght'` variation.
pub fn BareFontInfo::create(
  font_family~ : String,
  font_weight~ : String,
  font_size~ : Double,
  font_feature_settings~ : String,
  font_variation_settings~ : String,
  line_height~ : Double,
  letter_spacing~ : Double,
  pixel_ratio~ : Double,
) -> BareFontInfo {
  let mut line_height = line_height
  if line_height == 0.0 {
    line_height = golden_line_height_ratio * font_size
  } else if line_height < minimum_line_height.to_double() {
    // Values too small to be line heights in pixels are in ems.
    line_height *= font_size
  }
  // Enforce integer, minimum constraints (`Math.round` of a non-negative
  // ratio is `floor(x + 0.5)`).
  let mut rounded_line_height = (line_height + 0.5).to_int()
  if rounded_line_height < minimum_line_height {
    rounded_line_height = minimum_line_height
  }
  let mut font_weight = font_weight
  let mut font_variation_settings = font_variation_settings
  if font_variation_settings == font_variation_translate {
    if font_weight == "normal" || font_weight == "bold" {
      font_variation_settings = font_variation_off
    } else {
      font_variation_settings = "'wght' \{parse_decimal_prefix(font_weight)}"
      font_weight = "normal"
    }
  }
  {
    pixel_ratio,
    font_family,
    font_weight,
    font_size,
    font_feature_settings,
    font_variation_settings,
    line_height: rounded_line_height,
    letter_spacing,
  }
}

///|
/// Monaco `BareFontInfo.getId`: the `FontMeasurements` cache key.
pub fn BareFontInfo::get_id(self : BareFontInfo) -> String {
  "\{self.pixel_ratio}-\{self.font_family}-\{self.font_weight}-\{self.font_size}-\{self.font_feature_settings}-\{self.font_variation_settings}-\{self.line_height}-\{self.letter_spacing}"
}

///|
/// Monaco `BareFontInfo.getMassagedFontFamily`: the configured family with
/// the platform default appended as a fallback, quoting families that use
/// `+` or spaces.
pub fn BareFontInfo::get_massaged_font_family(self : BareFontInfo) -> String {
  massage_font_family(self.font_family, editor_font_defaults.font_family)
}

///|
fn massage_font_family(
  font_family : String,
  fallback_font_family : String,
) -> String {
  let massaged = wrap_in_quotes(font_family)
  if fallback_font_family != "" && font_family != fallback_font_family {
    "\{massaged}, \{fallback_font_family}"
  } else {
    massaged
  }
}

///|
fn wrap_in_quotes(font_family : String) -> String {
  if font_family.contains(",") ||
    font_family.contains("\"") ||
    font_family.contains("'") {
    // Looks like the font family might be already escaped.
    return font_family
  }
  if font_family.contains("+") || font_family.contains(" ") {
    // Wrap a font family using + or  with quotes.
    return "\"\{font_family}\""
  }
  font_family
}

///|
/// The decimal-prefix behavior of JavaScript `parseInt(value, 10)` used by
/// BareFontInfo's variation translation. Editor validation supplies canonical
/// weights, but retaining prefix/sign handling prevents raw strings such as
/// `0500` or `500suffix` from leaking into CSS variation syntax.
fn parse_decimal_prefix(value : String) -> String {
  let view = value.view()
  let index = for index = 0; index < value.length(); {
    let code = view.unsafe_get(index).to_int()
    if is_ecmascript_whitespace(code) {
      continue index + 1
    } else {
      break index
    }
  } nobreak {
    index
  }
  let (sign, index) = if index < value.length() {
    let code = view.unsafe_get(index).to_int()
    if code == 0x2D {
      (-1, index + 1)
    } else if code == 0x2B {
      (1, index + 1)
    } else {
      (1, index)
    }
  } else {
    (1, index)
  }
  let digit_start = index
  let (index, result) = for index = index, result = 0.0; index < value.length(); {
    let code = view.unsafe_get(index).to_int()
    if code < 0x30 || code > 0x39 {
      break (index, result)
    }
    continue index + 1, result * 10.0 + (code - 0x30).to_double()
  } nobreak {
    (index, result)
  }
  if index == digit_start {
    "NaN"
  } else {
    (sign.to_double() * result).to_string()
  }
}

///|
/// Leading whitespace accepted by ECMAScript `StringNumericLiteral`, which is
/// what `parseInt` trims before reading its optional sign and decimal prefix.
fn is_ecmascript_whitespace(code : Int32) -> Bool {
  (code >= 0x0009 && code <= 0x000D) ||
  code == 0x0020 ||
  code == 0x00A0 ||
  code == 0x1680 ||
  (code >= 0x2000 && code <= 0x200A) ||
  code == 0x2028 ||
  code == 0x2029 ||
  code == 0x202F ||
  code == 0x205F ||
  code == 0x3000 ||
  code == 0xFEFF
}

///|
/// Monaco `FontInfo`: the bare settings plus the widths measured from the
/// DOM (`FontMeasurements`). `is_trusted` is `false` for estimated readings
/// (never measured, or measured while the web font was still loading).
pub(all) struct FontInfo {
  pixel_ratio : Double
  font_family : String
  font_weight : String
  font_size : Double
  font_feature_settings : String
  font_variation_settings : String
  line_height : Int32
  letter_spacing : Double
  is_trusted : Bool
  is_monospace : Bool
  typical_halfwidth_character_width : Double
  typical_fullwidth_character_width : Double
  can_use_halfwidth_rightwards_arrow : Bool
  space_width : Double
  middot_width : Double
  wsmiddot_width : Double
  max_digit_width : Double
} derive(Eq, Debug)

///|
/// An estimated `FontInfo` for a `BareFontInfo` that has not been measured
/// yet (no DOM) — untrusted, like Monaco's `restoreFontInfo` readings, so a
/// real measurement replaces it. `0.6` per code unit is the advance ratio
/// of the default monospace stacks.
pub fn FontInfo::estimated(bare : BareFontInfo) -> FontInfo {
  let estimated_advance = bare.font_size * 0.6
  {
    pixel_ratio: bare.pixel_ratio,
    font_family: bare.font_family,
    font_weight: bare.font_weight,
    font_size: bare.font_size,
    font_feature_settings: bare.font_feature_settings,
    font_variation_settings: bare.font_variation_settings,
    line_height: bare.line_height,
    letter_spacing: bare.letter_spacing,
    is_trusted: false,
    is_monospace: true,
    typical_halfwidth_character_width: estimated_advance,
    typical_fullwidth_character_width: estimated_advance * 2.0,
    can_use_halfwidth_rightwards_arrow: true,
    space_width: estimated_advance,
    middot_width: estimated_advance,
    wsmiddot_width: estimated_advance,
    max_digit_width: estimated_advance,
  }
}

///|
/// The estimated `FontInfo` of the `EDITOR_FONT_DEFAULTS` — the headless
/// stand-in used before any DOM measurement.
pub fn FontInfo::default() -> FontInfo {
  FontInfo::estimated(
    BareFontInfo::create(
      font_family=editor_font_defaults.font_family,
      font_weight=editor_font_defaults.font_weight,
      font_size=editor_font_defaults.font_size,
      font_feature_settings=editor_font_ligatures_off,
      font_variation_settings=font_variation_off,
      line_height=editor_font_defaults.line_height,
      letter_spacing=editor_font_defaults.letter_spacing,
      pixel_ratio=1.0,
    ),
  )
}

///|
/// The `BareFontInfo` slice of this `FontInfo` (Monaco's upcast).
pub fn FontInfo::bare(self : FontInfo) -> BareFontInfo {
  {
    pixel_ratio: self.pixel_ratio,
    font_family: self.font_family,
    font_weight: self.font_weight,
    font_size: self.font_size,
    font_feature_settings: self.font_feature_settings,
    font_variation_settings: self.font_variation_settings,
    line_height: self.line_height,
    letter_spacing: self.letter_spacing,
  }
}

///|
pub fn FontInfo::get_id(self : FontInfo) -> String {
  self.bare().get_id()
}

///|
/// Monaco `FontInfo.equals` — deliberately ignores `pixelRatio`,
/// `isTrusted`, and `isMonospace`, exactly like the reference.
pub fn FontInfo::equals(self : FontInfo, other : FontInfo) -> Bool {
  self.font_family == other.font_family &&
  self.font_weight == other.font_weight &&
  self.font_size == other.font_size &&
  self.font_feature_settings == other.font_feature_settings &&
  self.font_variation_settings == other.font_variation_settings &&
  self.line_height == other.line_height &&
  self.letter_spacing == other.letter_spacing &&
  self.typical_halfwidth_character_width ==
  other.typical_halfwidth_character_width &&
  self.typical_fullwidth_character_width ==
  other.typical_fullwidth_character_width &&
  self.can_use_halfwidth_rightwards_arrow ==
  other.can_use_halfwidth_rightwards_arrow &&
  self.space_width == other.space_width &&
  self.middot_width == other.middot_width &&
  self.wsmiddot_width == other.wsmiddot_width &&
  self.max_digit_width == other.max_digit_width
}

///|
pub extend BareFontInfo with Debug::{to_repr}

///|
pub extend BareFontInfo with Eq::{equal, not_equal}

///|
pub extend EditorFontDefaults with Debug::{to_repr}

///|
pub extend EditorFontDefaults with Eq::{equal, not_equal}

///|
pub extend FontInfo with Debug::{to_repr}

///|
pub extend FontInfo with Eq::{equal, not_equal}