use nu_protocol::{ShellError, Span, Value};
use std::cmp::Ordering;

pub enum Reduce {
    Summation,
    Product,
    Minimum,
    Maximum,
}

pub type ReducerFunction =
    Box<dyn Fn(Value, Vec<Value>, Span, Span) -> Result<Value, ShellError> + Send + Sync + 'static>;

pub fn reducer_for(command: Reduce) -> ReducerFunction {
    match command {
        Reduce::Summation => Box::new(|_, values, span, head| sum(values, span, head)),
        Reduce::Product => Box::new(|_, values, span, head| product(values, span, head)),
        Reduce::Minimum => Box::new(|_, values, span, head| min(values, span, head)),
        Reduce::Maximum => Box::new(|_, values, span, head| max(values, span, head)),
    }
}

pub fn max(data: Vec<Value>, span: Span, head: Span) -> Result<Value, ShellError> {
    let mut biggest = data
        .first()
        .ok_or_else(|| ShellError::UnsupportedInput {
            msg: "Empty input".to_string(),
            input: "value originates from here".into(),
            msg_span: head,
            input_span: span,
        })?
        .clone();

    for value in &data {
        if value.partial_cmp(&biggest) == Some(Ordering::Greater) {
            biggest = value.clone();
        }
    }
    Ok(biggest)
}

pub fn min(data: Vec<Value>, span: Span, head: Span) -> Result<Value, ShellError> {
    let mut smallest = data
        .first()
        .ok_or_else(|| ShellError::UnsupportedInput {
            msg: "Empty input".to_string(),
            input: "value originates from here".into(),
            msg_span: head,
            input_span: span,
        })?
        .clone();

    for value in &data {
        if value.partial_cmp(&smallest) == Some(Ordering::Less) {
            smallest = value.clone();
        }
    }
    Ok(smallest)
}

pub fn sum(data: Vec<Value>, span: Span, head: Span) -> Result<Value, ShellError> {
    let initial_value = data.first();

    let mut acc = match initial_value {
        Some(v) => {
            let span = v.span();
            match v {
                Value::Filesize { .. } => Ok(Value::filesize(0, span)),
                Value::Duration { .. } => Ok(Value::duration(0, span)),
                Value::Int { .. } | Value::Float { .. } => Ok(Value::int(0, span)),
                _ => Ok(Value::nothing(head)),
            }
        }

        None => Err(ShellError::UnsupportedInput {
            msg: "Empty input".to_string(),
            input: "value originates from here".into(),
            msg_span: head,
            input_span: span,
        }),
    }?;

    for value in &data {
        match value {
            Value::Int { .. }
            | Value::Float { .. }
            | Value::Filesize { .. }
            | Value::Duration { .. } => {
                acc = acc.add(head, value, head)?;
            }
            Value::Error { error, .. } => return Err(*error.clone()),
            other => {
                return Err(ShellError::UnsupportedInput {
                    msg: format!(
                        "Attempted to compute the sum of a value '{}' that cannot be summed with a type of `{}`.",
                        other.coerce_string()?,
                        other.get_type()
                    ),
                    input: "value originates from here".into(),
                    msg_span: head,
                    input_span: other.span(),
                });
            }
        }
    }
    Ok(acc)
}

pub fn product(data: Vec<Value>, span: Span, head: Span) -> Result<Value, ShellError> {
    let initial_value = data.first();

    let mut acc = match initial_value {
        Some(v) => {
            let span = v.span();
            match v {
                Value::Int { .. } | Value::Float { .. } => Ok(Value::int(1, span)),
                _ => Ok(Value::nothing(head)),
            }
        }
        None => Err(ShellError::UnsupportedInput {
            msg: "Empty input".to_string(),
            input: "value originates from here".into(),
            msg_span: head,
            input_span: span,
        }),
    }?;

    for value in &data {
        match value {
            Value::Int { .. } | Value::Float { .. } => {
                acc = acc.mul(head, value, head)?;
            }
            Value::Error { error, .. } => return Err(*error.clone()),
            other => {
                return Err(ShellError::UnsupportedInput {
                    msg: format!(
                        "Attempted to compute the product of a value '{}' that cannot be multiplied with a type of `{}`.",
                        other.coerce_string()?,
                        other.get_type()
                    ),
                    input: "value originates from here".into(),
                    msg_span: head,
                    input_span: other.span(),
                });
            }
        }
    }
    Ok(acc)
}
