/*
========================================
Functions
----------------------------------------
*/

// The following vars need to be set
// here, before the rest of the system
// variables are set

$root-font-size: if($theme-respect-user-font-size, 100%, $theme-root-font-size);

$root-font-size-equiv: if(
  $theme-respect-user-font-size,
  16px,
  $theme-root-font-size
);

/*
========================================
General-purpose functions
----------------------------------------
*/

/*
----------------------------------------
map-deep-get()
----------------------------------------
@author Hugo Giraudel
@access public
@param {Map} $map - Map
@param {Arglist} $keys - Key chain
@return {*} - Desired value
----------------------------------------
*/

@function map-deep-get($map, $keys...) {
  @each $key in $keys {
    $map: map-get($map, $key);
  }

  @return $map;
}

/*
----------------------------------------
strip-unit()
----------------------------------------
Remove the unit of a length
@author Hugo Giraudel
@param {Number} $number - Number to remove unit from
@return {Number} - Unitless number
----------------------------------------
*/

@function strip-unit($number) {
  @if type-of($number) == "number" and not unitless($number) {
    @return $number / ($number * 0 + 1);
  }

  @return $number;
}

/*
----------------------------------------
multi-cat()
----------------------------------------
Concatenate two lists
----------------------------------------
*/

@function multi-cat($list1, $list2) {
  $this-list: ();

  @each $e in $list1 {
    @each $ee in $list2 {
      $this-block: $e + $ee;
      $this-list: join($this-list, $this-block);
    }
  }

  @return $this-list;
}

/*
----------------------------------------
map-collect()
----------------------------------------
Collect multiple maps into a single
large map
source: https://gist.github.com/bigglesrocks/d75091700f8f2be5abfe
----------------------------------------
*/

@function map-collect($maps...) {
  $collection: ();

  @each $map in $maps {
    $collection: map-merge($collection, $map);
  }

  @return $collection;
}

/*
----------------------------------------
smart-quote()
----------------------------------------
Quotes strings
Inspects `px`, `xs`, and `xl` numbers
Leaves bools as is
----------------------------------------
*/

@function smart-quote($value) {
  @if type-of($value) == "string" {
    @return quote($value);
  }

  @if type-of($value) == "number" and index(("px", "xl", "xs"), unit($value)) {
    @return inspect($value);
  }

  @if type-of($value) == "color" {
    @error 'Only use quoted color tokens in USWDS functions and mixins. '
      + 'See designsystem.digital.gov/design-tokens/color '
      + 'for more information.';
  }

  @return $value;
}

/*
----------------------------------------
remove()
----------------------------------------
Remove a value from a list
----------------------------------------
*/

@function remove($list, $value, $recursive: false) {
  $result: ();

  @for $i from 1 through length($list) {
    @if type-of(nth($list, $i)) == list and $recursive {
      $result: append($result, remove(nth($list, $i), $value, $recursive));
    } @else if nth($list, $i) != $value {
      $result: append($result, nth($list, $i));
    }
  }

  @return $result;
}

/*
----------------------------------------
strunquote()
----------------------------------------
Unquote a string
----------------------------------------
*/

@function strunquote($value) {
  @if type-of($value) == "string" {
    $value: unquote($value);
  }

  @return $value;
}

/*
----------------------------------------
to-map()
----------------------------------------
Convert a single value to a USWDS
value map.

Candidate for deprecation if we remove
isReadable
----------------------------------------
*/

@function to-map($key, $values) {
  $l: length($values);

  @if $key == "noModifier" or $key == "noValue" {
    $key: "";
  }

  @return (slug: $key, content: $values);
}

/*
----------------------------------------
base-to-map()
----------------------------------------
Convert a single base to a USWDS
value map.

Candidate for deprecation if we remove
isReadable
----------------------------------------
*/

@function base-to-map($values) {
  $l: length($values);

  @if $l == 1 or nth($values, $l) != isReadable {
    @return (slug: $values, isReadable: true);
  } @else {
    $values: remove($values, isReadable);

    @return (slug: unquote(nth($values, 1)), isReadable: true);
  }
}

/*
----------------------------------------
ns()
----------------------------------------
Add a namesspace of $type if that
namespace is set to output
----------------------------------------
*/

@function ns($type) {
  $type: smart-quote($type);

  @if not map-deep-get($theme-namespace, $type, output) {
    @return "";
  }

  @return map-deep-get($theme-namespace, $type, namespace);
}

/*
----------------------------------------
de-list()
----------------------------------------
Transform a one-element list or arglist
into that single element.
----------------------------------------
(1) => 1
((1)) => (1)
----------------------------------------
*/

@function de-list($value) {
  $types: ("list", "arglist");

  @if not index($types, type-of($value)) {
    @return $value;
  }

  $output: if(length($value) == 1, nth($value, 1), $value);

  @return $output;
}

/*
----------------------------------------
unpack()
----------------------------------------
Create lists of single items from lists
of lists.
----------------------------------------
(1, (2.1, 2.2), 3) -->
(1, 2.1, 2.2, 3)
----------------------------------------
*/

@function unpack($value) {
  $output: ();

  @if length($value) == 0 {
    @return $value;
  }

  @each $i in $value {
    @if type-of($i) == "list" {
      @each $ii in $i {
        $output: append($output, $ii, comma);
      }
    } @else {
      $output: append($output, $i, comma);
    }
  }

  @return de-list($output);
}

/*
----------------------------------------
get-last()
----------------------------------------
Return the last item of a list,
Return null if the value is null
----------------------------------------
*/

@function get-last($props) {
  $length: length($props);
  $last: if($length == 0, null, nth($props, -1));

  @return $last;
}

/*
----------------------------------------
has-important()
----------------------------------------
Check to see if `!important` is
being passed in a mixin's props
----------------------------------------
*/

