//! Mathematical calculation functions and expressions.

use crate::compat::Feature;
use crate::error::{ParserError, PrinterError};
use crate::macros::enum_property;
use crate::printer::Printer;
use crate::targets::{should_compile, Browsers};
use crate::traits::private::AddInternal;
use crate::traits::{IsCompatible, Parse, Sign, ToCss, TryMap, TryOp, TrySign};
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;

use super::angle::Angle;
use super::length::Length;
use super::number::CSSNumber;
use super::percentage::Percentage;
use super::time::Time;

/// A CSS [math function](https://www.w3.org/TR/css-values-4/#math-function).
///
/// Math functions may be used in most properties and values that accept numeric
/// values, including lengths, percentages, angles, times, etc.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
  feature = "serde",
  derive(serde::Serialize, serde::Deserialize),
  serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum MathFunction<V> {
  /// The [`calc()`](https://www.w3.org/TR/css-values-4/#calc-func) function.
  Calc(Calc<V>),
  /// The [`min()`](https://www.w3.org/TR/css-values-4/#funcdef-min) function.
  Min(Vec<Calc<V>>),
  /// The [`max()`](https://www.w3.org/TR/css-values-4/#funcdef-max) function.
  Max(Vec<Calc<V>>),
  /// The [`clamp()`](https://www.w3.org/TR/css-values-4/#funcdef-clamp) function.
  Clamp(Calc<V>, Calc<V>, Calc<V>),
  /// The [`round()`](https://www.w3.org/TR/css-values-4/#funcdef-round) function.
  Round(RoundingStrategy, Calc<V>, Calc<V>),
  /// The [`rem()`](https://www.w3.org/TR/css-values-4/#funcdef-rem) function.
  Rem(Calc<V>, Calc<V>),
  /// The [`mod()`](https://www.w3.org/TR/css-values-4/#funcdef-mod) function.
  Mod(Calc<V>, Calc<V>),
  /// The [`abs()`](https://drafts.csswg.org/css-values-4/#funcdef-abs) function.
  Abs(Calc<V>),
  /// The [`sign()`](https://drafts.csswg.org/css-values-4/#funcdef-sign) function.
  Sign(Calc<V>),
  /// The [`hypot()`](https://drafts.csswg.org/css-values-4/#funcdef-hypot) function.
  Hypot(Vec<Calc<V>>),
}

impl<V: IsCompatible> IsCompatible for MathFunction<V> {
  fn is_compatible(&self, browsers: Browsers) -> bool {
    match self {
      MathFunction::Calc(v) => Feature::CalcFunction.is_compatible(browsers) && v.is_compatible(browsers),
      MathFunction::Min(v) => {
        Feature::MinFunction.is_compatible(browsers) && v.iter().all(|v| v.is_compatible(browsers))
      }
      MathFunction::Max(v) => {
        Feature::MaxFunction.is_compatible(browsers) && v.iter().all(|v| v.is_compatible(browsers))
      }
      MathFunction::Clamp(a, b, c) => {
        Feature::ClampFunction.is_compatible(browsers)
          && a.is_compatible(browsers)
          && b.is_compatible(browsers)
          && c.is_compatible(browsers)
      }
      MathFunction::Round(_, a, b) => {
        Feature::RoundFunction.is_compatible(browsers) && a.is_compatible(browsers) && b.is_compatible(browsers)
      }
      MathFunction::Rem(a, b) => {
        Feature::RemFunction.is_compatible(browsers) && a.is_compatible(browsers) && b.is_compatible(browsers)
      }
      MathFunction::Mod(a, b) => {
        Feature::ModFunction.is_compatible(browsers) && a.is_compatible(browsers) && b.is_compatible(browsers)
      }
      MathFunction::Abs(v) => Feature::AbsFunction.is_compatible(browsers) && v.is_compatible(browsers),
      MathFunction::Sign(v) => Feature::SignFunction.is_compatible(browsers) && v.is_compatible(browsers),
      MathFunction::Hypot(v) => {
        Feature::HypotFunction.is_compatible(browsers) && v.iter().all(|v| v.is_compatible(browsers))
      }
    }
  }
}

enum_property! {
  /// A [rounding strategy](https://www.w3.org/TR/css-values-4/#typedef-rounding-strategy),
  /// as used in the `round()` function.
  pub enum RoundingStrategy {
    /// Round to the nearest integer.
    Nearest,
    /// Round up (ceil).
    Up,
    /// Round down (floor).
    Down,
    /// Round toward zero (truncate).
    ToZero,
  }
}

