@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 *;

/// An animation that bounces an element in to view. 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
/// bounce 'in' (pass `in`, `true`, or `i`) or 'out' (pass `out`, `false`, or `o`).
/// @param {String} $direction ['top'] - The direction the animation will appear
/// to be bouncing 'in' from or bouncing 'out' towards, depending on the value
/// of $in-or-out. Accepts the values of `top`, `bottom`, `left`, `right`,
/// `backwards` or `forwards`. Accepted values also include all 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 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 [null] - The timing function for the
/// animation. The default of `null` applies the default timing function of ease.
/// @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-number
/// @require {function} is-string
/// @require {function} list-to-string
///
/// @throw Invalid $in-or-out value error
/// @throw Invalid $direction value error
/// @throw Invalid $duration value error
/// @throw Invalid $delay value error
/// @throw Invalid $iterations value error
/// @throw Invalid $anim-dir value error
@mixin bounce(
  $in-or-out: 'in',
  $direction: 'top',
  $duration: null,
  $delay: null,
  $iterations: null,
  $anim-dir: null,
  $timing-func: null,
  $anim-num: null
) {
  // Default values for bounce in from the top $direction
  $dir-val1: translateY(-500px);
  $dir-val2: translateY(0);
  $dir-val3: translateY(-65px);
  $dir-val4: translateY(-28px);
  $dir-val5: translateY(-8px);
  $extra-translate-val: null;
  $fwd-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 $direction {
    @if is-list($direction) {
      $direction: list-to-string($direction);
    }

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

  @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 $delay and is-string($delay) {
    $delay: string.to-lower-case($delay);
  }

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

  @if equals-true($in-or-out) or $in-or-out == 'in' or $in-or-out == 'i' {
    $in-or-out: true;
    $anim-suffix: '-in-from';
  } @else if equals-false($in-or-out) or $in-or-out == 'out' {
    $in-or-out: false;
    $anim-suffix: '-out-to';
  } @else {
    @error 'Invalid value for $in-or-out of `#{meta.inspect($in-or-out)}` ' +
        'for the [ bounce() ] mixin.';
  }

  $direction: string.to-lower-case($direction);

  @if is-alias('bottom', $direction) {
    $direction: 'bottom';
  } @else if is-alias('right', $direction) {
    $direction: 'right';
  } @else if is-alias('left', $direction) {
    $direction: 'left';
  } @else if is-alias('forwards', $direction) {
    $direction: 'forwards';
    $fwd-bck: true;
  } @else if is-alias('backwards', $direction) {
    $direction: 'backwards';
    $fwd-bck: true;
  } @else if is-alias('top', $direction) {
    $direction: 'top';
  } @else {
    @error '`#{$direction}` is not a valid direction for the [ bounce() ] mixin.';
  }

  // If bounce is bouncing 'in'
  @if $in-or-out {
    @if $direction == 'bottom' {
      $dir-val1: translateY(500px);
      $dir-val2: translateY(0);
      $dir-val3: translateY(65px);
      $dir-val4: translateY(28px);
      $dir-val5: translateY(8px);
    } @else if $direction == 'right' {
      $dir-val1: translateX(600px);
      $dir-val2: translateX(0);
      $dir-val3: translateX(68px);
      $dir-val4: translateX(32px);
      $dir-val5: translateX(8px);
    } @else if $direction == 'left' {
      $dir-val1: translateX(-600px);
      $dir-val2: translateX(0);
      $dir-val3: translateX(-68px);
      $dir-val4: translateX(-32px);
      $dir-val5: translateX(-8px);
    } @else if $direction == 'forwards' {
      $dir-val1: scale(0);
      $dir-val2: scale(1);
      $dir-val3: scale(0.7);
      $dir-val4: scale(0.84);
      $dir-val5: scale(0.95);
    } @else if $direction == 'backwards' {
      $dir-val1: scale(7);
      $dir-val2: scale(1);
      $dir-val3: scale(1.5);
      $dir-val4: scale(1.24);
      $dir-val5: scale(1.04);
    }
  } @else {
    // If bounce is bouncing 'out'
    @if $direction == 'top' {
      $dir-val1: translateY(0);
      $dir-val2: translateY(-30px);
      $dir-val3: translateY(-38px);
      $dir-val4: translateY(-75px);
      $dir-val5: translateY(-800px);
    } @else if $direction == 'bottom' {
      $dir-val1: translateY(0);
      $dir-val2: translateY(30px);
      $dir-val3: translateY(38px);
      $dir-val4: translateY(75px);
      $dir-val5: translateY(800px);
    } @else if $direction == 'right' {
      $dir-val1: translateX(0);
      $dir-val2: translateX(30px);
      $dir-val3: translateX(38px);
      $dir-val4: translateX(80px);
      $dir-val5: translateX(1000px);
    } @else if $direction == 'left' {
      $dir-val1: translateX(0);
      $dir-val2: translateX(-30px);
      $dir-val3: translateX(-38px);
      $dir-val4: translateX(-80px);
      $dir-val5: translateX(-1000px);
    } @else if $direction == 'forwards' {
      $dir-val1: translateZ(0);
      $dir-val2: translateZ(90px);
      $dir-val3: translateZ(95px);
      $dir-val4: translateZ(150px);
      $dir-val5: translateZ(500px);
    } @else if $direction == 'backwards' {
      $dir-val1: translateZ(0);
      $dir-val2: translateZ(-100px);
      $dir-val3: translateZ(-110px);
      $dir-val4: translateZ(-200px);
      $dir-val5: translateZ(-900px) scale(0);
      $extra-translate-val: translateZ(0) scale(1);
    }
  }

  @if not $duration {
    $duration: if(($in-or-out), 1.1s, 1.5s);
  }
  @if not is-number($duration) {
    @error 'Invalid $duration value of `#{$duration}` for the [ bounce() ] ' +
        '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 `#{$duration}` for the [ bounce() ] ' +
        'mixin. The duration property must be in either s or ms 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 [ bounce() ] ' +
          '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 [ bounce() ] ' +
          'mixin. The delay property must be in either s or ms units.';
    } @else if $delay % 10 == 0ms {
      $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 (is-number($iterations) and math.is-unitless($iterations)) or
      is-global-value($iterations)
    {
      $iterations: $iterations;
    } @else {
      @error 'Invalid $iterations value of `#{meta.inspect($iterations)}` for ' +
          'the [ bounce() ] mixin.';
    }
  }

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

  @if not
    $timing-func or
    $timing-func ==
    'ease' or
    is-alias('none', $timing-func) or
    is-alias('normal', $timing-func)
  {
    // Take all potential values the user might pass to keep the default setting
    // and ensure that the timing function remains at that default
    $timing-func: null;
  }

  $anim-suffix: string.insert(
    $anim-suffix,
    '-#{$direction}',
    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
    );
  }

  transform: translate3d(0, 0, 0);

  @if $fwd-bck {
    transform-style: preserve-3d;
  }

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

  @at-root {
    @keyframes bounce#{$anim-suffix} {
      @if $in-or-out {
        0% {
          opacity: 0;
          transform: #{$dir-val1};
          animation-timing-function: ease-in;
        }

        38% {
          opacity: 1;
          transform: #{$dir-val2};
          animation-timing-function: ease-out;
        }

        55% {
          transform: #{$dir-val3};
          animation-timing-function: ease-in;
        }

        @if not $fwd-bck {
          72%,
          90%,
          100% {
            transform: #{$dir-val2};
            animation-timing-function: ease-out;
          }
        } @else {
          72%,
          89%,
          100% {
            transform: #{$dir-val2};
            animation-timing-function: ease-out;
          }
        }

        81% {
          transform: #{$dir-val4};
          animation-timing-function: ease-in;
        }

        95% {
          transform: #{$dir-val5};
          animation-timing-function: ease-in;
        }
      } @else {
        0%,
        15%,
        38% {
          transform: #{$dir-val1};
          animation-timing-function: ease-out;
        }

        5% {
          transform: #{$dir-val2};
          animation-timing-function: ease-in;
        }

        25% {
          transform: #{$dir-val3};
          animation-timing-function: ease-in;
        }

        52% {
          transform: #{$dir-val4};
          animation-timing-function: ease-in;
        }

        70% {
          transform: if(
            ($direction != 'backwards'),
            #{$dir-val1},
            #{$extra-translate-val}
          );
          animation-timing-function: ease-out;
        }

        85% {
          opacity: 1;
        }

        100% {
          @if $fwd-bck {
            animation-timing-function: ease-in;
          }

          opacity: 0;
          transform: #{$dir-val5};
        }
      }
    }
  }
}