@function has-important($props) {
  $props: de-list($props);

  @if get-last($props) == "!important" {
    @return true;
  }

  @return false;
}

/*
----------------------------------------
append-important()
----------------------------------------
Append `!important` to a list
----------------------------------------
*/

@function append-important($source, $destination) {
  @if get-last($source) == "!important" {
    @return append($destination, !important, comma);
  }

  @return $destination;
}

/*
----------------------------------------
spacing-multiple()
----------------------------------------
Converts a spacing unit multiple into
the desired final units (currently rem)
----------------------------------------
*/

@function spacing-multiple($unit) {
  $grid-to-rem: ($system-spacing-grid-base * $unit) / $root-font-size-equiv *
    1rem;

  @return $grid-to-rem;
}

/*
----------------------------------------
rem-to-px()
----------------------------------------
Converts a value in rem to a value in px
----------------------------------------
*/

@function rem-to-px($value-in-rem) {
  @if unit($value-in-rem) == "rem" {
    $rem-to-px: ($value-in-rem / 1rem) * $root-font-size-equiv;
    @return $rem-to-px;
  }
  @if unit($value-in-rem) != "px" {
    @error 'This value must be in either px or rem';
  }
  @return $value-in-rem;
}

/*
----------------------------------------
rem-to-user-em()
----------------------------------------
Converts a value in rem to a value in
[user-settings] em for use in media
queries
----------------------------------------
*/

@function rem-to-user-em($grid-in-rem) {
  $rem-to-user-em: ($grid-in-rem / 1rem) * 1em;

  @return $rem-to-user-em;
}

/*
----------------------------------------
validate-typeface-token()
----------------------------------------
Check to see if a typeface-token exists.
Throw an error if a passed token does
not exist in the typeface-token map.
----------------------------------------
*/

@function validate-typeface-token($typeface-token) {
  @if not map-has-key($all-typeface-tokens, $typeface-token) {
    @error '`#{$typeface-token}` is not a valid typeface token. '
      + 'Valid tokens: #{map-keys($all-typeface-tokens)} ';
  }

  @return $typeface-token;
}

/*
----------------------------------------
cap-height()
----------------------------------------
Get the cap height of a valid typeface
----------------------------------------
*/

@function cap-height($typeface-token) {
  @if not $typeface-token {
    @return false;
  }

  $typeface-token: validate-typeface-token($typeface-token);
  $token-data: map-get($all-typeface-tokens, $typeface-token);
  @return map-get($token-data, "cap-height");
}

/*
----------------------------------------
px-to-rem()
----------------------------------------
Converts a value in px to a value in rem
----------------------------------------
*/

@function px-to-rem($pixels) {
  @if not $pixels {
    @return false;
  }
  $px-to-rem: ($pixels / $root-font-size-equiv) * 1rem;
  $px-to-rem: round($px-to-rem * 100) / 100;

  @return $px-to-rem;
}

/*
----------------------------------------
normalize-type-scale()
----------------------------------------
Normalizes a specific face's optical size
to a set target
----------------------------------------
*/

@function normalize-type-scale($cap-height, $scale) {
  @if not $cap-height {
    @return false;
  }

  $this-scale: $system-base-cap-height * strip-unit($scale) / $cap-height * 1px;

  @return px-to-rem($this-scale);
}

/*
----------------------------------------
utility-font()
----------------------------------------
Get a normalized font-size in rem from
a family and a type size in either
system scale or project scale
----------------------------------------
Not the public-facing function.
Used for building the utilities and
withholds certain errors.
----------------------------------------
*/

@function utility-font($family, $scale) {
  @if not map-has-key($project-cap-heights, $family) {
    @error '#{$family} is not a valid font family token. '
      + 'Valid tokens: #{map-keys($project-cap-heights)}';
  }

  $quote-scale: smart-quote($scale);

  @if not map-get($all-type-scale, $quote-scale) {
    @error '`#{$scale}` is not a valid font scale token. '
      + 'Valid tokens: #{map-keys($all-type-scale)}';
  }

  $this-cap: map-get($project-cap-heights, $family);
  $this-scale: map-get($all-type-scale, $quote-scale);

  @if not $this-scale and $this-cap {
    @return false;
  }

  @return normalize-type-scale($this-cap, $this-scale);
}

/*
----------------------------------------
line-height()
lh()
----------------------------------------
Get a normalized line-height from
a family and a line-height scale unit
----------------------------------------
*/

@function lh($props...) {
  $props: unpack($props);

  @if not(length($props) == 2) {
    @error 'lh() needs both a valid face and line height token '
      + 'in the format `lh(FACE, HEIGHT)`.';
  }

  $family: smart-quote(nth($props, 1));
  $scale: smart-quote(nth($props, 2));

  @if not map-has-key($project-cap-heights, $family) {
    @error '#{$family} is not a valid font family token. '
      + 'Valid tokens: #{map-keys($project-cap-heights)}';
  }

  @if not map-get($system-line-height, $scale) {
    @error '`#{$scale}` is not a valid line-height token. '
      + 'Valid tokens: #{map-keys($system-line-height)}';
  }

  @if not map-get($project-cap-heights, $family) {
    @return false;
  }

  $this-cap: map-get($project-cap-heights, $family);
  $this-line-height: map-get($system-line-height, $scale);
  $normalized-line-height: $this-line-height /
    ($system-base-cap-height / $this-cap);
  $normalized-line-height: round($normalized-line-height * 10) / 10;

  @return $normalized-line-height;
}

@function line-height($props...) {
  @return lh($props...);
}

/*
----------------------------------------
convert-to-font-type()
----------------------------------------
Converts a font-role token into a
font-type token. Leaves font-type tokens
unchanged.
----------------------------------------
*/

