//
// Copyright 2021 Google LLC
// SPDX-License-Identifier: Apache-2.0
//

// Utility functions for "var()" custom property string manipulation.

// go/keep-sorted start by_regex='(.+) prefix_order=sass:
@use 'sass:map';
@use 'sass:meta';
@use 'sass:string';
@use 'assert';
@use 'string_ext';
@use 'throw';
// go/keep-sorted end

/// Creates a custom property `var()` string.
///
/// @example scss
///   @debug var.create(--foo); // "var(--foo)"
///   @debug var.create(--foo, 8px); // "var(--foo, 8px)"
///
/// @param {string} $name - The name of the custom property.
/// @param {*} $fallback [null] - Optional `var()` fallback value.
/// @return {string} A custom property `var()` string.
@function create($name, $fallback: null) {
  $name: assert.is-type($name, 'string');
  @if throw.get-error($name) {
    @return $name;
  }
  @if not string_ext.starts-with($name, '--') {
    @return throw.error(
      'Custom property names must start with "--". $name: #{meta.inspect($name)}',
      $source: 'var.create'
    );
  }

  @if $fallback == null {
    @return var(#{$name});
  }

  @return var(#{$name}, #{$fallback});
}

/// Returns the custom property variable name of `var()` string.
///
/// @example scss
///   $var: var(--foo);
///   @debug var.name($var); // "--foo"
///
/// @param {string} $var - A custom property `var()` string.
/// @return {string} The custom property variable name.
/// @throw If the value is not a custom property.
@function name($var) {
  $var: _parse-and-validate($var);
  @if throw.get-error($var) {
    @return $var;
  }
  @return map.get($var, 'name');
}

/// Returns the fallback value of a custom property `var()` string. The value
/// may be null if the `var()` does not have a fallback value.
///
/// @example scss
///   $var: var(--foo, var(--bar, 8px));
///   @debug var.fallback($var); // "var(--bar, 8px)"
///
/// @param {string} $var - A custom property `var()` string.
/// @return {string} The fallback value of the `var()` string. May be null if
///     the `var()` does not have a fallback value.
/// @throw If the value is not a custom property.
@function fallback($var) {
  $var: _parse-and-validate($var);
  @if throw.get-error($var) {
    @return $var;
  }
  @return map.get($var, 'fallback');
}

/// Returns the deep fallback value of a custom property `var()` string. The
/// value may be null if the `var()` does not have a fallback value.
///
/// If a fallback value is another `var()`, this function will return the final
/// concrete value in the chain.
///
/// @example scss
///   $var: var(--foo, var(--bar, 8px));
///   @debug var.deep-fallback($var); // "8px"
///
/// @param {string} $var - A custom property `var()` string.
/// @return {string} The deep fallback value of the `var()` string. May be null
///     if the `var()` does not have a fallback value.
/// @throw If the value is not a custom property.
@function deep-fallback($var) {
  $fallback: fallback($var);
  @while is-var($fallback) {
    $fallback: fallback($fallback);
  }

  @return $fallback;
}

/// Creates a new custom property `var()` string and returns it with the
/// specified new fallback value.
///
/// @example scss
///   $var: var(--foo, var(--bar, 8px));
///   $new-var: set-fallback($var, 16px);
///   @debug $new-var; // "var(--foo, 16px)"
///
/// @param {string} $var - A custom property `var()` string.
/// @param {*} $new-fallback - The new fallback value. May be null to remove a
///     value.
/// @return {string} A custom property `var()` string with the new fallback
///     value.
/// @throw If the value is not a custom property.
@function set-fallback($var, $new-fallback) {
  $name: name($var);
  @return create($name, $new-fallback);
}

/// Creates a new custom property `var()` string and returns it with the
/// specified new deep fallback value.
///
/// If the provided `var()` string's fallback value is another `var()`, this
/// function will set the final fallback value in the chain.
///
/// @example scss
///   $var: var(--foo, var(--bar, 8px));
///   $new-var: var.deep-set-fallback($var, 16px);
///   @debug $new-var; // "var(--foo, var(--bar, 16px))"
///
/// @param {string} $var - A custom property `var()` string.
/// @param {*} $new-fallback - The new fallback value. May be null to remove a
///     value.
/// @return {string} A custom property `var()` string with the new deep
///     fallback value.
/// @throw If the value is not a custom property.
@function deep-set-fallback($var, $new-fallback) {
  $old-fallback: fallback($var);
  @if is-var($old-fallback) {
    $new-fallback: deep-set-fallback($old-fallback, $new-fallback);
  }

  @return set-fallback($var, $new-fallback);
}

/// Indicates whether or not a value is a custom property `var()` string.
///
/// @example scss
///   $var: var(--foo);
///   @debug var.is-var($var); // true
///
/// @param {*} $var - The value to test.
/// @return {boolean} True if the value is a custom property `var()` string, or
///     false if not.
@function is-var($var) {
  @return _parse($var) != null;
}

/// Indicates whether or not a value is a `var()` string.
///
/// @param {*} $var - The value to test.
/// @return {boolean} True if the value is a custom property `var()` string, or
///     false if not.
@function _is-var-string($var) {
  @return meta.type-of($var) == 'string' and
    string_ext.starts-with($var, 'var(');
}

/// Parses a `var()` string into a Map with `name` and `fallback` keys. This
/// function returns null if the value is invalid.
///
/// @param {*} $var - The value to parse.
/// @return {map} A Map containing a string `name` key with the custom property
///     name and a `fallback` key with the fallback value (which may be null).
///     The returned Map itself may be null if the provided value is not valid.
@function _parse($var) {
  @if meta.type-of($var) ==
    'map' and
    map.has-key($var, 'name') and
    map.has-key($var, 'fallback')
  {
    @return $var;
  }

  @if not _is-var-string($var) {
    @return null;
  }

  // Remove function name and parens
  $var: string_ext.replace-start($var, 'var(', '');
  $var: string_ext.replace-end($var, ')', '');

  $name: string_ext.trim($var);
  $fallback: null;
  $comma: string.index($var, ',');
  @if $comma != null {
    $name: string_ext.trim(string.slice($var, 1, $comma - 1));
    $fallback: string_ext.trim(string.slice($var, $comma + 1));
  }

  @if $name == '' or $fallback == '' {
    @return null;
  }

  @return ('name': $name, 'fallback': $fallback);
}

/// Parses a `var()` string into a Map with `name` and `fallback` keys.
///
/// @param {*} $var - The value to parse.
/// @return {map} A Map containing a string `name` key with the custom property
///     name and a `fallback` key with the fallback value (which may be null).
/// @throw If the value is not a custom property.
@function _parse-and-validate($var) {
  $var-map: _parse($var);
  @if $var-map == null {
    @return throw.error(
      '#{meta.inspect($var)} is not a valid var() string',
      $source: 'var._parse-and-validate'
    );
  }

  @return $var-map;
}
