use super::channels::Channels;
use super::{
    check_alpha, check_channel, check_pct_range, eval_inner, is_not,
    is_special, make_call, relative_color, CallError, CheckedArg,
    FunctionMap, ResolvedArgs,
};
use crate::css::{CallArgs, Value};
use crate::sass::{ArgsError, FormalArgs, Name};
use crate::value::{Color, Rational, RgbFormat, Rgba};
use crate::Scope;
use num_traits::{one, Zero};

pub fn register(f: &mut Scope) {
    def_va!(f, _rgb(kwargs), |s| do_rgba(&name!(rgb), s));
    def_va!(f, _rgba(kwargs), |s| do_rgba(&name!(rgba), s));
    def!(f, red(color), |s| {
        let c: Color = s.get(name!(color))?;
        Ok(Value::scalar(c.to_rgba().red().round()))
    });
    def!(f, green(color), |s| {
        let c: Color = s.get(name!(color))?;
        Ok(Value::scalar(c.to_rgba().green().round()))
    });
    def!(f, blue(color), |s| {
        let c: Color = s.get(name!(color))?;
        Ok(Value::scalar(c.to_rgba().blue().round()))
    });
    def!(f, mix(color1, color2, weight = b"50%"), |s| {
        let a: Color = s.get(name!(color1))?;
        let a = a.to_rgba();
        let b: Color = s.get(name!(color2))?;
        let b = b.to_rgba();
        let w = s.get_map(name!(weight), check_pct_range)?;
        let one: Rational = one();

        let w_a = {
            let wa = a.alpha() - b.alpha();
            let w2 = w * 2 - 1;
            let divis = w2 * wa + 1;
            if divis.is_zero() {
                w
            } else {
                (((w2 + wa) / divis) + 1) / 2
            }
        };
        let w_b = one - w_a;

        let m_c = |c_a, c_b| w_a * c_a + w_b * c_b;
        Ok(Rgba::new(
            m_c(a.red(), b.red()),
            m_c(a.green(), b.green()),
            m_c(a.blue(), b.blue()),
            a.alpha() * w + b.alpha() * (one - w),
            RgbFormat::Name,
        )
        .into())
    });
    def!(f, invert(color, weight = b"100%"), |s| {
        match s.get(name!(color))? {
            Value::Color(col, _) => {
                let w = s.get_map(name!(weight), check_pct_range)?;
                Ok(do_invert(col, w))
            }
            col => {
                let w = s.get_map(name!(weight), check_pct_range)?;
                if w == one() {
                    match col {
                        v @ Value::Numeric(..) => {
                            Ok(make_call("invert", vec![v]))
                        }
                        v => Err(is_not(&v, "a color")).named(name!(color)),
                    }
                } else {
                    Err(CallError::msg("Only one argument may be passed to the plain-CSS invert() function."))
                }
            }
        }
    });
}

pub fn expose(m: &Scope, global: &mut FunctionMap) {
    for (gname, lname) in &[
        (name!(rgb), name!(_rgb)),
        (name!(rgba), name!(_rgba)),
        (name!(blue), name!(blue)),
        (name!(green), name!(green)),
        (name!(mix), name!(mix)),
        (name!(red), name!(red)),
    ] {
        global.insert(gname.clone(), m.get_lfunction(lname));
    }
    let mut f = Scope::builtin_module("sass:color");
    def!(f, invert(color, weight = b"100%"), |s| {
        match s.get(name!(color))? {
            Value::Color(col, _) => {
                let w = s.get_map(name!(weight), check_pct_range)?;
                Ok(do_invert(col, w))
            }
            col => {
                let w = s.get_map(name!(weight), check_pct_range)?;
                if w == one() {
                    match col {
                        v @ Value::Numeric(..) => {
                            Ok(make_call("invert", vec![v]))
                        }
                        v if is_special(&v) => {
                            Ok(make_call("invert", vec![v]))
                        }
                        v => Err(is_not(&v, "a color")).named(name!(color)),
                    }
                } else {
                    Err(CallError::msg("Only one argument may be passed to the plain-CSS invert() function."))
                }
            }
        }
    });
    #[allow(clippy::single_element_loop)] // for consistency
    for name in [name!(invert)] {
        let f = f.get_lfunction(&name);
        global.insert(name, f);
    }
}