@function convert-to-font-type($token) {
  @if map-has-key($project-font-role-tokens, $token) {
    @return map-get($project-font-role-tokens, $token);
  }

  @return $token;
}

/*
----------------------------------------
get-font-stack()
----------------------------------------
Get a font stack from a style- or
role-based font token.
----------------------------------------
*/

@function get-font-stack($token) {
  // Start by converting to a type token (sans, serif, etc)
  $type-token: convert-to-font-type($token);
  $output-display-name: true;
  $this-stack: null;
  // Get the font type metadata
  $this-font-map: map-get($project-font-type-tokens, $type-token);
  // Only output if the font type has an assigned typeface token
  @if map-get($this-font-map, "typeface-token") {
    $this-font-token: map-get($this-font-map, "typeface-token");
    // Get the typeface metadata
    $this-typeface-data: map-get($all-typeface-tokens, $this-font-token);
    $this-name: map-get($this-typeface-data, "display-name");
    // If it's a system typeface, don't output the display name
    @if map-has-key($this-typeface-data, "system-font") {
      $output-display-name: false;
    }
    // If there's a custom stack, use it and output the display name
    @if map-get($this-font-map, "custom-stack") {
      $this-stack: map-get($this-font-map, "custom-stack");
      $output-display-name: true;
    }
    // Otherwise, just get the token's default stack
    @else {
      $this-stack: map-deep-get(
        $all-typeface-tokens,
        $this-font-token,
        "stack"
      );
    }
    // If the typeface has no display name (system fonts), don't output the display name
    @if map-get($this-typeface-data, "display-name") == null {
      $output-display-name: false;
    }
    @if not $output-display-name {
      @return #{$this-stack};
    }
    @return unquote("#{$this-name}, #{$this-stack}");
  }
  @return false;
}

/*
----------------------------------------
get-typeface-token()
----------------------------------------
Get a typeface token from a font-type or
font-role token.
----------------------------------------
*/

@function get-typeface-token($font-token) {
  $this-token: $font-token;
  @if map-has-key($project-font-role-tokens, $font-token) {
    $this-token: map-get($project-font-role-tokens, $font-token);
  }
  @return map-deep-get(
    $project-font-type-tokens,
    $this-token,
    "typeface-token"
  );
}

/*
----------------------------------------
get-system-color()
----------------------------------------
Derive a system color from its
family, value, and vivid or a passed
variable that is, itself, a list
----------------------------------------
*/

@function get-system-color(
  $color-family: false,
  $color-grade: false,
  $color-variant: false
) {
  // If the arg being passed to the fn
  // is a variable defined as a list,
  // $color-family will contain this
  // entire list, and needs to be
  // unpacked.
  // ex:
  //    in settings:
  //      $theme-color-primary.'dark': 'blue', 70
  //    in the theme colors map:
  //      $color-primary-dark: get-system-color($theme-color-primary.'dark'),

  @if type-of($color-family) == "list" {
    @if length($color-family) > 2 {
      $color-variant: nth($color-family, 3);
    }
    $color-grade: nth($color-family, 2);
    $color-family: nth($color-family, 1);
  }

  $color-family: smart-quote($color-family);
  $color-variant: smart-quote($color-variant);

  // If the arg being passed to the fn
  // is false, it should output as `false`
  // to preserve a false value in the
  // target map
  // ex:
  //    in settings:
  //      $theme-color-primary.'darkest': false;
  //    in the theme colors map:
  //      'darkest': get-system-color($theme-color-primary.'darkest'),
  //      'darkest': false, // is the desired outcome
  // TODO: should a false-pass color function be a separate fn?

  @if not $color-family {
    @return false;
  }

  @if $color-variant {
    $output: map-deep-get(
      $system-colors,
      $color-family,
      $color-variant,
      $color-grade
    );

    @return $output;
  }

  $output: map-deep-get($system-colors, $color-family, $color-grade);

  @return $output;
}

/*
----------------------------------------
system-type-scale()
----------------------------------------
Get a value from the system type scale
----------------------------------------
*/

@function system-type-scale($scale) {
  $scale: smart-quote($scale);

  @if not $scale {
    @return false;
  }

  @if not map-has-key($system-type-scale, $scale) {
    @error '`#{$scale}` is not a valid type scale token. '
      + 'Valid tokens: #{map-keys($system-type-scale)}';
  }

  @return map-get($system-type-scale, $scale);
}

/*
----------------------------------------
calc-gap-offset()
----------------------------------------
Calculate a valid uswds unit that is
half the width of a given unit, for
calculating gap offset in the layout
grid.
----------------------------------------
*/

@function calc-gap-offset($gap-size) {
  $gap-size: smart-quote($gap-size);

  @if not map-has-key($spacing-to-value, $gap-size) {
    @error '`#{$gap-size}` is not a valid USWDS gap size token.';
  }

  $numeric-eq: map-get($spacing-to-value, $gap-size);
  $numeric-eq-half: inspect($numeric-eq / 2);

  @if not map-has-key($spacing-to-token, $numeric-eq-half) {
    @error '`#{$gap-size}` is not a valid USWDS gap size token. '
      + 'Column gaps need to have a standard size half their width.';
  }

  @return map-get($spacing-to-token, $numeric-eq-half);
}

/*
----------------------------------------
get-standard-values()
----------------------------------------
Gets a map of USWDS standard values
for a property
----------------------------------------
*/

@function get-standard-values($property) {
  @return map-deep-get($system-properties, $property, standard);
}

/*
----------------------------------------
number-to-token()
----------------------------------------
Converts an integer or numeric value
into a system value

Ex: 0.5   --> '05'
    -1px  --> 'neg-1px'
----------------------------------------
*/

@function number-to-token($number) {
  $number: inspect($number);

  @if not map-has-key($number-to-value, $number) {
    @return false;
  }

  @return map-get($number-to-value, $number);
}