impl Default for RoundingStrategy {
  fn default() -> Self {
    RoundingStrategy::Nearest
  }
}

fn round(value: f32, to: f32, strategy: RoundingStrategy) -> f32 {
  let v = value / to;
  match strategy {
    RoundingStrategy::Down => v.floor() * to,
    RoundingStrategy::Up => v.ceil() * to,
    RoundingStrategy::Nearest => v.round() * to,
    RoundingStrategy::ToZero => v.trunc() * to,
  }
}

fn modulo(a: f32, b: f32) -> f32 {
  ((a % b) + b) % b
}

impl<V: ToCss + std::ops::Mul<f32, Output = V> + TrySign + Clone + std::fmt::Debug> ToCss for MathFunction<V> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      MathFunction::Calc(calc) => {
        dest.write_str("calc(")?;
        calc.to_css(dest)?;
        dest.write_char(')')
      }
      MathFunction::Min(args) => {
        dest.write_str("min(")?;
        let mut first = true;
        for arg in args {
          if first {
            first = false;
          } else {
            dest.delim(',', false)?;
          }
          arg.to_css(dest)?;
        }
        dest.write_char(')')
      }
      MathFunction::Max(args) => {
        dest.write_str("max(")?;
        let mut first = true;
        for arg in args {
          if first {
            first = false;
          } else {
            dest.delim(',', false)?;
          }
          arg.to_css(dest)?;
        }
        dest.write_char(')')
      }
      MathFunction::Clamp(a, b, c) => {
        // If clamp() is unsupported by targets, output min()/max()
        if should_compile!(dest.targets.current, ClampFunction) {
          dest.write_str("max(")?;
          a.to_css(dest)?;
          dest.delim(',', false)?;
          dest.write_str("min(")?;
          b.to_css(dest)?;
          dest.delim(',', false)?;
          c.to_css(dest)?;
          dest.write_str("))")?;
          return Ok(());
        }

        dest.write_str("clamp(")?;
        a.to_css(dest)?;
        dest.delim(',', false)?;
        b.to_css(dest)?;
        dest.delim(',', false)?;
        c.to_css(dest)?;
        dest.write_char(')')
      }
      MathFunction::Round(strategy, a, b) => {
        dest.write_str("round(")?;
        if *strategy != RoundingStrategy::default() {
          strategy.to_css(dest)?;
          dest.delim(',', false)?;
        }
        a.to_css(dest)?;
        dest.delim(',', false)?;
        b.to_css(dest)?;
        dest.write_char(')')
      }
      MathFunction::Rem(a, b) => {
        dest.write_str("rem(")?;
        a.to_css(dest)?;
        dest.delim(',', false)?;
        b.to_css(dest)?;
        dest.write_char(')')
      }
      MathFunction::Mod(a, b) => {
        dest.write_str("mod(")?;
        a.to_css(dest)?;
        dest.delim(',', false)?;
        b.to_css(dest)?;
        dest.write_char(')')
      }
      MathFunction::Abs(v) => {
        dest.write_str("abs(")?;
        v.to_css(dest)?;
        dest.write_char(')')
      }
      MathFunction::Sign(v) => {
        dest.write_str("sign(")?;
        v.to_css(dest)?;
        dest.write_char(')')
      }
      MathFunction::Hypot(args) => {
        dest.write_str("hypot(")?;
        let mut first = true;
        for arg in args {
          if first {
            first = false;
          } else {
            dest.delim(',', false)?;
          }
          arg.to_css(dest)?;
        }
        dest.write_char(')')
      }
    }
  }
}

/// A mathematical expression used within the [`calc()`](https://www.w3.org/TR/css-values-4/#calc-func) function.
///
/// This type supports generic value types. Values such as [Length](super::length::Length), [Percentage](super::percentage::Percentage),
/// [Time](super::time::Time), and [Angle](super::angle::Angle) support `calc()` expressions.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
  feature = "serde",
  derive(serde::Serialize, serde::Deserialize),
  serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum Calc<V> {
  /// A literal value.
  Value(Box<V>),
  /// A literal number.
  Number(CSSNumber),
  /// A sum of two calc expressions.
  #[cfg_attr(feature = "visitor", skip_type)]
  Sum(Box<Calc<V>>, Box<Calc<V>>),
  /// A product of a number and another calc expression.
  #[cfg_attr(feature = "visitor", skip_type)]
  Product(CSSNumber, Box<Calc<V>>),
  /// A math function, such as `calc()`, `min()`, or `max()`.
  #[cfg_attr(feature = "visitor", skip_type)]
  Function(Box<MathFunction<V>>),
}

