use super::{Builtin, GlobalFunctionMap};

use num_bigint::BigInt;
use num_traits::{Signed, ToPrimitive, Zero};

#[cfg(feature = "random")]
use rand::{distributions::Alphanumeric, thread_rng, Rng};

use crate::{
    args::CallArgs,
    common::QuoteKind,
    error::SassResult,
    parse::Parser,
    unit::Unit,
    value::{Number, Value},
};

pub(crate) fn to_upper_case(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
    args.max_args(1)?;
    match args.get_err(0, "string")? {
        Value::String(mut i, q) => {
            i.make_ascii_uppercase();
            Ok(Value::String(i, q))
        }
        v => Err((
            format!("$string: {} is not a string.", v.inspect(args.span())?),
            args.span(),
        )
            .into()),
    }
}

pub(crate) fn to_lower_case(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
    args.max_args(1)?;
    match args.get_err(0, "string")? {
        Value::String(mut i, q) => {
            i.make_ascii_lowercase();
            Ok(Value::String(i, q))
        }
        v => Err((
            format!("$string: {} is not a string.", v.inspect(args.span())?),
            args.span(),
        )
            .into()),
    }
}

pub(crate) fn str_length(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
    args.max_args(1)?;
    match args.get_err(0, "string")? {
        Value::String(i, _) => Ok(Value::Dimension(
            Some(Number::from(i.chars().count())),
            Unit::None,
            true,
        )),
        v => Err((
            format!("$string: {} is not a string.", v.inspect(args.span())?),
            args.span(),
        )
            .into()),
    }
}

pub(crate) fn quote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
    args.max_args(1)?;
    match args.get_err(0, "string")? {
        Value::String(i, _) => Ok(Value::String(i, QuoteKind::Quoted)),
        v => Err((
            format!("$string: {} is not a string.", v.inspect(args.span())?),
            args.span(),
        )
            .into()),
    }
}

pub(crate) fn unquote(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
    args.max_args(1)?;
    match args.get_err(0, "string")? {
        i @ Value::String(..) => Ok(i.unquote()),
        v => Err((
            format!("$string: {} is not a string.", v.inspect(args.span())?),
            args.span(),
        )
            .into()),
    }
}

pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
    args.max_args(3)?;
    let (string, quotes) = match args.get_err(0, "string")? {
        Value::String(s, q) => (s, q),
        v => {
            return Err((
                format!("$string: {} is not a string.", v.inspect(args.span())?),
                args.span(),
            )
                .into())
        }
    };
    let str_len = string.chars().count();
    let start = match args.get_err(1, "start-at")? {
        Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
            return Err((format!("{} is not an int.", n.inspect()), args.span()).into())
        }
        Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => {
            n.to_integer().to_usize().unwrap_or(str_len + 1)
        }
        Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 1_usize,
        Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 1_usize,
        Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1))
            .to_usize()
            .unwrap(),
        Value::Dimension(None, Unit::None, ..) => {
            return Err(("NaN is not an int.", args.span()).into())
        }
        v @ Value::Dimension(..) => {
            return Err((
                format!(
                    "$start: Expected {} to have no units.",
                    v.inspect(args.span())?
                ),
                args.span(),
            )
                .into())
        }
        v => {
            return Err((
                format!("$start-at: {} is not a number.", v.inspect(args.span())?),
                args.span(),
            )
                .into())
        }
    };
    let mut end = match args.default_arg(2, "end-at", Value::Null)? {
        Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
            return Err((format!("{} is not an int.", n.inspect()), args.span()).into())
        }
        Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => {
            n.to_integer().to_usize().unwrap_or(str_len + 1)
        }
        Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 0_usize,
        Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 0_usize,
        Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1))
            .to_usize()
            .unwrap_or(str_len + 1),
        Value::Dimension(None, Unit::None, ..) => {
            return Err(("NaN is not an int.", args.span()).into())
        }
        v @ Value::Dimension(..) => {
            return Err((
                format!(
                    "$end: Expected {} to have no units.",
                    v.inspect(args.span())?
                ),
                args.span(),
            )
                .into())
        }
        Value::Null => str_len,
        v => {
            return Err((
                format!("$end-at: {} is not a number.", v.inspect(args.span())?),
                args.span(),
            )
                .into())
        }
    };

    if end > str_len {
        end = str_len;
    }

    if start > end || start > str_len {
        Ok(Value::String(String::new(), quotes))
    } else {
        Ok(Value::String(
            string
                .chars()
                .skip(start - 1)
                .take(end - start + 1)
                .collect(),
            quotes,
        ))
    }
}