/*
----------------------------------------
columns()
----------------------------------------
outputs a grid-col number based on
the number of desired columns in the
12-column grid

Ex: columns(2) --> 6
    grid-col(columns(2))
----------------------------------------
*/

@function columns($number) {
  $options: "auto", "fill";
  $number: smart-quote($number);

  @if index($options, $number) {
    @return $number;
  }
  @if 12 % $number != 0 {
    @error '`#{$number}` must be a divisor of 12.';
  }
  $columns: 12 / $number;
  @return $columns;
}

/*
----------------------------------------
get-uswds-value()
----------------------------------------
Finds and outputs a value from the
USWDS standard values.

Used to build other standard utility
functions and mixins.
----------------------------------------
*/

@function get-uswds-value($property, $value...) {
  @if type-of($value) == "arglist" and nth($value, 1) == override {
    @return nth($value, 2);
  }

  $value: nth($value, 1);
  $converted: number-to-token($value);
  $quoted-value: if(
    $converted,
    smart-quote($converted),
    smart-quote(nth($value, 1))
  );
  $our-standard-values: map-deep-get($system-properties, $property, standard);
  $our-extended-values: map-deep-get($system-properties, $property, extended);

  @if map-has-key($our-standard-values, $quoted-value) {
    $output: map-get($our-standard-values, $quoted-value);

    @if not $output {
      @if $theme-show-compile-warnings {
        @error '`#{$value}` is set as a `false` value '
          + 'for the #{$property} property in your project settings '
          + 'and will not output properly. '
          + 'Set the value of `#{$value}` in project settings.';
      }
    }

    @return $output;
  }

  @if map-has-key($our-extended-values, $quoted-value) {
    @if $theme-show-compile-warnings {
      @warn '`#{$value}` is an extended USWDS `#{$property}` token. '
        + 'This is OK, but only components built with standard tokens can be accepted back into the system. '
        + 'Standard `#{$property}` values: #{map-keys($our-standard-values)}';
    }

    @return map-get($our-extended-values, $quoted-value);
  }

  // TODO: what are these last two cases? Evaluate.
  @if not(type-of($value) == "number" and not unitless($value)) {
    @error '`#{$value}` is not a valid `#{$property}` token. '
      + 'You should correct this. Standard `#{$property}` tokens: '
      + ' #{map-keys($our-standard-values)}';
  }

  @if $theme-show-compile-warnings {
    @warn '`#{$value}` is not a USWDS `#{$property}` token. '
      + 'This is OK, but only components built with standard '
      + 'tokens can be accepted back into the system. '
      + 'Standard `#{$property}` values: #{map-keys($our-standard-values)}';
  }

  @return $value;
}

/*
----------------------------------------
pow()
----------------------------------------
Raises a unitless number to the power
of another unitless number

Includes helper functions
----------------------------------------
*/

@function pow($number, $exponent) {
  @if (round($exponent) != $exponent) {
    @return exp($exponent * ln($number));
  }

  $value: 1;

  @if $exponent > 0 {
    @for $i from 1 through $exponent {
      $value: $value * $number;
    }
  } @else if $exponent < 0 {
    @for $i from 1 through -$exponent {
      $value: $value / $number;
    }
  }

  @return $value;
}

@function factorial($value) {
  $result: 1;

  @if $value == 0 {
    @return $result;
  }

  @for $index from 1 through $value {
    $result: $result * $index;
  }

  @return $result;
}

@function summation($iteratee, $input, $initial: 0, $limit: 100) {
  $sum: 0;

  @for $index from $initial to $limit {
    $sum: $sum + call($iteratee, $input, $index);
  }

  @return $sum;
}

@function exp-maclaurin($x, $n) {
  @return (pow($x, $n) / factorial($n));
}

@function exp($value) {
  @return summation(get-function("exp-maclaurin"), $value, 0, 100);
}

@function ln-maclaurin($x, $n) {
  @return (pow(-1, $n + 1) / $n) * (pow($x - 1, $n));
}

@function ln($value) {
  $ten-exp: 1;
  $ln-ten: 2.30258509;

  @while ($value > pow(10, $ten-exp)) {
    $ten-exp: $ten-exp + 1;
  }

  @return summation(
      get-function("ln-maclaurin"),
      $value / pow(10, $ten-exp),
      1,
      100
    ) + $ten-exp * $ln-ten;
}

/// Returns the luminance of `$color` as a float (between 0 and 1)
/// 1 is pure white, 0 is pure black
/// @param {Color} $color - Color
/// @return {Number}
/// @link http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef Reference
@function luminance($color) {
  $colors: (
    "red": red($color),
    "green": green($color),
    "blue": blue($color),
  );

  @each $name, $value in $colors {
    $adjusted: 0;
    $value: $value / 256;

    @if $value < 0.03928 {
      $value: $value / 12.92;
    } @else {
      $value: ($value + 0.055) / 1.055;
      $value: pow($value, 2.4);
    }

    $colors: map-merge(
      $colors,
      (
        $name: $value,
      )
    );
  }

  $lum: (map-get($colors, "red") * 0.2126) +
    (map-get($colors, "green") * 0.7152) + (map-get($colors, "blue") * 0.0722);
  $lum: round($lum * 1000) / 1000;

  @return $lum;
}

