@use 'sass:list';
@use 'sass:map';
@use 'sass:math';
@use 'sass:string';
@use 'conv-to-rem' as *;
@use 'equals-true' as *;
@use 'is-map' as *;
@use 'is-string' as *;
@use 'to-fixed' as *;

/// Calculate the least square fit linear regression, or "trend-line", of a
/// mapped set of provided values.
///
/// @param {Map} $map - A map where the keys are viewport widths in pixels,
/// and each value associated is the desired pixel size at that given view width.
/// @param {Bool} $convert-to-rem [true] - If true, will convert the `px` value
/// in the returned CSS `calc()` equation to `rem` units.
///
/// @return {Calculation} A linear equation expressed as a calc() function.
///
/// @example scss
/// .element {
///   font-size: trend-line((576px: 24px, 768px: 24px, 992px: 34px));
/// }
/// // Compiles to...
/// .element {
///   font-size: calc(2.46062992vw + 0.51083rem);
/// }
///
/// @access public
/// @group Utilities
/// @require {function} conv-to-rem
/// @require {function} equals-true
/// @require {function} is-map
/// @require {function} is-string
/// @require {function} to-fixed
///
/// @throw Invalid data type for `$map` parameter.
/// @throw The $map parameter must have at least 2 key:value pairs.
@function trend-line($map, $conv-to-rem: true) {
  // Validate the input
  @if not is-map($map) {
    @error 'Invalid data type for $map parameter in the [ trend-line() ] ' +
        'function. Value must be a map.';
  }

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

  // Get the number of provided breakpoints
  $num-points: list.length(map.keys($map));

  // Validate that the map describes at least two points.
  @if $num-points < 2 {
    @error 'The $map passed to the [ trend-line() ] fucntion must contain ' +
        'at least 2 key:value pairs.';
  }

  // Calculate the averages...
  $res-sum: 0;
  $size-sum: 0;

  @each $resolution, $size in $map {
    $res-sum: $res-sum + $resolution;
    $size-sum: $size-sum + $size;
  }

  $res-avg: math.div($res-sum, $num-points);
  $size-avg: math.div($size-sum, $num-points);

  // Calculate the differences...
  $multiplied-diff: 0;
  $squared-diff: 0;

  @each $resolution, $size in $map {
    // Calculate the differences from the averages
    $res-diff: $resolution - $res-avg;
    $size-diff: $size - $size-avg;

    // Calculate the sum of multiplied differences
    $multiplied-diff: $multiplied-diff + ($res-diff * $size-diff);

    // Calculate the sum of squared resolution differences
    $squared-diff: $squared-diff + ($res-diff * $res-diff);
  }

  // Validate that $squared-diff is a non-zero value
  @if $squared-diff == 0 {
    @error 'An error has occured in the [ trend-line() ] function. Invalid ' +
        '$map values were passed, resulting in a divide by zero calculation.';
  }

  // Calculate the slope of the line
  $slope: math.div($multiplied-diff, $squared-diff);

  // Validate the slope is a non-zero value.
  @if $slope == 0 {
    @error 'The [ trend-line() ] function\'s $map parameters must have a ' +
        'non-zero slope.';
  }

  // Calculate the y-intercept of the line
  $y-intercept: $size-avg - ($slope * $res-avg);

  // Return the CSS calc() function equation for the trend line
  // If $conv-to-rem is true, y-intercept will be converted to a rem value
  @if equals-true($conv-to-rem) or $conv-to-rem == 'rem' {
    @return calc(to-fixed($slope * 100vw, 8) + to-fixed(conv-to-rem($y-intercept), 5));
  }

  @return calc(to-fixed($slope * 100vw, 8) + to-fixed($y-intercept));

}
