//! Additional erlang type conversion BIFs.

use crate::atom::Atom;
use crate::native::ProcessContext;
use crate::term::Term;
use crate::term::binary::{Binary, packed_word_count, write_binary};
use crate::term::boxed::{Cons, Float, Tuple, write_cons, write_float, write_tuple};

pub fn bif_atom_to_binary(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
    let [atom_term] = args else {
        return Err(badarg());
    };
    let atom = atom_term.as_atom().ok_or_else(badarg)?;
    let table = context.atom_table().ok_or_else(badarg)?;
    let name = table.resolve(atom).ok_or_else(badarg)?;
    make_binary(name.as_bytes())
}

pub fn bif_binary_to_float(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
    let _ = context;
    let [binary_term] = args else {
        return Err(badarg());
    };
    let binary = Binary::new(*binary_term).ok_or_else(badarg)?;
    let text = std::str::from_utf8(binary.as_bytes()).map_err(|_| badarg())?;
    let value = text.parse::<f64>().map_err(|_| badarg())?;
    make_float(value)
}

pub fn bif_binary_to_integer(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
    let _ = context;
    let [binary_term] = args else {
        return Err(badarg());
    };
    binary_to_integer(*binary_term, 10)
}

pub fn bif_binary_to_integer_radix(
    args: &[Term],
    context: &mut ProcessContext,
) -> Result<Term, Term> {
    let _ = context;
    let [binary_term, radix_term] = args else {
        return Err(badarg());
    };
    let radix = radix_term
        .as_small_int()
        .and_then(|value| u32::try_from(value).ok())
        .filter(|value| (2..=36).contains(value))
        .ok_or_else(badarg)?;
    binary_to_integer(*binary_term, radix)
}

pub fn bif_float(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
    let _ = context;
    let [value] = args else {
        return Err(badarg());
    };
    if let Some(integer) = value.as_small_int() {
        return make_float(integer as f64);
    }
    if Float::new(*value).is_some() {
        return Ok(*value);
    }
    Err(badarg())
}

pub fn bif_integer_to_binary(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
    let _ = context;
    let [integer_term] = args else {
        return Err(badarg());
    };
    let integer = integer_term.as_small_int().ok_or_else(badarg)?;
    make_binary(integer.to_string().as_bytes())
}

pub fn bif_integer_to_binary_radix(
    args: &[Term],
    context: &mut ProcessContext,
) -> Result<Term, Term> {
    let _ = context;
    let [integer_term, radix_term] = args else {
        return Err(badarg());
    };
    let integer = integer_term.as_small_int().ok_or_else(badarg)?;
    let radix = radix_term
        .as_small_int()
        .and_then(|value| u32::try_from(value).ok())
        .filter(|value| (2..=36).contains(value))
        .ok_or_else(badarg)?;
    make_binary(format_integer(integer, radix)?.as_bytes())
}

pub fn bif_integer_to_list(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
    let _ = context;
    let [integer_term] = args else {
        return Err(badarg());
    };
    let integer = integer_term.as_small_int().ok_or_else(badarg)?;
    make_list(integer.to_string().bytes().map(i64::from))
}

pub fn bif_iolist_to_binary(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
    let _ = context;
    let [iodata] = args else {
        return Err(badarg());
    };
    let mut bytes = Vec::new();
    collect_iodata(*iodata, &mut bytes)?;
    make_binary(&bytes)
}

pub fn bif_list_to_bitstring(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
    bif_iolist_to_binary(args, context)
}

pub fn bif_list_to_tuple(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
    let _ = context;
    let [list_term] = args else {
        return Err(badarg());
    };
    make_tuple(&list_to_vec(*list_term)?)
}

pub fn bif_tuple_to_list(args: &[Term], context: &mut ProcessContext) -> Result<Term, Term> {
    let _ = context;
    let [tuple_term] = args else {
        return Err(badarg());
    };
    let tuple = Tuple::new(*tuple_term).ok_or_else(badarg)?;
    let mut values = Vec::with_capacity(tuple.arity());
    for index in 0..tuple.arity() {
        values.push(tuple.get(index).ok_or_else(badarg)?);
    }
    make_term_list(&values)
}

fn binary_to_integer(binary_term: Term, radix: u32) -> Result<Term, Term> {
    let binary = Binary::new(binary_term).ok_or_else(badarg)?;
    let text = std::str::from_utf8(binary.as_bytes()).map_err(|_| badarg())?;
    let integer = i64::from_str_radix(text, radix).map_err(|_| badarg())?;
    Term::try_small_int(integer).ok_or_else(badarg)
}

fn format_integer(integer: i64, radix: u32) -> Result<String, Term> {
    if radix == 10 {
        return Ok(integer.to_string());
    }

    let negative = integer.is_negative();
    let mut value = integer.unsigned_abs();
    let mut digits = Vec::new();
    if value == 0 {
        digits.push(b'0');
    }
    while value > 0 {
        let digit = (value % u64::from(radix)) as u8;
        let byte = if digit < 10 {
            b'0' + digit
        } else {
            b'A' + (digit - 10)
        };
        digits.push(byte);
        value /= u64::from(radix);
    }
    if negative {
        digits.push(b'-');
    }
    digits.reverse();
    String::from_utf8(digits).map_err(|_| badarg())
}

fn collect_iodata(term: Term, bytes: &mut Vec<u8>) -> Result<(), Term> {
    if term.is_nil() {
        return Ok(());
    }
    if let Some(byte) = term.as_small_int() {
        let byte = u8::try_from(byte).map_err(|_| badarg())?;
        bytes.push(byte);
        return Ok(());
    }
    if let Some(binary) = Binary::new(term) {
        bytes.extend_from_slice(binary.as_bytes());
        return Ok(());
    }
    let cons = Cons::new(term).ok_or_else(badarg)?;
    collect_iodata(cons.head(), bytes)?;
    collect_iodata(cons.tail(), bytes)
}

fn list_to_vec(term: Term) -> Result<Vec<Term>, Term> {
    let mut values = Vec::new();
    let mut current = term;
    loop {
        if current.is_nil() {
            return Ok(values);
        }
        let cons = Cons::new(current).ok_or_else(badarg)?;
        values.push(cons.head());
        current = cons.tail();
    }
}

fn make_binary(bytes: &[u8]) -> Result<Term, Term> {
    let heap = Box::leak(vec![0u64; 2 + packed_word_count(bytes.len())].into_boxed_slice());
    write_binary(heap, bytes).ok_or_else(badarg)
}

fn make_float(value: f64) -> Result<Term, Term> {
    if !value.is_finite() {
        return Err(badarg());
    }
    let heap = Box::leak(Box::new([0u64; 2]));
    write_float(heap, value).ok_or_else(badarg)
}

fn make_list(values: impl DoubleEndedIterator<Item = i64>) -> Result<Term, Term> {
    let terms: Result<Vec<Term>, Term> = values
        .map(|value| Term::try_small_int(value).ok_or_else(badarg))
        .collect();
    make_term_list(&terms?)
}

fn make_term_list(values: &[Term]) -> Result<Term, Term> {
    let mut tail = Term::NIL;
    for value in values.iter().rev() {
        let heap = Box::leak(Box::new([0u64; 2]));
        tail = write_cons(heap, *value, tail).ok_or_else(badarg)?;
    }
    Ok(tail)
}

fn make_tuple(values: &[Term]) -> Result<Term, Term> {
    let heap = Box::leak(vec![0u64; 1 + values.len()].into_boxed_slice());
    write_tuple(heap, values).ok_or_else(badarg)
}

fn badarg() -> Term {
    Term::atom(Atom::BADARG)
}