/// Casts a string into a number
///
/// @param {String | Number} $value - Value to be parsed
///
/// @return {Number}
///
@function to-number($value) {
  @if type-of($value) == "number" {
    @return $value;
  } @else if type-of($value) != "string" {
    $_: log("Value for `to-number` should be a number or a string.");
  }

  $result: 0;
  $digits: 0;
  $minus: str-slice($value, 1, 1) == "-";
  $numbers: (
    "0": 0,
    "1": 1,
    "2": 2,
    "3": 3,
    "4": 4,
    "5": 5,
    "6": 6,
    "7": 7,
    "8": 8,
    "9": 9,
  );

  @for $i from if($minus, 2, 1) through str-length($value) {
    $character: str-slice($value, $i, $i);

    @if not(index(map-keys($numbers), $character) or $character == ".") {
      @return to-length(if($minus, -$result, $result), str-slice($value, $i));
    }

    @if $character == "." {
      $digits: 1;
    } @else if $digits == 0 {
      $result: $result * 10 + map-get($numbers, $character);
    } @else {
      $digits: $digits * 10;
      $result: $result + map-get($numbers, $character) / $digits;
    }
  }

  @return if($minus, -$result, $result);
}

/*
----------------------------------------
decompose()
----------------------------------------
Convert a color token into into a list
of form [family], [grade], [variant]

Vivid variants return "vivid" as the
variant.

If neither grade nor variant exists,
returns 'null'
----------------------------------------
*/

@function decompose($token) {
  $separator: "-";
  $family: false;
  $grade: false;
  $variant: false;
  $exceptions: (
    "black": 100,
    "white": 0,
  );

  $token: get-color-token-assignment($token);
  $split: str-split($token, $separator);
  $grade: nth($split, length($split));

  @if str-index($grade, "v") {
    $variant: "vivid";
    $grade: str-slice($grade, 1, (str-index($grade, "v") - 1));
  }

  @if length($split) == 3 {
    $family: nth($split, 1) + $separator + nth($split, 2);
  } @else {
    $family: nth($split, 1);
  }

  $grade: to-number($grade);

  @if map-has-key($exceptions, $family) {
    $grade: map-get($exceptions, $family);
  }

  @return $family, $grade, $variant;
}

/*
----------------------------------------
test-colors()
----------------------------------------
Check to see if all system colors
fall between the proper relative
luminance range for their grade.

Has a couple quirks, as the luminance()
function returns slightly different
results than expected.
----------------------------------------
*/

@function test-colors($map) {
  $exceptions: "black", "white", "transparent", "black-transparent",
    "white-transparent";

  @each $token, $value in $map {
    $family: nth(decompose($token), 1);
    $grade: nth(decompose($token), 2);
    @if not $value {
      // empty block
    } @else if not index($exceptions, $family) {
      $computed: get-color-grade($value);
      @debug "Checked #{$family}-#{$grade}";
      @if $grade <= 5 {
        // empty block
      } @else if $computed != $grade {
        @warn "#{$token} (#{$value}) lum: #{luminance($value)} is not in the range #{map-get($system-luminance-grade-ranges, $grade)}";
      }
    }
  }

  @return 1;
}

/*
----------------------------------------
str-split()
----------------------------------------
Split a string at a given separator
and convert into a lisrt of substrings
----------------------------------------
*/

@function str-split($string, $separator) {
  $split-arr: ();
  $index: str-index($string, $separator);
  @while $index != null {
    $item: str-slice($string, 1, $index - 1);
    $split-arr: append($split-arr, $item);
    $string: str-slice($string, $index + 1);
    $index: str-index($string, $separator);
  }
  $split-arr: append($split-arr, $string);

  @return $split-arr;
}

/*
----------------------------------------
str-replace()
----------------------------------------
Replace any substring with another
string
----------------------------------------
*/

@function str-replace($string, $search, $replace: "") {
  $index: str-index($string, $search);

  @if $index {
    @return str-slice($string, 1, $index - 1) + $replace +
      str-replace(
        str-slice($string, $index + str-length($search)),
        $search,
        $replace
      );
  }

  @return $string;
}

/*
----------------------------------------
get-color-token-assignment()
----------------------------------------
Get the system token equivalent of any
theme color token
----------------------------------------
*/

@function get-color-token-assignment($color-token) {
  $system-token: $color-token;
  $grade: null;

  @if map-has-key($assignments-theme-color, $color-token) {
    $system-token: map-get($assignments-theme-color, $system-token);
  } @else if not map-has-key($system-color-shortcodes, $color-token) {
    @error "'#{$color-token}' is not a valid color token.";
  }

  @return $system-token;
}

/*
----------------------------------------
get-color-grade()
----------------------------------------
Derive the grade equivalent any color,
even non-token colors
----------------------------------------
*/

@function get-color-grade($color-token) {
  $grade: null;
  $lum: null;
  $color: false;

  @if type-of($color-token) == "color" {
    $color: $color-token;
  } @else if type-of(get-color-token-assignment($color-token)) == "color" {
    $color: get-color-token-assignment($color-token);
  }

  @if $color {
    $lum: luminance($color);

    @each $grade, $range in $system-luminance-grade-ranges {
      $min: nth($range, 1);
      $max: nth($range, 2);
      $next-max: false;
      @if $grade < 100 {
        @if $grade == 5 {
          $next-max: nth(map-get($system-luminance-grade-ranges, 10), 2);
        } @else {
          $next-max: nth(
            map-get($system-luminance-grade-ranges, ($grade + 10)),
            2
          );
        }
      }
      @if ($lum >= $min) and ($lum <= $max) {
        @return $grade;
      }
      @if $next-max and ($lum < $min) and ($lum > $next-max) {
        @return $grade + 4.9;
      }
    }
  }

  $system-token: get-color-token-assignment($color-token);
  $grade: nth(decompose($system-token), 2);
  @return $grade;
}

/*
----------------------------------------
color()
----------------------------------------
Derive a color from a color shortcode
----------------------------------------
*/

