use super::channels::Channels;
use super::{
    check_alpha, check_amount, check_hue, eval_inner, is_not, is_special,
    relative_color, CallError, CheckedArg, FunctionMap, NumOrSpecial,
    ResolvedArgs,
};
use crate::css::{CallArgs, Value};
use crate::output::Format;
use crate::sass::{ArgsError, FormalArgs, Name};
use crate::value::{Color, Hsla, Numeric, Unit};
use crate::Scope;

pub fn register(f: &mut Scope) {
    def_va!(f, _hsl(kwargs), |s| do_hsla(&name!(hsl), s));
    def_va!(f, _hsla(kwargs), |s| do_hsla(&name!(hsla), s));
    def!(f, complement(color), |s| {
        Ok(s.get::<Color>(name!(color))?.rotate_hue(180.into()).into())
    });
    def!(f, hue(color), |s| {
        let col = s.get::<Color>(name!(color))?;
        let hsla = col.to_hsla();
        Ok(Value::Numeric(Numeric::new(hsla.hue(), Unit::Deg), true))
    });
    def!(f, saturation(color), |s| {
        Ok(percentage(s.get::<Color>(name!(color))?.to_hsla().sat()))
    });
    def!(f, lightness(color), |s| {
        Ok(percentage(s.get::<Color>(name!(color))?.to_hsla().lum()))
    });
    def!(f, grayscale(color), |args| match args.get(name!(color))? {
        Value::Color(col, _) => {
            let is_rgb = col.is_rgb();
            let col = col.to_hsla();
            Ok(Hsla::new(col.hue(), 0., col.lum(), col.alpha(), !is_rgb)
                .into())
        }
        v @ Value::Numeric(..) => Ok(Value::call("grayscale", [v])),
        v => Err(is_not(&v, "a color")).named(name!(color)),
    });
}

pub fn expose(m: &Scope, global: &mut FunctionMap) {
    for (gname, lname) in &[
        (name!(hsl), name!(_hsl)),
        (name!(hsla), name!(_hsla)),
        (name!(complement), name!(complement)),
        (name!(hue), name!(hue)),
        (name!(lightness), name!(lightness)),
        (name!(saturation), name!(saturation)),
    ] {
        global.insert(gname.clone(), m.get_lfunction(lname));
    }
    let mut f = Scope::builtin_module("sass:color");
    def!(f, adjust_hue(color, degrees), |s| {
        let col = s.get::<Color>(name!(color))?;
        if let Some(adj) = s.get_opt_map(name!(degrees), check_hue)? {
            Ok(col.rotate_hue(adj).into())
        } else {
            Ok(col.into())
        }
    });
    def!(f, darken(color, amount), |s| {
        let col = s.get::<Color>(name!(color))?;
        let col = col.to_hsla();
        let lum = col.lum() - s.get_map(name!(amount), check_amount)?;
        Ok(Hsla::new(col.hue(), col.sat(), lum, col.alpha(), false).into())
    });
    def!(f, desaturate(color, amount), |s| {
        let col = s.get::<Color>(name!(color))?;
        let col = col.to_hsla();
        let sat = col.sat() - s.get_map(name!(amount), check_amount)?;
        Ok(Hsla::new(col.hue(), sat, col.lum(), col.alpha(), false).into())
    });
    def!(f, grayscale(color), |args| match args.get(name!(color))? {
        Value::Color(col, _) => {
            let col = col.to_hsla();
            Ok(
                Hsla::new(col.hue(), 0., col.lum(), col.alpha(), false)
                    .into(),
            )
        }
        v => NumOrSpecial::try_from(v)
            .map_err(|e| is_not(e.value(), "a color"))
            .named(name!(color))
            .map(|v| Value::call("grayscale", [v])),
    });
    def_va!(f, saturate(kwargs), |s| {
        let a1 = FormalArgs::new(vec![one_arg!(color), one_arg!(amount)]);
        let a2 = FormalArgs::new(vec![one_arg!(amount)]);
        let args = s.get_map(name!(kwargs), CallArgs::from_value)?;
        match eval_inner(&name!(saturate), &a1, s, args.clone()) {
            Ok(s) => {
                let col = s.get::<Color>(name!(color))?;
                let sat = s.get_map(name!(amount), check_amount)?;
                let col = col.to_hsla();
                let sat = (col.sat() + sat).clamp(0., 1.);
                Ok(Hsla::new(col.hue(), sat, col.lum(), col.alpha(), false)
                    .into())
            }
            Err(CallError::Args(ArgsError::Missing(_), _)) => {
                eval_inner(&name!(saturate), &a2, s, args)?
                    .get::<NumOrSpecial>(name!(amount))
                    .map(|sat| Value::call("saturate", [sat]))
            }
            Err(ae) => Err(ae),
        }
    });
    def!(f, lighten(color, amount), |s| {
        let col = s.get::<Color>(name!(color))?;
        let col = col.to_hsla();
        let lum = col.lum() + s.get_map(name!(amount), check_amount)?;
        Ok(Hsla::new(col.hue(), col.sat(), lum, col.alpha(), false).into())
    });
    for (gname, lname) in &[
        (name!(adjust_hue), name!(adjust_hue)),
        (name!(darken), name!(darken)),
        (name!(grayscale), name!(grayscale)),
        (name!(desaturate), name!(desaturate)),
        (name!(lighten), name!(lighten)),
        (name!(saturate), name!(saturate)),
    ] {
        global.insert(gname.clone(), f.get_lfunction(lname));
    }
}