impl<V: IsCompatible> IsCompatible for Calc<V> {
  fn is_compatible(&self, browsers: Browsers) -> bool {
    match self {
      Calc::Sum(a, b) => a.is_compatible(browsers) && b.is_compatible(browsers),
      Calc::Product(_, v) => v.is_compatible(browsers),
      Calc::Function(f) => f.is_compatible(browsers),
      Calc::Value(v) => v.is_compatible(browsers),
      Calc::Number(..) => true,
    }
  }
}

enum_property! {
  /// A mathematical constant.
  pub enum Constant {
    /// The base of the natural logarithm
    "e": E,
    /// The ratio of a circle’s circumference to its diameter
    "pi": Pi,
    /// infinity
    "infinity": Infinity,
    /// -infinity
    "-infinity": NegativeInfinity,
    /// Not a number.
    "nan": Nan,
  }
}

impl Into<f32> for Constant {
  fn into(self) -> f32 {
    use std::f32::consts;
    use Constant::*;
    match self {
      E => consts::E,
      Pi => consts::PI,
      Infinity => f32::INFINITY,
      NegativeInfinity => -f32::INFINITY,
      Nan => f32::NAN,
    }
  }
}

impl<
    'i,
    V: Parse<'i>
      + std::ops::Mul<f32, Output = V>
      + AddInternal
      + TryOp
      + TryMap
      + TrySign
      + std::cmp::PartialOrd<V>
      + Into<Calc<V>>
      + TryFrom<Calc<V>>
      + TryFrom<Angle>
      + TryInto<Angle>
      + Clone
      + std::fmt::Debug,
  > Parse<'i> for Calc<V>
{
  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    Self::parse_with(input, |_| None)
  }
}