@function color($value, $flags...) {
  $value: unpack($value);

  // Non-token colors may be passed with specific flags
  @if type-of($value) == color {
    // override or set-theme will allow any color
    @if index($flags, override) or index($flags, set-theme) {
      // override + no-warn will skip warnings
      @if index($flags, no-warn) {
        @return $value;
      }

      @if $theme-show-compile-warnings {
        @warn 'Override: `#{$value}` is not a USWDS color token.';
      }

      @return $value;
    }
  }

  // False values may be passed through when setting theme colors
  @if $value == false {
    @if index($flags, set-theme) {
      @return $value;
    }
  }

  // Now, any value should be evaluated as a token

  $value: smart-quote($value);

  @if map-has-key($system-color-shortcodes, $value) {
    $our-color: map-get($system-color-shortcodes, $value);
    @if $our-color == false {
      @error '`#{$value}` is a color that does not exist '
        + 'or is set to false.';
    }
    @return $our-color;
  }

  // If we're using the theme flag, $project-color-shortcodes has not yet been set
  @if not index($flags, set-theme) {
    @if map-has-key($project-color-shortcodes, $value) {
      $our-color: (map-get($project-color-shortcodes, $value));
      @if $our-color == false {
        @error '`#{$value}` is a color that does not exist '
          + 'or is set to false.';
      }
      @return $our-color;
    }
  }

  @error '`#{$value}` is not a valid USWDS color token. '
      + 'See designsystem.digital.gov/design-tokens/color '
      + 'for more information.';
}

/*
----------------------------------------
advanced-color()
----------------------------------------
Derive a color from a color triplet:
[family], [grade], [variant]
----------------------------------------
*/

// color() can have a 1, 2, or 3 arguments passed to it:
//
// [family]
// ex: color('primary')
//     - the default in a theme palette family
//
// [family], [grade]
// ex: color('red', 50)
//     - a standard system color
// ex: color('accent-warm', 'light')
//     - a standard theme color
// ex: color('primary', 'vivid')
//     - in theme colors, 'vivid' is considered a grade
//
// [family], [grade], [vivid]
// ex: color('red', 50, 'vivid')
//     - a vivid system color
//     - only system colors required three arguments

@function advanced-color(
  $color-family: false,
  $color-grade: false,
  $color-variant: false
) {
  // Convert any arglists into lists
  $color-family: if(
    type-of($color-family) == "arglist",
    unpack($color-family),
    $color-family
  );

  // If $color-family is a list, color() had a variable
  // passed to it, and args need to be re-set with the
  // values from the $color-family list:
  @if type-of($color-family) == "list" {
    @if length($color-family) > 2 {
      $color-variant: nth($color-family, 3);
    }
    $color-grade: nth($color-family, 2);
    $color-family: nth($color-family, 1);
  }

  // Set initial state of vars
  $color-family: smart-quote($color-family);
  $color-grade: smart-quote($color-grade);
  $color-variant: smart-quote($color-variant);

  // @debug '#{$color-family}: #{type-of($color-family)}, #{$color-grade}: #{type-of($color-grade)}, #{$color-variant}: #{type-of($color-variant)}' ;

  // If there are no args, throw an error
  @if not $color-family {
    @error 'Include a color in the form [family], [grade], [vivid]';
  }

  // If the grade is a number, it's a system color
  // ex: ('red', 50)
  @if type-of($color-grade) == "number" {
    @return get-system-color($color-family, $color-grade, $color-variant);
  }

  // non-number grades are associated with non-default theme colors
  // ex: ('base', 'darker')
  // default theme colors have no grade
  // ex: ('base')
  @if map-has-key($all-project-colors, $color-family) {
    @if not
      map-has-key(map-get($all-project-colors, $color-family), $color-grade)
    {
      @error '`#{$color-grade}` is not a valid grade of `#{$color-family}`. '
        + 'Valid grades: '
        + '#{map-keys(map-get($all-project-colors, $color-family))}';
    }
  } @else {
    @error '`#{$color-family}` is not a valid theme family token. '
      + 'Valid family tokens: #{map-keys($all-project-colors)}';
  }
  @return map-deep-get($all-project-colors, $color-family, $color-grade);
}

/*
----------------------------------------
units()
----------------------------------------
Converts a spacing unit into
the desired final units (currently rem)
----------------------------------------
*/

@function units($value) {
  $converted: if(
    type-of($value) == "string",
    quote($value),
    number-to-token($value)
  );

  @if not map-has-key($project-spacing-standard, $converted) {
    @error '`#{$value}` is not a valid spacing unit token. '
      + 'Valid spacing unit tokens: '
      + '#{map-keys($project-spacing-standard)}';
  }

  @return map-get($project-spacing-standard, $converted);
}

/*
----------------------------------------
get-palettes()
----------------------------------------
Build a single map of plugin values
from a list of plugin keys.
----------------------------------------
*/

@function get-palettes($list) {
  $our-palettes: ();

  @if type-of($list) == "map" {
    @error 'Use a list of strings as plugin values.';
  }

  @each $palette in $list {
    @if not map-has-key($palette-registry, $palette) {
      @error '#{$palette} isn\'t in the registry.';
    }

    $our-palettes: map-merge(
      $our-palettes,
      map-get($palette-registry, $palette)
    );
  }

  @return $our-palettes;
}

/*
----------------------------------------
border-radius()
----------------------------------------
Get a border-radius from the system
border-radii
----------------------------------------
*/

@function border-radius($value) {
  @if map-has-key($all-border-radius, $value) {
    @return map-get($all-border-radius, $value);
  } @else {
    @error '`#{$value}` is not a valid border radius token. '
      + 'Valid tokens: #{map-keys($all-border-radius)}';
  }
}

/*
----------------------------------------
font-weight()
fw()
----------------------------------------
Get a font-weight value from the
system font-weight
----------------------------------------
*/

@function font-weight($value) {
  @return get-uswds-value(font-weight, $value);
}

@function fw($value) {
  @return font-weight($value);
}