fn do_invert(col: Color, w: Rational) -> Value {
    let rgba = col.to_rgba();
    let inv = |v: Rational| -(v - 255) * w + v * -(w - 1);
    Rgba::new(
        inv(rgba.red()),
        inv(rgba.green()),
        inv(rgba.blue()),
        rgba.alpha(),
        rgba.source(),
    )
    .into()
}

fn do_rgba(fn_name: &Name, s: &ResolvedArgs) -> Result<Value, CallError> {
    let a1 = FormalArgs::new(vec![one_arg!(channels)]);
    let a1b = FormalArgs::new(vec![one_arg!(color), one_arg!(alpha)]);
    let a2 = FormalArgs::new(vec![
        one_arg!(red),
        one_arg!(green),
        one_arg!(blue = b"null"),
        one_arg!(alpha = b"null"),
    ]);
    let a2_show = FormalArgs::new(vec![
        one_arg!(red),
        one_arg!(green),
        one_arg!(blue),
        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(&["red", "green", "blue"]))
            .and_then(|c| match c {
                Channels::Data([h, s, l, a]) => {
                    rgba_from_values(fn_name, h, s, l, a)
                }
                Channels::Special(channels) => {
                    Ok(make_call(fn_name.as_ref(), vec![channels]))
                }
            }),
        Err(err @ CallError::Args(ArgsError::Missing(_), _)) => Err(err),
        Err(_) => match eval_inner(fn_name, &a1b, s, args.clone()) {
            Ok(s) => {
                let c = s.get(name!(color))?;
                let a = s.get(name!(alpha))?;
                if is_special(&c) || is_special(&a) {
                    if let Ok(c) = Color::try_from(c.clone()) {
                        let c = c.to_rgba();
                        Ok(make_call(
                            fn_name.as_ref(),
                            vec![
                                Value::scalar(c.red()),
                                Value::scalar(c.green()),
                                Value::scalar(c.blue()),
                                a,
                            ],
                        ))
                    } else {
                        Ok(make_call(fn_name.as_ref(), vec![c, a]))
                    }
                } else {
                    let mut c = Color::try_from(c).named(name!(color))?;
                    c.set_alpha(check_alpha(a).named(name!(alpha))?);
                    c.reset_source();
                    Ok(c.into())
                }
            }
            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)
                    })?;
                rgba_from_values(
                    fn_name,
                    s.get(name!(red))?,
                    s.get(name!(green))?,
                    s.get(name!(blue))?,
                    s.get(name!(alpha))?,
                )
            }
        },
    }
}

fn rgba_from_values(
    fn_name: &Name,
    r: Value,
    g: Value,
    b: Value,
    a: Value,
) -> Result<Value, CallError> {
    if is_special(&r) || is_special(&g) || is_special(&b) || is_special(&a) {
        Ok(make_call(fn_name.as_ref(), vec![r, g, b, a]))
    } else {
        Ok(Rgba::new(
            check_channel(r).named(name!(red))?,
            check_channel(g).named(name!(green))?,
            check_channel(b).named(name!(blue))?,
            check_alpha(a).named(name!(alpha))?,
            RgbFormat::Rgb,
        )
        .into())
    }
}

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

    #[test]
    fn test_high() {
        assert_eq!(
            do_evaluate(&[], b"rgb(150%, 300, 256);"),
            "rgb(255, 255, 255)"
        );
    }

    #[test]
    fn test_low() {
        assert_eq!(do_evaluate(&[], b"rgb(-3, -2%, 0);"), "rgb(0, 0, 0)");
    }
    #[test]
    fn test_mid() {
        assert_eq!(
            do_evaluate(&[], b"rgb(50%, 255/2, 25% + 25);"),
            "rgb(128, 128, 128)"
        );
    }
}