impl<
    'i,
    V: Parse<'i>
      + std::ops::Mul<f32, Output = V>
      + AddInternal
      + TryOp
      + TryMap
      + TrySign
      + std::cmp::PartialOrd<V>
      + Into<Calc<V>>
      + TryFrom<Calc<V>>
      + TryFrom<Angle>
      + TryInto<Angle>
      + Clone
      + std::fmt::Debug,
  > Calc<V>
{
  pub(crate) fn parse_with<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
    input: &mut Parser<'i, 't>,
    parse_ident: Parse,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let location = input.current_source_location();
    let f = input.expect_function()?;
    match_ignore_ascii_case! { &f,
      "calc" => {
        let calc = input.parse_nested_block(|input| Calc::parse_sum(input, parse_ident))?;
        match calc {
          Calc::Value(_) | Calc::Number(_) => Ok(calc),
          _ => Ok(Calc::Function(Box::new(MathFunction::Calc(calc))))
        }
      },
      "min" => {
        let mut args = input.parse_nested_block(|input| input.parse_comma_separated(|input| Calc::parse_sum(input, parse_ident)))?;
        let mut reduced = Calc::reduce_args(&mut args, std::cmp::Ordering::Less);
        if reduced.len() == 1 {
          return Ok(reduced.remove(0))
        }
        Ok(Calc::Function(Box::new(MathFunction::Min(reduced))))
      },
      "max" => {
        let mut args = input.parse_nested_block(|input| input.parse_comma_separated(|input| Calc::parse_sum(input, parse_ident)))?;
        let mut reduced = Calc::reduce_args(&mut args, std::cmp::Ordering::Greater);
        if reduced.len() == 1 {
          return Ok(reduced.remove(0))
        }
        Ok(Calc::Function(Box::new(MathFunction::Max(reduced))))
      },
      "clamp" => {
        let (mut min, mut center, mut max) = input.parse_nested_block(|input| {
          let min = Some(Calc::parse_sum(input, parse_ident)?);
          input.expect_comma()?;
          let center: Calc<V> = Calc::parse_sum(input, parse_ident)?;
          input.expect_comma()?;
          let max = Some(Calc::parse_sum(input, parse_ident)?);
          Ok((min, center, max))
        })?;

        // According to the spec, the minimum should "win" over the maximum if they are in the wrong order.
        let cmp = match (&max, &center) {
          (Some(Calc::Value(max_val)), Calc::Value(center_val)) => center_val.partial_cmp(&max_val),
          (Some(Calc::Number(max_val)), Calc::Number(center_val)) => center_val.partial_cmp(max_val),
          _ => None,
        };

        // If center is known to be greater than the maximum, replace it with maximum and remove the max argument.
        // Otherwise, if center is known to be less than the maximum, remove the max argument.
        match cmp {
          Some(std::cmp::Ordering::Greater) => {
            center = std::mem::take(&mut max).unwrap();
          }
          Some(_) => {
            max = None;
          }
          None => {}
        }

        if cmp.is_some() {
          let cmp = match (&min, &center) {
            (Some(Calc::Value(min_val)), Calc::Value(center_val)) => center_val.partial_cmp(&min_val),
            (Some(Calc::Number(min_val)), Calc::Number(center_val)) => center_val.partial_cmp(min_val),
            _ => None,
          };

          // If center is known to be less than the minimum, replace it with minimum and remove the min argument.
          // Otherwise, if center is known to be greater than the minimum, remove the min argument.
          match cmp {
            Some(std::cmp::Ordering::Less) => {
              center = std::mem::take(&mut min).unwrap();
            }
            Some(_) => {
              min = None;
            }
            None => {}
          }
        }

        // Generate clamp(), min(), max(), or value depending on which arguments are left.
        match (min, max) {
          (None, None) => Ok(center),
          (Some(min), None) => Ok(Calc::Function(Box::new(MathFunction::Max(vec![min, center])))),
          (None, Some(max)) => Ok(Calc::Function(Box::new(MathFunction::Min(vec![center, max])))),
          (Some(min), Some(max)) => Ok(Calc::Function(Box::new(MathFunction::Clamp(min, center, max))))
        }
      },
      "round" => {
        input.parse_nested_block(|input| {
          let strategy = if let Ok(s) = input.try_parse(RoundingStrategy::parse) {
            input.expect_comma()?;
            s
          } else {
            RoundingStrategy::default()
          };

          Self::parse_math_fn(
            input,
            |a, b| round(a, b, strategy),
            |a, b| MathFunction::Round(strategy, a, b),
            parse_ident
          )
        })
      },
      "rem" => {
        input.parse_nested_block(|input| {
          Self::parse_math_fn(input, std::ops::Rem::rem, MathFunction::Rem, parse_ident)
        })
      },
      "mod" => {
        input.parse_nested_block(|input| {
          Self::parse_math_fn(input, modulo, MathFunction::Mod, parse_ident)
        })
      },
      "sin" => Self::parse_trig(input, f32::sin, false, parse_ident),
      "cos" => Self::parse_trig(input, f32::cos, false, parse_ident),
      "tan" => Self::parse_trig(input, f32::tan, false, parse_ident),
      "asin" => Self::parse_trig(input, f32::asin, true, parse_ident),
      "acos" => Self::parse_trig(input, f32::acos, true, parse_ident),
      "atan" => Self::parse_trig(input, f32::atan, true, parse_ident),
      "atan2" => {
        input.parse_nested_block(|input| {
          let res = Self::parse_atan2(input, parse_ident)?;
          if let Ok(v) = V::try_from(res) {
            return Ok(Calc::Value(Box::new(v)))
          }

          Err(input.new_custom_error(ParserError::InvalidValue))
        })
      },
      "pow" => {
        input.parse_nested_block(|input| {
          let a = Self::parse_numeric(input, parse_ident)?;
          input.expect_comma()?;
          let b = Self::parse_numeric(input, parse_ident)?;
          Ok(Calc::Number(a.powf(b)))
        })
      },
      "log" => {
        input.parse_nested_block(|input| {
          let value = Self::parse_numeric(input, parse_ident)?;
          if input.try_parse(|input| input.expect_comma()).is_ok() {
            let base = Self::parse_numeric(input, parse_ident)?;
            Ok(Calc::Number(value.log(base)))
          } else {
            Ok(Calc::Number(value.ln()))
          }
        })
      },
      "sqrt" => Self::parse_numeric_fn(input, f32::sqrt, parse_ident),
      "exp" => Self::parse_numeric_fn(input, f32::exp, parse_ident),
      "hypot" => {
        input.parse_nested_block(|input| {
          let args: Vec<Self> = input.parse_comma_separated(|input| Calc::parse_sum(input, parse_ident))?;
          Self::parse_hypot(&args)?
            .map_or_else(
              || Ok(Calc::Function(Box::new(MathFunction::Hypot(args)))),
              |v| Ok(v)
            )
        })
      },
      "abs" => {
        input.parse_nested_block(|input| {
          let v: Calc<V> = Self::parse_sum(input, parse_ident)?;
          Self::apply_map(&v, f32::abs)
            .map_or_else(
              || Ok(Calc::Function(Box::new(MathFunction::Abs(v)))),
              |v| Ok(v)
            )
        })
      },
      "sign" => {
        input.parse_nested_block(|input| {
          let v: Calc<V> = Self::parse_sum(input, parse_ident)?;
          match &v {
            Calc::Number(n) => return Ok(Calc::Number(n.sign())),
            Calc::Value(v) => {
              // First map so we ignore percentages, which must be resolved to their
              // computed value in order to determine the sign.
              if let Some(v) = v.try_map(|s| s.sign()) {
                // sign() always resolves to a number.
                return Ok(Calc::Number(v.try_sign().unwrap()));
              }
            }
            _ => {}
          }

          Ok(Calc::Function(Box::new(MathFunction::Sign(v))))
        })
      },
       _ => Err(location.new_unexpected_token_error(Token::Ident(f.clone()))),
    }
  }

  fn parse_sum<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
    input: &mut Parser<'i, 't>,
    parse_ident: Parse,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let mut cur: Calc<V> = Calc::parse_product(input, parse_ident)?;
    loop {
      let start = input.state();
      match input.next_including_whitespace() {
        Ok(&Token::WhiteSpace(_)) => {
          if input.is_exhausted() {
            break; // allow trailing whitespace
          }
          match *input.next()? {
            Token::Delim('+') => {
              let next = Calc::parse_product(input, parse_ident)?;
              cur = cur.add(next).map_err(|_| input.new_custom_error(ParserError::InvalidValue))?;
            }
            Token::Delim('-') => {
              let mut rhs = Calc::parse_product(input, parse_ident)?;
              rhs = rhs * -1.0;
              cur = cur.add(rhs).map_err(|_| input.new_custom_error(ParserError::InvalidValue))?;
            }
            ref t => {
              let t = t.clone();
              return Err(input.new_unexpected_token_error(t));
            }
          }
        }
        _ => {
          input.reset(&start);
          break;
        }
      }
    }
    Ok(cur)
  }

  fn parse_product<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
    input: &mut Parser<'i, 't>,
    parse_ident: Parse,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let mut node = Calc::parse_value(input, parse_ident)?;
    loop {
      let start = input.state();
      match input.next() {
        Ok(&Token::Delim('*')) => {
          // At least one of the operands must be a number.
          let rhs = Self::parse_value(input, parse_ident)?;
          if let Calc::Number(val) = rhs {
            node = node * val;
          } else if let Calc::Number(val) = node {
            node = rhs;
            node = node * val;
          } else {
            return Err(input.new_unexpected_token_error(Token::Delim('*')));
          }
        }
        Ok(&Token::Delim('/')) => {
          let rhs = Self::parse_value(input, parse_ident)?;
          if let Calc::Number(val) = rhs {
            if val != 0.0 {
              node = node * (1.0 / val);
              continue;
            }
          }
          return Err(input.new_custom_error(ParserError::InvalidValue));
        }
        _ => {
          input.reset(&start);
          break;
        }
      }
    }
    Ok(node)
  }

  fn parse_value<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
    input: &mut Parser<'i, 't>,
    parse_ident: Parse,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    // Parse nested calc() and other math functions.
    if let Ok(calc) = input.try_parse(Self::parse) {
      match calc {
        Calc::Function(f) => {
          return Ok(match *f {
            MathFunction::Calc(c) => c,
            _ => Calc::Function(f),
          })
        }
        c => return Ok(c),
      }
    }

    if input.try_parse(|input| input.expect_parenthesis_block()).is_ok() {
      return input.parse_nested_block(|input| Calc::parse_sum(input, parse_ident));
    }

    if let Ok(num) = input.try_parse(|input| input.expect_number()) {
      return Ok(Calc::Number(num));
    }

    if let Ok(constant) = input.try_parse(Constant::parse) {
      return Ok(Calc::Number(constant.into()));
    }

    let location = input.current_source_location();
    if let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) {
      if let Some(v) = parse_ident(ident.as_ref()) {
        return Ok(v);
      }

      return Err(location.new_unexpected_token_error(Token::Ident(ident.clone())));
    }

    let value = input.try_parse(V::parse)?;
    Ok(Calc::Value(Box::new(value)))
  }

  fn reduce_args(args: &mut Vec<Calc<V>>, cmp: std::cmp::Ordering) -> Vec<Calc<V>> {
    // Reduces the arguments of a min() or max() expression, combining compatible values.
    // e.g. min(1px, 1em, 2px, 3in) => min(1px, 1em)
    // Also handles plain numbers: min(1, 2, 3) => min(1, 2)
    let mut reduced: Vec<Calc<V>> = vec![];
    for arg in args.drain(..) {
      let mut found = None;
      match &arg {
        Calc::Value(val) => {
          for b in reduced.iter_mut() {
            if let Calc::Value(v) = b {
              match val.partial_cmp(v) {
                Some(ord) if ord == cmp => {
                  found = Some(Some(b));
                  break;
                }
                Some(_) => {
                  found = Some(None);
                  break;
                }
                None => {}
              }
            }
          }
        }
        Calc::Number(val) => {
          for b in reduced.iter_mut() {
            if let Calc::Number(v) = b {
              match val.partial_cmp(v) {
                Some(ord) if ord == cmp => {
                  found = Some(Some(b));
                  break;
                }
                Some(_) => {
                  found = Some(None);
                  break;
                }
                None => {}
              }
            }
          }
        }
        _ => {}
      }
      if let Some(r) = found {
        if let Some(r) = r {
          *r = arg
        }
      } else {
        reduced.push(arg)
      }
    }
    reduced
  }

  fn parse_math_fn<
    't,
    O: FnOnce(f32, f32) -> f32,
    F: FnOnce(Calc<V>, Calc<V>) -> MathFunction<V>,
    Parse: Copy + Fn(&str) -> Option<Calc<V>>,
  >(
    input: &mut Parser<'i, 't>,
    op: O,
    fallback: F,
    parse_ident: Parse,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let a: Calc<V> = Calc::parse_sum(input, parse_ident)?;
    input.expect_comma()?;
    let b: Calc<V> = Calc::parse_sum(input, parse_ident)?;

    Ok(Self::apply_op(&a, &b, op).unwrap_or_else(|| Calc::Function(Box::new(fallback(a, b)))))
  }

  fn apply_op<'t, O: FnOnce(f32, f32) -> f32>(a: &Calc<V>, b: &Calc<V>, op: O) -> Option<Self> {
    match (a, b) {
      (Calc::Value(a), Calc::Value(b)) => {
        if let Some(v) = a.try_op(&**b, op) {
          return Some(Calc::Value(Box::new(v)));
        }
      }
      (Calc::Number(a), Calc::Number(b)) => return Some(Calc::Number(op(*a, *b))),
      _ => {}
    }

    None
  }

  fn apply_map<'t, O: FnOnce(f32) -> f32>(v: &Calc<V>, op: O) -> Option<Self> {
    match v {
      Calc::Number(n) => return Some(Calc::Number(op(*n))),
      Calc::Value(v) => {
        if let Some(v) = v.try_map(op) {
          return Some(Calc::Value(Box::new(v)));
        }
      }
      _ => {}
    }

    None
  }

  fn parse_trig<'t, F: FnOnce(f32) -> f32, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
    input: &mut Parser<'i, 't>,
    f: F,
    to_angle: bool,
    parse_ident: Parse,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    input.parse_nested_block(|input| {
      let v: Calc<Angle> = Calc::parse_sum(input, |v| {
        parse_ident(v).and_then(|v| -> Option<Calc<Angle>> {
          match v {
            Calc::Number(v) => Some(Calc::Number(v)),
            Calc::Value(v) => (*v).try_into().ok().map(|v| Calc::Value(Box::new(v))),
            _ => None,
          }
        })
      })?;
      let rad = match v {
        Calc::Value(angle) if !to_angle => f(angle.to_radians()),
        Calc::Number(v) => f(v),
        _ => return Err(input.new_custom_error(ParserError::InvalidValue)),
      };

      if to_angle && !rad.is_nan() {
        if let Ok(v) = V::try_from(Angle::Rad(rad)) {
          return Ok(Calc::Value(Box::new(v)));
        } else {
          return Err(input.new_custom_error(ParserError::InvalidValue));
        }
      } else {
        Ok(Calc::Number(rad))
      }
    })
  }

  fn parse_numeric<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
    input: &mut Parser<'i, 't>,
    parse_ident: Parse,
  ) -> Result<f32, ParseError<'i, ParserError<'i>>> {
    let v: Calc<CSSNumber> = Calc::parse_sum(input, |v| {
      parse_ident(v).and_then(|v| match v {
        Calc::Number(v) => Some(Calc::Number(v)),
        _ => None,
      })
    })?;
    match v {
      Calc::Number(n) => Ok(n),
      Calc::Value(v) => Ok(*v),
      _ => Err(input.new_custom_error(ParserError::InvalidValue)),
    }
  }

  fn parse_numeric_fn<'t, F: FnOnce(f32) -> f32, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
    input: &mut Parser<'i, 't>,
    f: F,
    parse_ident: Parse,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    input.parse_nested_block(|input| {
      let v = Self::parse_numeric(input, parse_ident)?;
      Ok(Calc::Number(f(v)))
    })
  }

  fn parse_atan2<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
    input: &mut Parser<'i, 't>,
    parse_ident: Parse,
  ) -> Result<Angle, ParseError<'i, ParserError<'i>>> {
    // atan2 supports arguments of any <number>, <dimension>, or <percentage>, even ones that wouldn't
    // normally be supported by V. The only requirement is that the arguments be of the same type.
    // Try parsing with each type, and return the first one that parses successfully.
    if let Ok(v) = input.try_parse(|input| Calc::<Length>::parse_atan2_args(input, |_| None)) {
      return Ok(v);
    }

    if let Ok(v) = input.try_parse(|input| Calc::<Percentage>::parse_atan2_args(input, |_| None)) {
      return Ok(v);
    }

    if let Ok(v) = input.try_parse(|input| Calc::<Angle>::parse_atan2_args(input, |_| None)) {
      return Ok(v);
    }

    if let Ok(v) = input.try_parse(|input| Calc::<Time>::parse_atan2_args(input, |_| None)) {
      return Ok(v);
    }

    Calc::<CSSNumber>::parse_atan2_args(input, |v| {
      parse_ident(v).and_then(|v| match v {
        Calc::Number(v) => Some(Calc::Number(v)),
        _ => None,
      })
    })
  }

  fn parse_atan2_args<'t, Parse: Copy + Fn(&str) -> Option<Calc<V>>>(
    input: &mut Parser<'i, 't>,
    parse_ident: Parse,
  ) -> Result<Angle, ParseError<'i, ParserError<'i>>> {
    let a = Calc::<V>::parse_sum(input, parse_ident)?;
    input.expect_comma()?;
    let b = Calc::<V>::parse_sum(input, parse_ident)?;

    match (&a, &b) {
      (Calc::Value(a), Calc::Value(b)) => {
        if let Some(v) = a.try_op_to(&**b, |a, b| Angle::Rad(a.atan2(b))) {
          return Ok(v);
        }
      }
      (Calc::Number(a), Calc::Number(b)) => return Ok(Angle::Rad(a.atan2(*b))),
      _ => {}
    }

    // We don't have a way to represent arguments that aren't angles, so just error.
    // This will fall back to an unparsed property, leaving the atan2() function intact.
    Err(input.new_custom_error(ParserError::InvalidValue))
  }

  fn parse_hypot<'t>(args: &Vec<Self>) -> Result<Option<Self>, ParseError<'i, ParserError<'i>>> {
    if args.len() == 1 {
      return Ok(Some(args[0].clone()));
    }

    if args.len() == 2 {
      return Ok(Self::apply_op(&args[0], &args[1], f32::hypot));
    }

    let mut iter = args.iter();
    let first = match Self::apply_map(&iter.next().unwrap(), |v| v.powi(2)) {
      Some(v) => v,
      None => return Ok(None),
    };
    let sum = iter.try_fold(first, |acc, arg| {
      Self::apply_op(&acc, &arg, |a, b| a + b.powi(2)).map_or_else(|| Err(()), |v| Ok(v))
    });

    let sum = match sum {
      Ok(s) => s,
      Err(_) => return Ok(None),
    };

    Ok(Self::apply_map(&sum, f32::sqrt))
  }
}

