@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-global-value' as *;
@use '../../functions/is-falsey' 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 *;

/// Generates an hingeing open or hingeing closed animation that turns an
/// element from one of its corners. It is recommended that the
/// `animation-stage()` mixin be used on the parent element.
///
/// @param {String | Bool} $open-or-closed ['open'] - Determines if the element
/// will hinge 'open' (`open`, `true`, or `o`) or 'closed' (`closed`, `close`,
/// `false`, or `c`).
/// @param {String | Bool} $fwd-or-bck ['forwards'] - Determines if the element
/// will hinge 'forwards' (`forwards`, `forward`, `fwd`, or `true`), or hinge
/// 'backwards' (`backwards`, `backward`, `back`, `bck`, or `false`).
/// @param {String} $position ['top'] - This animation will either hinge open or
/// closed from this position on the element, The value of $position must be one
/// of the following: `top`, `top-right`, `right`, `bottom-right`, `bottom`,
/// `bottom-left`, `left, `top-left`, or any valid aliases for these $position
/// values.
/// @param {Bool | String} $has-fade [false] - Adds a fade to the animation that
/// fades out on 'open' and fades in when 'closed', depending on the value of
/// $open-or-closed. To flag as true, pass `true`, `'true'` or `'fade'`. If the
/// fade is turned on, the animation will not display the backside of the
/// element if you are trying to achieve that with `animation-front-face()`, and
/// `animation-back-face()` mixins.
/// @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. A default of null will apply a timing-function of ease-out
/// on a hinge-open animation and ease-in on a hinge-closed 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 $open-or-closed value error.
/// @throw Invalid $fwd-or-bck value error.
/// @throw Invalid $position value error.
/// @throw Invalid $has-fade value error.
/// @throw Invalid $duration value error.
/// @throw Invalid $delay value error.
/// @throw Invalid $iterations value error.
/// @throw Invalid $anim-dir value error.
@mixin hinge(
  $open-or-closed: 'open',
  $fwd-or-bck: 'forwards',
  $position: 'top',
  $has-fade: false,
  $duration: 1s,
  $delay: null,
  $iterations: null,
  $anim-dir: null,
  $timing-func: null,
  $anim-num: null,
) {
  $perspective: perspective(800px);
  $trans-val-1: null;
  $trans-val-2: null;
  $temp-swap: null;
  $trans-origin: null;
  $opacity-val-1: 1;
  $opacity-val-2: 0;
  $direction-multiplier: 1;
  $anim-suffix: '';

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

  @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($open-or-closed) or
      $open-or-closed == 'open' or
      $open-or-closed == 'o'
  {
    $open-or-closed: true;
    $anim-suffix: '-open-';
  } @else if equals-false($open-or-closed) or
      $open-or-closed == 'closed' or
      $open-or-closed == 'close' or
      $open-or-closed == 'c' {
    $open-or-closed: false;
    $anim-suffix: '-close-';
  } @else {
    @error 'Invalid $open-or-closed value of #{meta.inspect($open-or-closed)} ' +
        'for the [ hinge() ] mixin. It must be either `open` (or `o` or `true`) ' +
        'or `closed` (or `close` `c` or `false`.';
  }

  @if equals-true($fwd-or-bck) or
      $fwd-or-bck == 'forwards' or
      $fwd-or-bck == 'forward' or
      $fwd-or-bck == 'fwd' or
      $fwd-or-bck == 'f'
  {
    $anim-suffix: string.insert(
      $anim-suffix,
      'forwards-from-',
      string.length($anim-suffix) + 1
    );
  } @else if equals-false($fwd-or-bck) or
      $fwd-or-bck == 'backwards' or
      $fwd-or-bck == 'backward' or
      $fwd-or-bck == 'back' or
      $fwd-or-bck == 'bck' or
      $fwd-or-bck == 'b'
  {
    $direction-multiplier: -1;
    $anim-suffix: string.insert(
      $anim-suffix,
      'backwards-from-',
      string.length($anim-suffix) + 1
    );
  } @else {
    @error 'Invalid $fwd-or-bck value of #{meta.inspect($fwd-or-bck)} ' +
        'for the [ hinge() ] mixin. It must be either `forwards` (or `fwd` or ' +
        '`true`) or `backwards` (or `back`, `bck`, or `false`.';
  }

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

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

    @if is-alias('top', $position) {
      $trans-val-1: $perspective rotateX(0deg);
      $trans-val-2: $perspective rotateX($direction-multiplier * 180deg);
      $trans-origin: 50% 0%;
      $anim-suffix: string.insert(
        $anim-suffix,
        'top',
        string.length($anim-suffix) + 1
      );
    } @else if is-alias('top right', $position) {
      $trans-val-1: rotate3d(1, 1, 0, 0deg);
      $trans-val-2: rotate3d(1, 1, 0, $direction-multiplier * 180deg);
      $trans-origin: 100% 0%;
      $anim-suffix: string.insert(
        $anim-suffix,
        'top-right',
        string.length($anim-suffix) + 1
      );
    } @else if is-alias('right', $position) {
      $trans-val-1: $perspective rotateY(0deg);
      $trans-val-2: $perspective rotateY($direction-multiplier * 180deg);
      $trans-origin: 100% 50%;
      $anim-suffix: string.insert(
        $anim-suffix,
        'right',
        string.length($anim-suffix) + 1
      );
    } @else if is-alias('bottom right', $position) {
      $trans-val-1: rotate3d(-1, 1, 0, 0deg);
      $trans-val-2: rotate3d(-1, 1, 0, $direction-multiplier * 180deg);
      $trans-origin: 100% 100%;
      $anim-suffix: string.insert(
        $anim-suffix,
        'bottom-right',
        string.length($anim-suffix) + 1
      );
    } @else if is-alias('bottom', $position) {
      $trans-val-1: $perspective rotateX(0deg);
      $trans-val-2: $perspective rotateX($direction-multiplier * -180deg);
      $trans-origin: 50% 100%;
      $anim-suffix: string.insert(
        $anim-suffix,
        'bottom',
        string.length($anim-suffix) + 1
      );
    } @else if is-alias('bottom left', $position) {
      $trans-val-1: rotate3d(1, 1, 0, 0deg);
      $trans-val-2: rotate3d(1, 1, 0, $direction-multiplier * -180deg);
      $trans-origin: 0% 100%;
      $anim-suffix: string.insert(
        $anim-suffix,
        'bottom-left',
        string.length($anim-suffix) + 1
      );
    } @else if is-alias('left', $position) {
      $trans-val-1: $perspective rotateY(0deg);
      $trans-val-2: $perspective rotateY($direction-multiplier * -180deg);
      $trans-origin: 0% 50%;
      $anim-suffix: string.insert(
        $anim-suffix,
        'left',
        string.length($anim-suffix) + 1
      );
    } @else if is-alias('top left', $position) {
      $trans-val-1: rotate3d(-1, 1, 0, 0deg);
      $trans-val-2: rotate3d(-1, 1, 0, $direction-multiplier * -180deg);
      $trans-origin: 0% 0%;
      $anim-suffix: string.insert(
        $anim-suffix,
        'top-left',
        string.length($anim-suffix) + 1
      );
    } @else {
      @error 'Invalid $position of #{meta.inspect($position)} chosen for ' +
          'the [ hinge() ] mixin. The $position value must be one of the ' +
          'following: `top`, `top-right`, `right`, `bottom-right`, `bottom`, ' +
          '`bottom-left`, `left`, `top-left`.';
    }
  } @else {
    @error 'You must pass a $position for the [ hinge() ] mixin. The ' +
        '$position value must be one of the following: `top`, `top-right` ' +
        '`right`, `bottom-right`, `bottom`, `bottom-left`, `left`, `top-left`.';
  }

  @if equals-true($has-fade) or $has-fade == 'fade' {
    $has-fade: true;
  } @else if equals-false($has-fade) or
      $has-fade == 'no-fade' or
      $has-fade == 'no fade' or
      $has-fade == 'nofade' or
      $has-fade == '!fade' {
    $has-fade: false;
  } @else {
    @error 'Invalid value for $has-fade of #{meta.inspect($has-fade)} passed ' +
        'to the [ swing() ] mixin. You must pass a Bool or a String. To ' +
        'include a fade effect with the animation, accepted values are `true` ' +
        'or `\'fade\'` to flag the animation to include a fade effect.';
  }

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

  @if equals-false($timing-func) or is-alias('normal', $timing-func) {
    $timing-func: if(($open-or-closed), ease-out, ease-in);
  }

  // If hinge is closing rather than opening, reverse the necessary values
  @if not $open-or-closed {
    $temp-swap: $trans-val-1;
    $trans-val-1: $trans-val-2;
    $trans-val-2: $temp-swap;
    $opacity-val-1: 0;
    $opacity-val-2: 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 not $has-fade {
    transform-style: preserve-3d;

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

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

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

        transform: $trans-val-1;
        transform-origin: $trans-origin;
      }

      @if $has-fade {
        @if $open-or-closed {
          52% {
            opacity: $opacity-val-1;
          }
        } @else {
          48% {
            opacity: $opacity-val-2;
          }
        }
      }

      100% {
        @if $has-fade {
          opacity: $opacity-val-2;
        }

        transform: $trans-val-2;
        transform-origin: $trans-origin;
      }
    }
  }
}
