use libm::{fabs, modf};

use crate::{scales, utils::f64_eq, BaseUnit, FormatSizeOptions, Kilo, ToF64, Unsigned};

pub struct ISizeFormatter<T: ToF64, O: AsRef<FormatSizeOptions>> {
    value: T,
    options: O,
}

impl<V: ToF64, O: AsRef<FormatSizeOptions>> ISizeFormatter<V, O> {
    pub fn new(value: V, options: O) -> Self {
        ISizeFormatter { value, options }
    }
}

impl<T: ToF64, O: AsRef<FormatSizeOptions>> core::fmt::Display for ISizeFormatter<T, O> {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        let opts = self.options.as_ref();
        let divider = opts.kilo.value();

        let mut size: f64 = self.value.to_f64();
        let mut scale_idx = 0;

        if let Some(val) = opts.fixed_at {
            while scale_idx != val as usize {
                size /= divider;
                scale_idx += 1;
            }
        } else {
            while fabs(size) >= divider {
                size /= divider;
                scale_idx += 1;
            }
        }

        let mut scale = match (opts.units, opts.long_units, opts.base_unit) {
            (Kilo::Decimal, false, BaseUnit::Byte) => scales::SCALE_DECIMAL[scale_idx],
            (Kilo::Decimal, true, BaseUnit::Byte) => scales::SCALE_DECIMAL_LONG[scale_idx],
            (Kilo::Binary, false, BaseUnit::Byte) => scales::SCALE_BINARY[scale_idx],
            (Kilo::Binary, true, BaseUnit::Byte) => scales::SCALE_BINARY_LONG[scale_idx],
            (Kilo::Decimal, false, BaseUnit::Bit) => scales::SCALE_DECIMAL_BIT[scale_idx],
            (Kilo::Decimal, true, BaseUnit::Bit) => scales::SCALE_DECIMAL_BIT_LONG[scale_idx],
            (Kilo::Binary, false, BaseUnit::Bit) => scales::SCALE_BINARY_BIT[scale_idx],
            (Kilo::Binary, true, BaseUnit::Bit) => scales::SCALE_BINARY_BIT_LONG[scale_idx],
        };

        // Remove "s" from the scale if the size is 1.x
        let (fpart, ipart) = modf(size);
        if f64_eq(ipart, 1.0)
            && (opts.long_units || (opts.base_unit == BaseUnit::Bit && scale_idx == 0))
        {
            scale = &scale[0..scale.len() - 1];
        }

        let places = if f64_eq(fpart, 0.0) {
            opts.decimal_zeroes
        } else {
            opts.decimal_places
        };

        let space = if opts.space_after_value { " " } else { "" };

        write!(f, "{:.*}{}{}{}", places, size, space, scale, opts.suffix)
    }
}

impl<'a, U: ToF64 + Unsigned + Copy, O: AsRef<FormatSizeOptions>> From<&'a SizeFormatter<U, O>>
    for ISizeFormatter<U, &'a O>
{
    fn from(source: &'a SizeFormatter<U, O>) -> Self {
        ISizeFormatter {
            value: source.value,
            options: &source.options,
        }
    }
}

pub struct SizeFormatter<T: ToF64 + Unsigned, O: AsRef<FormatSizeOptions>> {
    value: T,
    options: O,
}

impl<V: ToF64 + Unsigned, O: AsRef<FormatSizeOptions>> SizeFormatter<V, O> {
    pub fn new(value: V, options: O) -> Self {
        SizeFormatter { value, options }
    }
}

impl<T: ToF64 + Unsigned + Copy, O: AsRef<FormatSizeOptions> + Copy> core::fmt::Display
    for SizeFormatter<T, O>
{
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        write!(f, "{}", ISizeFormatter::from(self))
    }
}