impl<V: std::ops::Mul<f32, Output = V>> std::ops::Mul<f32> for Calc<V> {
  type Output = Self;

  fn mul(self, other: f32) -> Self {
    if other == 1.0 {
      return self;
    }

    match self {
      Calc::Value(v) => Calc::Value(Box::new(*v * other)),
      Calc::Number(n) => Calc::Number(n * other),
      Calc::Sum(a, b) => Calc::Sum(Box::new(*a * other), Box::new(*b * other)),
      Calc::Product(num, calc) => {
        let num = num * other;
        if num == 1.0 {
          return *calc;
        }
        Calc::Product(num, calc)
      }
      Calc::Function(f) => match *f {
        MathFunction::Calc(c) => Calc::Function(Box::new(MathFunction::Calc(c * other))),
        _ => Calc::Product(other, Box::new(Calc::Function(f))),
      },
    }
  }
}

impl<V: AddInternal + std::convert::Into<Calc<V>> + std::convert::TryFrom<Calc<V>> + std::fmt::Debug> Calc<V> {
  pub(crate) fn add(self, other: Calc<V>) -> Result<Calc<V>, <V as TryFrom<Calc<V>>>::Error> {
    Ok(match (self, other) {
      (Calc::Value(a), Calc::Value(b)) => (a.add(*b)).into(),
      (Calc::Number(a), Calc::Number(b)) => Calc::Number(a + b),
      (Calc::Sum(a, b), Calc::Number(c)) => {
        if let Calc::Number(a) = *a {
          Calc::Sum(Box::new(Calc::Number(a + c)), b)
        } else if let Calc::Number(b) = *b {
          Calc::Sum(a, Box::new(Calc::Number(b + c)))
        } else {
          Calc::Sum(Box::new(Calc::Sum(a, b)), Box::new(Calc::Number(c)))
        }
      }
      (Calc::Number(a), Calc::Sum(b, c)) => {
        if let Calc::Number(b) = *b {
          Calc::Sum(Box::new(Calc::Number(a + b)), c)
        } else if let Calc::Number(c) = *c {
          Calc::Sum(Box::new(Calc::Number(a + c)), b)
        } else {
          Calc::Sum(Box::new(Calc::Number(a)), Box::new(Calc::Sum(b, c)))
        }
      }
      (a @ Calc::Number(_), b)
      | (a, b @ Calc::Number(_))
      | (a @ Calc::Product(..), b)
      | (a, b @ Calc::Product(..)) => Calc::Sum(Box::new(a), Box::new(b)),
      (Calc::Function(a), b) => Calc::Sum(Box::new(Calc::Function(a)), Box::new(b)),
      (a, Calc::Function(b)) => Calc::Sum(Box::new(a), Box::new(Calc::Function(b))),
      (Calc::Value(a), b) => (a.add(V::try_from(b)?)).into(),
      (a, Calc::Value(b)) => (V::try_from(a)?.add(*b)).into(),
      (a @ Calc::Sum(..), b @ Calc::Sum(..)) => V::try_from(a)?.add(V::try_from(b)?).into(),
    })
  }
}

