@use "sass:math";
@use "sass:list";

@mixin test {
    margin: 0;
}

@mixin typed-text($id, $text, $duration) {
  &::before {
    @include typed($id, $text, $duration);
  }

  &::after {
    @include cursor($id, 6s);
  }
}

/// Animate a blinking cursor.
@mixin cursor($id, $duration) {
  $name: cursor-#{$id};
  // The number of times we need to blink is = the number of full
  // seconds (500ms * 2) that fit in the total duration, rounded up,
  // and doubled.
  $iterations: math.ceil(math.div($duration, 1s)) * 2;

  animation: $name ease-in-out 500ms $iterations alternate;
  content: " ";

  @keyframes #{$name} {
    from {
      content: " ";
    }

    to {
      content: "█";
    }
  }
}

/// Animate a piece of text as if it was being typed by a human.
@mixin typed($id, $text, $duration) {
  word-break: break-all;

  // We don't want a linearly typed set of text, which makes this
  // significantly more complex.
  //
  // CSS animations normally do not permit per-frame changes in
  // duration (since the total animation time is fixed). This means we
  // need to create multiple animations, and delay them so that they
  // happen in the time sequence we want.
  //
  // We generate the raw values with _generate-animations, and then
  // split up the result into the animation API.
  $frames: str-length($text);
  $animations: _generate-animations($id, $frames, 1.2s);

  animation-name: _unzip($animations, 1);
  animation-delay: _unzip($animations, 3);
  animation-fill-mode: forwards;
  content: "";

  // We need to type each character in separate animations, see above
  // comment.
  @each $name, $character in $animations {
    @keyframes #{$name} {
      from {
        content: str-slice($text, 0, $character);
      }

      to {
        content: str-slice($text, 0, $character + 1);
      }
    }
  }
}

/// Unzip a nested set of lists, taking the nth value of each sublist.
@function _unzip($lists, $i) {
  $out: ();
  $sep: comma;
  @each $sublist in $lists {
    $out: list.append($out, list.nth($sublist, $i), $sep);
  }
  @return $out;
}

/// Compute the sum of all numbers in a list.
@function _sum($list) {
  $out: 0;
  @each $val in $list {
    $out: $out + $val;
  }
  @return $out;
}

/// Produce a list from a shorter list by repeating it up until size
/// $length.
@function _round-robin($base, $length) {
  $out: ();
  $sep: list.separator($out);
  @for $i from 0 through $length {
    $out: list.append($out, list.nth($base, $i % list.length($base) + 1));
  }
  @return $out;
}

/// Generate the actual animation values.
///
/// This generates a nested list as:
///
///   (keyframe-name, index, start time)
///
/// The duration of each frame is taken from the internal $delays in a
/// round robin fashion, to give some amount of human-like variance to
/// the duration of each frame.
///
/// Start time is set to the time at which the frame should start to
/// achieve the desired frame-by-frame duration.
@function _generate-animations($id, $number, $total_duration) {
  $out: ();
  $sep: list.separator($out);

  // A set of "human-like" delays for each typed character. In
  // practice, my typing seems to be about 20-70ms, but it looks a bit
  // nicer to increase all typing by 20ms to make the effect more
  // noticeable.
  //
  // Numbers generated once with a random number generator, rather
  // than using `math.random()`, since they end up in CSS verbatim,
  // and the build would be non-reproducible if we didn't do it this
  // way. Using `math.random() wouldn't change this dynamically each
  // time the page loads anyway, so we don't really lose anything by
  // pre-generating these numbers.
  $delays: 69ms, 83ms, 49ms, 48ms, 52ms, 59ms, 40ms, 71ms, 80ms, 67ms;

  @for $animation from 0 through $number {
    $out: list.append(
      $out,
      (
        type-#{$id}-#{$animation},
        $animation,
        _sum(_round_robin($delays, $animation))
      ),
      $sep
    );
  }

  @return $out;
}