/*
----------------------------------------
feature()
----------------------------------------
Gets a valid USWDS font feature setting
----------------------------------------
*/

@function feature($value) {
  @return get-uswds-value(feature, $value);
}

/*
----------------------------------------
flex()
----------------------------------------
Gets a valid USWDS flex value
----------------------------------------
*/

@function flex($value) {
  @return get-uswds-value(flex, $value);
}

/*
----------------------------------------
font-family()
family()
----------------------------------------
Get a font-family stack from a
role-based or type-based font family
----------------------------------------
*/

@function font-family($value) {
  @return get-uswds-value(font-family, $value);
}

@function ff($value) {
  @return font-family($value);
}

@function family($value) {
  @return font-family($value);
}

/*
----------------------------------------
letter-spacing()
ls()
----------------------------------------
Get a letter-spacing value from the
system letter-spacing
----------------------------------------
*/

@function letter-spacing($value) {
  $lh-map: map-get($system-properties, letter-spacing);
  $fn-map: map-get($lh-map, function);
  @if map-has-key($fn-map, $value) {
    @return map-get($fn-map, $value);
  }
  @if type-of($value) == "number" {
    @error '`#{$value}` is a not a valid letter-spacing token. '
      + 'Valid letter-spacing tokens: #{map-keys($fn-map)}';
  }
  @return get-uswds-value(letter-spacing, $value);
}

@function ls($value) {
  @return letter-spacing($value);
}

/*
----------------------------------------
measure()
----------------------------------------
Gets a valid USWDS reading line length
----------------------------------------
*/

@function measure($value) {
  @return get-uswds-value(measure, $value);
}

/*
----------------------------------------
opacity()
----------------------------------------
Get an opacity from the system
opacities
----------------------------------------
*/

@function opacity($value) {
  @return get-uswds-value(opacity, $value);
}

/*
----------------------------------------
order()
----------------------------------------
Get an order value from the
system orders
----------------------------------------
*/

@function order($value) {
  @return get-uswds-value(order, $value);
}

/*
----------------------------------------
radius()
----------------------------------------
Get a border-radius value from the
system letter-spacing
----------------------------------------
*/

@function radius($value) {
  @return get-uswds-value(border-radius, $value);
}

/*
----------------------------------------
font-size()
----------------------------------------
Get type scale value from a [family] and
[scale]
----------------------------------------
*/

@function font-size($family, $scale, $force: false) {
  $our-family: smart-quote($family);
  $our-scale: smart-quote($scale);

  @if not map-has-key($project-cap-heights, $our-family) {
    @error '#{$our-family} is not a valid font family token. '
      + 'Valid tokens: #{map-keys($project-cap-heights)}';
  }
  @if not map-get($all-type-scale, $our-scale) {
    @error '`#{$our-scale}` is not a valid font scale token. '
      + 'Valid token: #{map-keys($all-type-scale)}';
  }

  $this-cap: map-get($project-cap-heights, $our-family);
  $this-scale: map-get($all-type-scale, $our-scale);

  @if not $force {
    @if not($this-scale and $this-cap) {
      @error 'The scale `#{$our-scale}` is disabled '
        + 'in your project\'s theme settings. '
        + 'Set its value to `true` to use this family.';
    }
  }

  @return normalize-type-scale($this-cap, $this-scale);
}

@function fs($family, $scale) {
  @return font-size($family, $scale);
}

@function size($family, $scale) {
  @return font-size($family, $scale);
}

/*
----------------------------------------
z-index()
z()
----------------------------------------
Get a z-index value from the
system z-index
----------------------------------------
*/

@function z-index($value) {
  @return get-uswds-value(z-index, $value);
}

@function z($value) {
  @return z-index($value);
}

@function get-token-from-bg(
  $bg-color,
  $preferred-text-color: "white",
  $fallback-text-color: "ink",
  $wcag-target: "AA"
) {
  $magic-numbers: (
    "AA": 50,
    "AAA": 70,
    "AA-large": 40,
  );
  $target-magic-number: map-get($magic-numbers, $wcag-target);
  $grade-bg: get-color-grade($bg-color);
  $grade-preferred: get-color-grade($preferred-text-color);
  $magic-num-preferred: abs($grade-bg - $grade-preferred);
  $color: false;

  //@debug "Background grade: #{$grade-bg} | Preferred text grade: #{$grade-preferred} | Magic number: #{$magic-num-preferred} | Target: #{$target-magic-number}";

  @if $magic-num-preferred >= $target-magic-number {
    $color: $preferred-text-color;
  } @else {
    $grade-fallback: get-color-grade($fallback-text-color);
    $magic-num-fallback: abs($grade-bg - $grade-fallback);
    $color: $fallback-text-color;
  }

  @if not $color {
    @error "Neither '#{$preferred-text-color}' nor '#{$fallback-text-color}' have #{$wcag-target} contrast on a '#{$bg-color}' background.";
  }

  @return $color;
}

@function get-color-from-bg(
  $bg-color,
  $preferred-text-color: "white",
  $fallback-text-color: "ink",
  $wcag-target: "AA"
) {
  $color: get-token-from-bg(
    $bg-color,
    $preferred-text-color,
    $fallback-text-color,
    $wcag-target
  );
  @return color($color);
}