impl<V: ToCss + std::ops::Mul<f32, Output = V> + TrySign + Clone + std::fmt::Debug> ToCss for Calc<V> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    let was_in_calc = dest.in_calc;
    dest.in_calc = true;

    let res = match self {
      Calc::Value(v) => v.to_css(dest),
      Calc::Number(n) => n.to_css(dest),
      Calc::Sum(a, b) => {
        a.to_css(dest)?;
        // Whitespace is always required.
        let b = &**b;
        if b.is_sign_negative() {
          dest.write_str(" - ")?;
          let b = b.clone() * -1.0;
          b.to_css(dest)
        } else {
          dest.write_str(" + ")?;
          b.to_css(dest)
        }
      }
      Calc::Product(num, calc) => {
        if num.abs() < 1.0 {
          let div = 1.0 / num;
          calc.to_css(dest)?;
          dest.delim('/', true)?;
          div.to_css(dest)
        } else {
          num.to_css(dest)?;
          dest.delim('*', true)?;
          calc.to_css(dest)
        }
      }
      Calc::Function(f) => f.to_css(dest),
    };

    dest.in_calc = was_in_calc;
    res
  }
}

impl<V: TrySign> TrySign for Calc<V> {
  fn try_sign(&self) -> Option<f32> {
    match self {
      Calc::Number(v) => v.try_sign(),
      Calc::Value(v) => v.try_sign(),
      Calc::Product(c, v) => v.try_sign().map(|s| s * c.sign()),
      Calc::Function(f) => f.try_sign(),
      _ => None,
    }
  }
}

impl<V: TrySign> TrySign for MathFunction<V> {
  fn try_sign(&self) -> Option<f32> {
    match self {
      MathFunction::Abs(_) => Some(1.0),
      MathFunction::Max(values) | MathFunction::Min(values) => {
        let mut iter = values.iter();
        if let Some(sign) = iter.next().and_then(|f| f.try_sign()) {
          for value in iter {
            if let Some(s) = value.try_sign() {
              if s != sign {
                return None;
              }
            } else {
              return None;
            }
          }
          return Some(sign);
        } else {
          return None;
        }
      }
      MathFunction::Clamp(a, b, c) => {
        if let (Some(a), Some(b), Some(c)) = (a.try_sign(), b.try_sign(), c.try_sign()) {
          if a == b && b == c {
            return Some(a);
          }
        }
        return None;
      }
      MathFunction::Round(_, a, b) => {
        if let (Some(a), Some(b)) = (a.try_sign(), b.try_sign()) {
          if a == b {
            return Some(a);
          }
        }
        return None;
      }
      MathFunction::Sign(v) => v.try_sign(),
      MathFunction::Calc(v) => v.try_sign(),
      _ => None,
    }
  }
}
