@use 'sass:meta';
@use 'sass:list';
@use 'sass:math';
@use 'sass:string';
@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 *;

/// Generates an animation that fades an element in or out. If a forward or
/// backward direction is chosen to augment the fade, it is recommended that
/// the `animation-stage()` mixin be used on the parent element.
///
/// @param {String | Bool} $in-or-out ['in'] - Determines if the element will
/// fade 'in' (pass `in`, `true`, or `i`) or 'out' (pass `out` `false`,
/// or `o`).
/// @param {String} $direction [null] - Will optionally either fade 'in' from
/// this direction or fade 'out' towards this direction, depending on the value
/// of $in-or-out. May also not come from a direction at all if `null` is passed.
/// Accepted values are `null`, `top`, `top-right`, `right`, `bottom-right`,
/// `bottom`, `bottom-left`, `left`, `top-left`, `forwards`, and `backwards`, or
/// any valid aliases for these $direction values.
/// @param {Number} $duration [null] - 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 animation-direction property.
/// Can be `normal`, `reverse`, `alternate`, or `alternate-reverse`. Can also
/// be any global value. Default of `null` assumes the CSS property default of
/// `normal`.
/// @param {Timing-Function} $timing-func [null] - The timing function for the
/// animation. A null value passed here will default to the recommended timing
/// function for this animation.
/// @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 $in-or-out value error.
/// @throw Invalid $duration value error.
/// @throw Invalid $delay value error.
/// @throw Invalid $iterations value error.
/// @throw Invalid $anim-dir value error.
@mixin fade(
  $in-or-out: 'in',
  $direction: null,
  $duration: null,
  $delay: null,
  $iterations: null,
  $anim-dir: null,
  $timing-func: null,
  $anim-num: null
) {
  // Default values for a fade 'in' with no direction
  $opacity-val-1: 0;
  $opacity-val-2: 1;
  $trans-val-1: null;
  $trans-val-2: null;
  $temp-swap: null;
  $is-fwd-or-bck: false;
  $anim-suffix: '';

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

  @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 equals-true($in-or-out) or $in-or-out == 'in' or $in-or-out == 'i' {
    $in-or-out: true;
    $anim-suffix: '-in';
  } @else if equals-false($in-or-out) or $in-or-out == 'out' {
    $in-or-out: false;
    $anim-suffix: '-out';
  } @else {
    @error 'Invalid value for $in-or-out of `#{meta.inspect($in-or-out)}` ' +
        'in the [ fade ] mixin. Value must be either `in`, `out`, true, or false.';
  }

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

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

    @if is-alias('bottom', $direction) {
      $trans-val-1: translateY(50px);
      $trans-val-2: translateY(0);
      $direction: 'bottom';
    } @else if is-alias('forwards', $direction) {
      $is-fwd-or-bck: true;
      $trans-val-1: translateZ(80px);
      $trans-val-2: translateZ(0);
      $direction: 'front';
    } @else if is-alias('backwards', $direction) {
      $is-fwd-or-bck: true;
      $trans-val-1: translateZ(-80px);
      $trans-val-2: translateZ(0);
      $direction: 'back';
    } @else if is-alias('top', $direction) {
      $trans-val-1: translateY(-50px);
      $trans-val-2: translateY(0);
      $direction: 'top';
    } @else if is-alias('left', $direction) {
      $trans-val-1: translateX(-50px);
      $trans-val-2: translateX(0);
      $direction: 'left';
    } @else if is-alias('right', $direction) {
      $trans-val-1: translateX(50px);
      $trans-val-2: translateX(0);
      $direction: 'right';
    } @else if is-alias('top left', $direction) {
      $trans-val-1: translateX(-50px) translateY(-50px);
      $trans-val-2: translateX(0) translateY(0);
      $direction: 'top-left';
    } @else if is-alias('top right', $direction) {
      $trans-val-1: translateX(50px) translateY(-50px);
      $trans-val-2: translateX(0) translateY(0);
      $direction: 'top-right';
    } @else if is-alias('bottom right', $direction) {
      $trans-val-1: translateX(50px) translateY(50px);
      $trans-val-2: translateX(0) translateY(0);
      $direction: 'bottom-right';
    } @else if is-alias('bottom left', $direction) {
      $trans-val-1: translateX(-50px) translateY(50px);
      $trans-val-2: translateX(0) translateY(0);
      $direction: 'bottom-left';
    } @else {
      @error 'Invalid $direction of `#{meta.inspect($direction)}` chosen for the ' +
          '[ fade() ] mixin. The value of $direction must either be `null` or ' +
          'one of the following: `top`, `top-right`, `right`, `bottom-right`, ' +
          '`bottom`, `bottom-left`, `left`, `top-left`, `forwards`, or `backwards`.';
    }
  } @else {
    $direction: null;
  }

  @if $direction {
    @if $in-or-out {
      $anim-suffix: string.insert(
        $anim-suffix,
        '-from',
        string.length($anim-suffix) + 1
      );
    } @else {
      $anim-suffix: string.insert(
        $anim-suffix,
        '-to',
        string.length($anim-suffix) + 1
      );
    }

    $anim-suffix: string.insert(
      $anim-suffix,
      '-#{$direction}',
      string.length($anim-suffix) + 1
    );
  }

  // If fading out rather than in, reverse the necessary values
  @if not $in-or-out {
    $opacity-val-1: 1;
    $opacity-val-2: 0;

    $temp-swap: $trans-val-1;
    $trans-val-1: $trans-val-2;
    $trans-val-2: $temp-swap;
  }

  @if not $duration {
    @if $in-or-out {
      $duration: if((not $direction), 1.2s, 0.6s);
    } @else {
      $duration: if((not $direction), 1s, 0.7s);
    }
  }

  @if not is-number($duration) {
    @error 'Invalid $duration value of `#{meta.inspect($duration)}` for the ' +
        '[ fade ] 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 ' +
        '[ fade ] mixin. The duration property must be in either s or ms units.';
  } @else if math.unit($duration) == 'ms' and $duration % 100 == 0ms {
    // If time in ms would be shorter as seconds, convert it
    $duration: math.div($duration, 1000ms) * 1s;
  }

  @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 [ fade() ] 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 [ fade() ] mixin.' +
          'The delay property must be in either s or ms units.';
    } @else if math.unit($delay) == 'ms' and $delay % 100 == 0ms {
      // If time in ms would be shorter as seconds, convert it
      $delay: math.div($delay, 1000ms) * 1s;
    }
  }

  @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 [ fade() ] mixin. Value must be either a unitless number, ' +
          '`infinite`, `initial` or `inherit`.';
    }
  }

  @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 ' +
        '[ fade() ] mixin.';
  }

  @if equals-false($timing-func) or is-alias('normal', $timing-func) {
    @if $in-or-out {
      $timing-func: cubic-bezier(0.39, 0.575, 0.565, 1); // ease-out-sine
    } @else {
      $timing-func: if(
        (not $direction),
        ease-out,
        cubic-bezier(0.25, 0.46, 0.45, 0.94)
      );
    }
  }

  @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
    );
  }

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

  @if $is-fwd-or-bck {
    transform: translate3d(0, 0, 0);
    transform-style: preserve-3d;
  }

  @at-root {
    @keyframes fade#{$anim-suffix} {
      0% {
        opacity: $opacity-val-1;
        @if $direction {
          transform: $trans-val-1;
        }
      }

      100% {
        opacity: $opacity-val-2;
        @if $direction {
          transform: $trans-val-2;
        }
      }
    }
  }
}
