@use 'sass:meta';
@use 'sass:list';
@use 'sass:math';
@use 'sass:string';
@use '../../functions/equals-false' 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 *;
@use '../utilities/only-moz' as *;

/// Animates an element by rotating it along a point and/or an axis. For the the
/// best effect on a diagonal rotation, use the `animation-stage` mixin on the
/// parent element, and a front and back child element within the rotated
/// element, with the `animation-front-face()` and `animation-back-face()` mixins
/// applied, respectively.
///
/// @param {String} $point ['center'] - The point the element will appear to be
/// rotating around. Can be `center`, `top`, `top-right,` `right`,
/// `bottom-right`, `bottom`, `bottom-left`, `left, `top-left`, and all valid
/// aliases for these $point values.
/// @param {Number} $duration [0.4s] - The duration of the animation (`s` or `ms`).
/// @param {Number} $delay [null] - The optional delay time for the animation.
/// @param {String} $axis [null] - The animation can optionally rotate along an
/// axis line. This value can be `null` (or `false`, `no`, `n`), `horizontal` (or
/// `horiz`, `horz`, `hor`, `h`) or `vertical` (or `vert`, `ver`, `v`). Along
/// the horizontal axis, only `top`, `bottom`, or `center` $point values are
/// valid. Along the vertical axis, only `left`, `right`, or `center` $point
/// values are valid.
/// @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. Default value of null will apply the recommended function for
/// this animation of `cubic-bezier(0.25, 0.46, 0.45, 0.94)`, unless the `$axis`
/// value is `null`, in which case the timing-function defaults to `linear`.
/// @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} is-alias
/// @require {function} is-global-value
/// @require {function} is-falsey
/// @require {function} is-list
/// @require {function} is-number
/// @require {function} is-string
/// @require {function} list-to-string
/// @require {mixin} only-moz
///
/// @throw Invalid $point value error.
/// @throw Invalid $axis value error.
/// @throw Invalid $duration value error.
/// @throw Invalid $delay value error.
/// @throw Invalid $anim-dir value error.
@mixin rotate(
  $point: 'center',
  $duration: 0.4s,
  $delay: null,
  $axis: null,
  $iterations: null,
  $anim-dir: null,
  $timing-func: null,
  $anim-num: null
) {
  // Default values for the $point of 'center' with an $axis of `null`
  $trans-val-1: rotate(0);
  $trans-val-2: rotate(360deg);
  $trans-origin-val: null;
  $trans-val-diag: null;
  $is-diagonal: false;
  $anim-suffix: '-around';

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

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

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

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

  @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 is-alias('top', $point) {
    $trans-origin-val: 50% 0%;
    $point: 'top';
  } @else if is-alias('top right', $point) {
    $trans-origin-val: 100% 0%;
    $point: 'top-right';
  } @else if is-alias('right', $point) {
    $trans-origin-val: 100% 50%;
    $point: 'right';
  } @else if is-alias('bottom right', $point) {
    $trans-origin-val: 100% 100%;
    $point: 'bottom-right';
  } @else if is-alias('bottom', $point) {
    $trans-origin-val: 50% 100%;
    $point: 'bottom';
  } @else if is-alias('bottom left', $point) {
    $trans-origin-val: 0% 100%;
    $point: 'bottom-left';
  } @else if is-alias('left', $point) {
    $trans-origin-val: 0%;
    $point: 'left';
  } @else if is-alias('top left', $point) {
    $trans-origin-val: 0% 0%;
    $point: 'top-left';
  } @else if is-alias('center', $point) {
    $point: 'center';
  } @else {
    @error 'Invalid $point value of #{meta.inspect($point)} for the ' +
        '[ rotate() ] mixin. The value of $point must be one of the following: ' +
        '`top`, `top-right`, `right`, `bottom-right`, `bottom`, `bottom-left`, ' +
        '`left`, `top-left`, or `center`.';
  }

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

  @if not $axis or is-alias('none', $axis) {
    $axis: null;
  }

  @if $axis {
    @if is-alias('top-to-bottom', $axis) {
      $trans-val-1: rotateY(0);
      $trans-val-2: rotateY(360deg);
      $anim-suffix: string.insert(
        $anim-suffix,
        '-vertically',
        string.length($anim-suffix) + 1
      );

      @if $point != 'left' and $point != 'right' and $point != 'center' {
        @error 'You have chosen an axis value of \'vertical\' that cannot be ' +
            'used with the $point value of #{meta.inspect($point)} for the ' +
            '[ rotate() ] mixin. A vertical axis must have $point value of ' +
            'either `left`, `right`, or `center`.';
      } @else if $point == 'left' {
        $trans-origin-val: 0% 50%;
      } @else if $point == 'right' {
        $trans-origin-val: 100% 50%;
      }
    } @else if is-alias('left-to-right', $axis) {
      $trans-val-1: rotateX(0);
      $trans-val-2: rotateX(-360deg);
      $anim-suffix: string.insert(
        $anim-suffix,
        '-horizontally',
        string.length($anim-suffix) + 1
      );

      @if $point != 'top' and $point != 'bottom' and $point != 'center' {
        @error 'You have chosen an axis value of \'horizontal\' that cannot be ' +
            'used with the $point value of #{meta.inspect($point)} for the ' +
            '[ rotate() ] mixin. A horizontal axis must have $point value of ' +
            'either `top`, `bottom`, or `center`.';
      } @else if $point == 'top' {
        $trans-origin-val: 50% 0%;
      } @else if $point == 'bottom' {
        $trans-origin-val: 50% 100%;
      }
    } @else if
      is-alias('bottom-left-to-top-right', $axis) or
      is-alias('bottom-right-to-top-left', $axis)
    {
      $is-diagonal: true;
      $anim-suffix: string.insert(
        $anim-suffix,
        '-diagonally',
        string.length($anim-suffix) + 1
      );

      @if $point !=
        'center' and
        $point !=
        'top-right' and
        $point !=
        'top-left' and
        $point !=
        'bottom-right' and
        $point !=
        'bottom-left'
      {
        @error 'You have chosen an axis value of \'diagonal\' that cannot be ' +
            'used with the $point value of #{meta.inspect($point)} for the ' +
            '[ rotate() ] mixin. A diagonal axis must have $point value of ' +
            'either `top-left`, `top-right`, `bottom-right`, `bottom-left`, ' +
            'or `center`.';
      } @else if $point == 'center' {
        @if is-alias('bottom-right-to-top-left', $axis) {
          $trans-val-1: rotate3d(-1, 1, 0, 0);
          $trans-val-diag: rotate3d(-1, 1, 0, 180deg);
          $trans-val-2: rotate3d(-1, 1, 0, 360deg);
        } @else {
          // Settings for 'diagonal2'/'bottom-right-to-top-left' axis
          $trans-val-1: rotate3d(1, 1, 0, 0);
          $trans-val-diag: rotate3d(1, 1, 0, -180deg);
          $trans-val-2: rotate3d(1, 1, 0, -360deg);
        }
      } @else if $point == 'top-right' {
        $trans-val-1: rotate3d(1, 1, 0, 0);
        $trans-val-diag: rotate3d(1, 1, 0, -180deg);
        $trans-val-2: rotate3d(1, 1, 0, -360deg);
        $trans-origin-val: 100% 0%;
      } @else if $point == 'bottom-right' {
        $trans-val-1: rotate3d(-1, 1, 0, 0);
        $trans-val-diag: rotate3d(-1, 1, 0, -180deg);
        $trans-val-2: rotate3d(-1, 1, 0, -360deg);
        $trans-origin-val: 100% 100%;
      } @else if $point == 'bottom-left' {
        $trans-val-1: rotate3d(1, 1, 0, 0);
        $trans-val-diag: rotate3d(1, 1, 0, 180deg);
        $trans-val-2: rotate3d(1, 1, 0, 360deg);
        $trans-origin-val: 0% 100%;
      } @else if $point == 'top-left' {
        $trans-val-1: rotate3d(-1, 1, 0, 0);
        $trans-val-diag: rotate3d(-1, 1, 0, 180deg);
        $trans-val-2: rotate3d(-1, 1, 0, 360deg);
        $trans-origin-val: 0% 0%;
      }
    } @else {
      @error 'Invalid $axis value of #{meta.inspect($axis)} for the ' +
          '[ rotate() ] mixin. The value of $axis must either be `null` or one ' +
          'of the following: `horizontal`, `vertical`, `diagonal1`, or ' +
          '`diagonal2`.';
    }
  }

  @if not is-number($duration) {
    @error 'Invalid $duration value of `#{meta.inspect($duration)}` for the ' +
        '[ rotate() ] 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 ' +
        '[ rotate() ] 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 ' +
          '[ rotate() ] 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 ' +
          '[ rotate() ] 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 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 [ rotate() ] 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 ' +
        '[ rotate() ] mixin.';
  }

  @if equals-false($timing-func) or is-alias('normal', $timing-func) {
    $timing-func: if(
      $is-diagonal,
      linear,
      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
    );
  }

  transform: translate3d(0, 0, 0);

  @if $axis {
    transform-style: preserve-3d;

    @include only-moz {
      backface-visibility: hidden;
    }
  }

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

  @at-root {
    @keyframes rotate#{$anim-suffix} {
      0% {
        transform: $trans-val-1;

        @if $point != 'center' {
          transform-origin: $trans-origin-val;
        }
      }

      @if $is-diagonal {
        50% {
          transform: $trans-val-diag;

          @if $point != 'center' {
            transform-origin: $trans-origin-val;
          }
        }
      }

      100% {
        transform: $trans-val-2;

        @if $point != 'center' {
          transform-origin: $trans-origin-val;
        }
      }
    }
  }
}