fn do_hsla(fn_name: &Name, s: &ResolvedArgs) -> Result<Value, CallError> {
    let a1 = FormalArgs::new(vec![one_arg!(channels)]);
    let a2 = FormalArgs::new(vec![
        one_arg!(hue),
        one_arg!(saturation),
        one_arg!(lightness = b"null"),
        one_arg!(alpha = b"null"),
    ]);
    let a2_show = FormalArgs::new(vec![
        one_arg!(hue),
        one_arg!(saturation),
        one_arg!(lightness),
        one_arg!(alpha),
    ]);
    let args = s.get_map(name!(kwargs), CallArgs::from_value)?;
    if relative_color(&args) {
        return Ok(Value::Call(fn_name.to_string(), args));
    }
    match eval_inner(fn_name, &a1, s, args.clone()) {
        Ok(s) => Channels::try_from(s.get::<Value>(name!(channels))?)
            .map_err(|e| e.conv(&["hue", "saturation", "lightness"]))
            .and_then(|c| match c {
                Channels::Data([h, s, l, a]) => {
                    hsla_from_values(fn_name, h, s, l, a)
                }
                Channels::Special(channels) => {
                    Ok(Value::call(fn_name.as_ref(), [channels]))
                }
            }),
        Err(err @ CallError::Args(ArgsError::Missing(_), _)) => Err(err),
        Err(_) => {
            let s = eval_inner(fn_name, &a2_show, s, args.clone()).or_else(
                |e| eval_inner(fn_name, &a2, s, args).map_err(|_| e),
            )?;
            hsla_from_values(
                fn_name,
                s.get(name!(hue))?,
                s.get(name!(saturation))?,
                s.get(name!(lightness))?,
                s.get(name!(alpha))?,
            )
        }
    }
}

fn hsla_from_values(
    fn_name: &Name,
    h: Value,
    s: Value,
    l: Value,
    a: Value,
) -> Result<Value, CallError> {
    if is_special(&h) || is_special(&s) || is_special(&l) || is_special(&a) {
        Ok(Value::call(
            fn_name.as_ref(),
            [h, s, l, a].into_iter().filter(|v| v != &Value::Null),
        ))
    } else if l == Value::Null {
        Err(CallError::msg("Missing argument $lightness."))
    } else {
        Ok(Hsla::new(
            check_hue(h).named(name!(hue))?,
            f64::max(0., check_pct_opt(s).named(name!(saturation))?),
            check_pct_opt(l).named(name!(lightness))?,
            check_alpha(a).named(name!(alpha))?,
            true, // ??
        )
        .into())
    }
}

pub fn percentage(v: f64) -> Value {
    Numeric::percentage(v).into()
}

/// Gets a percentage as a fraction 0 .. 1.
/// If v is not a percentage, keep it as it is.
fn check_pct_opt(v: Value) -> Result<f64, String> {
    let v = Numeric::try_from(v)?;
    if !v.unit.is_percent() {
        // Note: The deprecation warning should include the parameter name
        // and line of call, but we don't have that here.
        dep_warn!(
            "Passing a number without unit % ({}) is deprecated.\
             \nTo preserve current behavior: calc($weight / 1{} * 1%)\
             \nMore info: https://sass-lang.com/d/function-units",
            v.format(Format::introspect()),
            v.unit
        );
    }
    Ok(f64::from(v.value) / 100.)
}

#[test]
fn test_hsl_black() {
    assert_eq!(do_evaluate(&[], b"hsl(17, 32%, 0%);"), "hsl(17, 32%, 0%)")
}
#[test]
fn test_hsl_white() {
    assert_eq!(
        do_evaluate(&[], b"hsl(300, 82%, 100%);"),
        "hsl(300, 82%, 100%)"
    )
}
#[test]
fn test_hsl_gray() {
    assert_eq!(do_evaluate(&[], b"hsl(300, 0%, 50%);"), "hsl(300, 0%, 50%)")
}
#[test]
fn test_hsl_red() {
    assert_eq!(do_evaluate(&[], b"hsl(0, 75%, 88%);"), "hsl(0, 75%, 88%)")
}
#[test]
fn test_hsl_yellow() {
    assert_eq!(
        do_evaluate(&[], b"hsl(60, 100%, 63%);"),
        "hsl(60, 100%, 63%)"
    )
}

#[test]
fn test_hsl_blue_magenta() {
    assert_eq!(
        do_evaluate(&[], b"hsl(270, 75%, 38%);"),
        "hsl(270, 75%, 38%)"
    )
}

#[cfg(test)]
use crate::variablescope::test::do_evaluate;