pub(crate) fn str_index(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
    args.max_args(2)?;
    let s1 = match args.get_err(0, "string")? {
        Value::String(i, _) => i,
        v => {
            return Err((
                format!("$string: {} is not a string.", v.inspect(args.span())?),
                args.span(),
            )
                .into())
        }
    };

    let substr = match args.get_err(1, "substring")? {
        Value::String(i, _) => i,
        v => {
            return Err((
                format!("$substring: {} is not a string.", v.inspect(args.span())?),
                args.span(),
            )
                .into())
        }
    };

    Ok(match s1.find(&substr) {
        Some(v) => Value::Dimension(Some(Number::from(v + 1)), Unit::None, true),
        None => Value::Null,
    })
}

pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser) -> SassResult<Value> {
    args.max_args(3)?;
    let (s1, quotes) = match args.get_err(0, "string")? {
        Value::String(i, q) => (i, q),
        v => {
            return Err((
                format!("$string: {} is not a string.", v.inspect(args.span())?),
                args.span(),
            )
                .into())
        }
    };

    let substr = match args.get_err(1, "insert")? {
        Value::String(i, _) => i,
        v => {
            return Err((
                format!("$insert: {} is not a string.", v.inspect(args.span())?),
                args.span(),
            )
                .into())
        }
    };

    let index = match args.get_err(2, "index")? {
        Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => {
            return Err((
                format!("$index: {} is not an int.", n.inspect()),
                args.span(),
            )
                .into())
        }
        Value::Dimension(Some(n), Unit::None, _) => n,
        Value::Dimension(None, Unit::None, ..) => {
            return Err(("$index: NaN is not an int.", args.span()).into())
        }
        v @ Value::Dimension(..) => {
            return Err((
                format!(
                    "$index: Expected {} to have no units.",
                    v.inspect(args.span())?
                ),
                args.span(),
            )
                .into())
        }
        v => {
            return Err((
                format!("$index: {} is not a number.", v.inspect(args.span())?),
                args.span(),
            )
                .into())
        }
    };

    if s1.is_empty() {
        return Ok(Value::String(substr, quotes));
    }

    let len = s1.chars().count();

    // Insert substring at char position, rather than byte position
    let insert = |idx, s1: String, s2| {
        s1.chars()
            .enumerate()
            .map(|(i, c)| {
                if i + 1 == idx {
                    c.to_string() + s2
                } else if idx == 0 && i == 0 {
                    s2.to_owned() + &c.to_string()
                } else {
                    c.to_string()
                }
            })
            .collect::<String>()
    };

    let string = if index.is_positive() {
        insert(
            index
                .to_integer()
                .to_usize()
                .unwrap_or(len + 1)
                .min(len + 1)
                - 1,
            s1,
            &substr,
        )
    } else if index.is_zero() {
        insert(0, s1, &substr)
    } else {
        let idx = index.abs().to_integer().to_usize().unwrap_or(len + 1);
        if idx > len {
            insert(0, s1, &substr)
        } else {
            insert(len - idx + 1, s1, &substr)
        }
    };

    Ok(Value::String(string, quotes))
}

#[cfg(feature = "random")]
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn unique_id(args: CallArgs, _: &mut Parser) -> SassResult<Value> {
    args.max_args(0)?;
    let mut rng = thread_rng();
    let string = std::iter::repeat(())
        .map(|()| rng.sample(Alphanumeric))
        .map(char::from)
        .take(7)
        .collect();
    Ok(Value::String(string, QuoteKind::None))
}

pub(crate) fn declare(f: &mut GlobalFunctionMap) {
    f.insert("to-upper-case", Builtin::new(to_upper_case));
    f.insert("to-lower-case", Builtin::new(to_lower_case));
    f.insert("str-length", Builtin::new(str_length));
    f.insert("quote", Builtin::new(quote));
    f.insert("unquote", Builtin::new(unquote));
    f.insert("str-slice", Builtin::new(str_slice));
    f.insert("str-index", Builtin::new(str_index));
    f.insert("str-insert", Builtin::new(str_insert));
    #[cfg(feature = "random")]
    f.insert("unique-id", Builtin::new(unique_id));
}
