@use 'sass:meta';
@use 'sass:string';
@use 'sass:list';
@use 'sass:math';
@use '../../functions/equals-false' as *;
@use '../../functions/equals-true' as *;
@use '../../functions/is-alias' as *;
@use '../../functions/is-falsey' as *;
@use '../../functions/is-global-value' as *;
@use '../../functions/is-list' as *;
@use '../../functions/is-number' as *;
@use '../../functions/is-string' as *;
@use '../../functions/list-to-string' as *;

/// Animates text by tracking it out of view. It is recommended to use the
/// `animation-stage()` mixin on the parent element.
///
/// @param {Bool | String} $expand-contract ['contract'] - Determines whether
/// the text will, `expand` (or `grow` or `true`) or `contract` (or `shrink` or
/// `false`) as it animates.
/// @param {Bool} $animate-in-place [true] - If true, the animation will
/// animate in its location on a 2-dimaensional plane. If false, it will allow
/// the animation to track towards the $destination, appearing to move forwards
/// on an `expand` animation, and to move backwards on `contract` animations.
/// @param {String} $destination [null] - The destination the animation appears
/// to be expanding or contracting towards. Can be `null`, `top`, or `bottom`.
/// @param {Number} $duration [0.9s] - The duration of the animation (`s` or `ms`).
/// @param {Number} $delay [null] - The optional delay time for the animation.
/// @param {Number | String} $iterations [null] - The number of iterations
/// to complete. Sets the value for the `animation-iteration-count` property.
/// Can be `infinite`, any unitless number, or any CSS global value.
/// @param {String} $anim-dir [null] - Sets the value for the
/// `animation-direction` property. Can be `normal`, `reverse`, `alternate`, or
/// `alternate-reverse`. Can also be any CSS global value. Default of `null`
/// assumes the CSS property default of `normal`.
/// @param {Timing-Function} $timing-func [cubic-bezier(0.55, 0.085, 0.68, 0.53)] -
/// The timing function for the animation. Using the default is recommended.
/// @param {Number | String} $anim-num [null] - If the mixin is used more than
/// once with different values in a stylesheet, you can pass a number or string
/// to the mixin here that gets appeneded to end of the animation name so that
/// the animations do not overwrite each other.
///
/// @group Animations
/// @require {function} equals-false
/// @require {function} equals-true
/// @require {function} is-alias
/// @require {function} is-falsey
/// @require {function} is-global-value
/// @require {function} is-list
/// @require {function} is-number
/// @require {function} is-string
/// @require {function} list-to-string
///
/// @throw Invalid $expand-contract value error.
/// @throw Invalid data type for $animate-in-place error.
/// @throw Invalid $destination value error.
/// @throw Invalid data type for $duration error.
/// @throw Invalid data type for $delay error.
/// @throw Invalid data type for $iterations error.
/// @throw Invalid keyword value for $anim-dir error.
/// @throw Conflict between value for $animate-in-place and $destination error.
@mixin text-tracking-out(
  $expand-contract: 'contract',
  $animate-in-place: true,
  $destination: null,
  $duration: 0.8s,
  $delay: null,
  $iterations: null,
  $anim-dir: null,
  $timing-func: cubic-bezier(0.55, 0.085, 0.68, 0.53),
  $anim-num: null
) {
  $anim-suffix: '';

  @if $expand-contract and is-string($expand-contract) {
    $expand-contract: string.to-lower-case($expand-contract);
  }

  @if $animate-in-place and is-string($animate-in-place) {
    $animate-in-place: string.to-lower-case($animate-in-place);
  }

  @if $destination and is-string($destination) {
    $destination: string.to-lower-case($destination);
  }

  @if $delay and is-string($delay) {
    $delay: string.to-lower-case($delay);
  }

  @if $anim-dir {
    @if is-list($anim-dir) {
      $anim-dir: list-to-string($anim-dir);
    }

    @if is-string($anim-dir) {
      $anim-dir: string.to-lower-case($anim-dir);
    }
  }

  @if $timing-func and is-string($timing-func) {
    $timing-func: string.to-lower-case($timing-func);
  }

  @if equals-true($expand-contract) or is-alias('expand', $expand-contract) {
    $expand-contract: true;
  } @else if is-falsey($expand-contract) or is-alias('contract', $expand-contract) {
    $expand-contract: false;
  }

  @if is-alias('top', $destination) {
    $destination: 'top';
  } @else if is-alias('bottom', $destination) {
    $destination: 'bottom';
  } @else if is-falsey($destination) or
    is-alias('normal', $destination) or
    is-alias('none', $destination)
  {
    $destination: null;
  } @else {
    @error 'Invalid destination value of `#{meta.inspect($destination)}` for ' +
        'the [ text-tracking-out() ] mixin. The value for $destination must ' +
        'be one of the following: `null`, `top`, or `bottom`';
  }

  @if equals-true($animate-in-place) {
    @if $destination {
      @warn 'The [ text-tracking-out() ] mixin cannot create an animation ' +
          'that moves towards the the `#{meta.inspect($destination)}` while ' +
          'the $animate-in-place parameter is set to `true`.';
    }
  } @else if is-falsey($animate-in-place) {
    $animate-in-place: false;

    @if $expand-contract {
      $anim-suffix: '-expand-forwards';
    } @else {
      $anim-suffix: '-contract-backwards';
    }
  } @else {
    @error 'Invalid $animate-in-place value of `#{meta.inspect($animate-in-place)}`' +
        ' used for the [ text-tracking-out() ] mixin. This value should be ' +
        'true or false.';
  }

  @if not is-number($duration) {
    @error 'Invalid $duration value of `#{meta.inspect($duration)}` for the ' +
        '[ text-tracking-out() ] mixin. The duration property must be a number.';
  } @else if math.is-unitless($duration) {
    // If no there is no unit for $duration, unless $duration > 60, assume seconds
    $duration: if($duration > 60, $duration * 1ms, $duration * 1s);
  } @else if not list.index('s' 'ms', math.unit($duration)) {
    @error 'Invalid $duration value of `#{meta.inspect($duration)}` for the ' +
        '[ text-tracking-out() ] mixin. The duration property must be in either ' +
        's (seconds) or ms (milliseconds) units.';
  } @else if $duration % 10 == 0ms {
    // If time in ms would be shorter as seconds, convert it
    $duration: math.div($duration, 1000ms) * 1s;

    // Get rid of possible leading zero in front of decimal place
    @if string.slice($duration + '', 1, 2) == '0.' {
      $duration: #{string.slice($duration + '', 2)};
    }
  }

  @if is-falsey($delay) or
      is-alias('none', $delay) or
      $delay == 0s or $delay == 0ms
  {
    $delay: null;
  }

  @if $delay {
    @if not is-number($delay) {
      @error 'Invalid delay value of `#{meta.inspect($delay)}` for the ' +
          '[ text-tracking-out() ] mixin. The delay property must be a number.';
    } @else if math.is-unitless($delay) {
      // If no there is no unit for $delay, unless $delay > 60, assume seconds
      $delay: if($delay > 60, $delay * 1ms, $delay * 1s);
    } @else if not list.index('s' 'ms', math.unit($delay)) {
      @error 'Invalid delay value of `#{meta.inspect($delay)}` for the ' +
          '[ text-tracking-out() ] mixin. The delay property must be in either ' +
          '`s` (seconds) or `ms` (milliseconds) units.';
    } @else if $delay % 10 == 0ms {
      // If time in ms would be shorter as seconds, convert it
      $delay: math.div($delay, 1000ms) * 1s;

      @if string.slice($delay + '', 1, 2) == '0.' {
        $delay: #{string.slice($delay + '', 2)};
      }
    }
  }

  @if $iterations {
    @if is-string($iterations) {
      $iterations: string.to-lower-case($iterations);
    }

    @if is-alias('infinite', $iterations) {
      $iterations: infinite;
    } @else if not (
        is-number($iterations) and math.is-unitless($iterations)
      ) and not is-global-value($iterations)
    {
      @error 'Invalid $iterations value of `#{meta.inspect($iterations)}` ' +
          'for the [ text-tracking-out() ] mixin.';
    }
  } @else {
    $iterations: null;
  }

  @if is-falsey($anim-dir) or
      is-alias('none', $anim-dir) or
      is-alias('normal', $anim-dir)
  {
    $anim-dir: null;
  } @else if is-alias('reverse', $anim-dir) {
    $anim-dir: reverse;
  } @else if is-alias('alternate', $anim-dir) {
    $anim-dir: alternate;
  } @else if is-alias('alternate-reverse', $anim-dir) {
    $anim-dir: alternate-reverse;
  } @else if not is-global-value($anim-dir) {
    @error 'Invalid animation-direction value of `#{$anim-dir}` for the ' +
        '[ text-tracking-out() ] mixin.';
  }

  @if not $timing-func or is-alias('default', $timing-func) {
    $timing-func: cubic-bezier(0.55, 0.085, 0.68, 0.53);
  }

  @if $destination == 'top' {
    $anim-suffix: string.insert(
      $anim-suffix,
      '-to-top',
      string.length($anim-suffix) + 1
    );
  } @else if $destination == 'bottom' {
    $anim-suffix: string.insert(
      $anim-suffix,
      '-to-bottom',
      string.length($anim-suffix) + 1
    );
  }

  @if equals-false($anim-num) or $anim-num == '' {
    $anim-num: null;
  } @else {
    $anim-suffix: string.insert(
      $anim-suffix,
      '--#{$anim-num}',
      string.length($anim-suffix) + 1
    );
  }

  @if not $animate-in-place {
    transform: translate3d(0, 0, 0);
    transform-style: preserve-3d;
  }

  animation: txt-track-out#{$anim-suffix} #{$duration} #{$timing-func} #{$delay}
      #{$iterations} #{$anim-dir} both;

  @at-root {
    @keyframes txt-track-out#{$anim-suffix} {
      0% {
        opacity: 1;

        @if not $expand-contract {
          @if not $animate-in-place {
            @if not $destination {
              transform: translateZ(0);
            } @else {
              transform: translateZ(0) translateY(0);
            }
          }
        } @else {
          @if not $animate-in-place {
            @if not $destination {
              transform: translateZ(0);
            } @else {
              transform: translateZ(0) translateY(0);
            }
          }
        }
      }

      @if not $expand-contract and $animate-in-place and not $destination {
        50% {
          opacity: 1;
        }
      } @else {
        60% {
          @if not $expand-contract {
            opacity: 1;
          } @else {
            opacity: 0.8;
          }
        }
      }

      100% {
        opacity: 0;

        @if not $expand-contract {
          letter-spacing: -0.5em;

          @if not $animate-in-place {
            @if not $destination {
              transform: translateZ(-500px);
            } @else if $destination == 'top' {
              transform: translateZ(-500px) translateY(-300px);
            } @else {
              transform: translateZ(-500px) translateY(300px);
            }
          }
        } @else {
          letter-spacing: 1em;

          @if not $animate-in-place {
            @if not $destination {
              transform: translateZ(300px);
            } @else if $destination == 'top' {
              transform: translateZ(300px) translateY(-200px);
            } @else {
              transform: translateZ(300px) translateY(200px);
            }
          }
        }
      }
    }
  }
}