@function get-link-tokens-from-bg(
  $bg-color,
  $preferred-link-color: $theme-link-color,
  $fallback-link-color: $theme-link-reverse-color,
  $wcag-target: "AA"
) {
  $magic-numbers: (
    "AA": 50,
    "AAA": 70,
    "AA-large": 40,
  );
  $grade-step: 10;
  $found: false;
  $decomposed: false;

  @if $preferred-link-color == default {
    $preferred-link-color: $theme-link-color;
  }

  $target-magic-number: map-get($magic-numbers, $wcag-target);
  $bg-grade: get-color-grade($bg-color);
  $our-color-tokens: ($preferred-link-color, $fallback-link-color);

  $link-token: false;
  $hover-token: false;

  @each $color-token in $our-color-tokens {
    //@debug "color token: " + $color-token;
    // If the color token is a custom color, set a $custom flag
    $custom: if(
      type-of(map-get($assignments-theme-color, $color-token)) == "color",
      true,
      false
    );

    // Only get a link color if one has not yet been found
    @if not $found {
      $link-grade-token: get-color-grade($color-token);
      $link-grade: if($link-grade-token < 10, 0, $link-grade-token);
      $link-magic-number: abs($bg-grade - $link-grade);
      $token-darker: false;
      $token-lighter: false;
      $link-family: false;
      $link-vivid: false;
      $hover-grade: false;
      $hover-vivid: false;

      // If the link color is custom, output theme tokens, not system tokens
      @if $custom {
        //@debug "uses custom color.";
        $custom-token: $color-token;
        $custom-token-lighter: false;
        $custom-token-darker: false;
        $custom-split: str-split($custom-token, "-");
        $custom-grade: false;
        $custom-grade-lighter: false;
        $custom-grade-darker: false;
        //@debug "custom split:" + $custom-split;

        // set family as the first string in the split
        $custom-family: nth($custom-split, 1);

        // ignore vivid in token calculations, treat as default
        @if index($custom-split, "vivid") {
          $custom-split: remove($custom-split, "vivid");
        }

        // set family and grade for "accent" families, since their family includes the split character
        @if $custom-family == "accent" {
          $custom-family: $custom-family + "-" + nth($custom-split, 2);
          $custom-grade: if(
            length($custom-split) == 3,
            nth($custom-split, 3),
            "default"
          );
        } @else {
          $custom-grade: if(
            length($custom-split) == 2,
            nth($custom-split, 2),
            "default"
          );
        }

        //@debug "custom family: " + $custom-family;
        //@debug "custom grade: " + $custom-grade;

        $custom-family-lighter: $custom-family;
        $custom-family-darker: $custom-family;
        $custom-grade-index: index($uswds-color-theme-grades, $custom-grade);

        // If it's the lightest grade, use "white" for the lighter family
        @if $custom-grade-index == 1 {
          $custom-family-lighter: "white";
        } @else {
          $custom-grade-lighter: nth(
            $uswds-color-theme-grades,
            ($custom-grade-index - 1)
          );
        }
        //@debug "lighter grade: " + $custom-grade-lighter;
        // If it's the darkest grade, use "black" for the lighter family
        @if $custom-grade-index == length($uswds-color-theme-grades) {
          $custom-family-darker: "black";
        } @else {
          $custom-grade-darker: nth(
            $uswds-color-theme-grades,
            ($custom-grade-index + 1)
          );
        }
        //@debug "darker grade: " + $custom-grade-darker;

        // If any calculated grade is "default", don't output the grade
        $custom-grade-darker: if(
          $custom-grade-darker == "default",
          false,
          $custom-grade-darker
        );
        $custom-grade-lighter: if(
          $custom-grade-lighter == "default",
          false,
          $custom-grade-lighter
        );

        // Build the custom lighter and darker tokens
        $token-darker: if(
          $custom-grade-darker,
          $custom-family + "-" + $custom-grade-darker,
          $custom-family-darker
        );
        $token-lighter: if(
          $custom-grade-lighter,
          $custom-family + "-" + $custom-grade-lighter,
          $custom-family-lighter
        );
      } @else {
        //@debug "not custom";
        $decomposed: decompose($color-token);
      }

      @if $link-grade == 0 {
        @warn 'Tokens with grades less than 10 (including "white") aren\'t valid link color tokens, since they have no lighter hover states.';
      } @else if $link-grade == 100 {
        @warn '"black" isn\'t a valid link color token, since it has no darker hover state.';
      }

      // Check that link meets contrast target
      @else if $link-magic-number >= $target-magic-number {
        $found: true;
        // Calculate additional link properties

        $link-token: $color-token;
        @if not $custom {
          $link-family: nth($decomposed, 1);
          //@debug "link family: " + $link-family;
          $link-vivid: "";
          @if nth($decomposed, 3) {
            $link-vivid: "v";
          }
        }

        // If link is darker than bg, use darker hover
        // Exclude black as it has no darker hover
        @if ($link-grade > $bg-grade) and ($link-grade != 100) {
          //@debug "Link is darker than background";
          @if $token-darker {
            //@debug "Getting darker token...";
            $hover-token: $token-darker;
          } @else {
            $hover-grade: $link-grade + $grade-step;
            $hover-vivid: if($hover-grade == 90, "", $link-vivid);
            $hover-token: if(
              $hover-grade == 100,
              "black",
              #{$link-family}-#{$hover-grade}#{$hover-vivid}
            );
          }
        }

        // If link is lighter than bg, use lighter hover
        // Exclude white equivalents as they have no lighter hover
        @else if ($link-grade != 0) and ($link-grade != 100) {
          //@debug "Link is lighter than background";
          @if $token-lighter {
            //@debug "Getting lighter token...";
            $hover-token: $token-lighter;
          } @else {
            $hover-grade: $link-grade - $grade-step;
            $hover-token: if(
              $hover-grade == 0,
              "white",
              #{$link-family}-#{$hover-grade}#{$link-vivid}
            );
          }
        }
      }
    }
  }

  @if not $hover-token {
    @error 'Neither "#{$preferred-link-color}" nor "#{$fallback-link-color}" can be #{$wcag-target} contrast links and hovers on a "#{$bg-color}" background.';
  }

  //@debug "#{$link-token}, #{$hover-token}";
  @return $link-token, $hover-token;
}
