// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use super::{BaseUnit, Dimensionality};
use crate::loader::Context;
use crate::output::{Digits, NumberParts};
use crate::runtime::Show;
use crate::types::{BigInt, Numeric};
use serde_derive::{Deserialize, Serialize};
use std::fmt;
use std::ops::{Add, Div, Mul, Neg, Sub};

/// The basic representation of a number with a unit.
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Number {
    pub value: Numeric,
    pub unit: Dimensionality,
}

impl Number {
    pub fn one() -> Number {
        Number {
            value: Numeric::one(),
            unit: Dimensionality::new(),
        }
    }

    pub fn one_unit(unit: BaseUnit) -> Number {
        Number::new_unit(Numeric::one(), unit)
    }

    pub fn zero() -> Number {
        Number {
            value: Numeric::zero(),
            unit: Dimensionality::new(),
        }
    }

    /// Creates a dimensionless value.
    pub fn new(num: Numeric) -> Number {
        Number {
            value: num,
            unit: Dimensionality::new(),
        }
    }

    pub fn new_dims(value: Numeric, unit: Dimensionality) -> Number {
        Number { value, unit }
    }

    /// Creates a value with a single dimension.
    pub fn new_unit(num: Numeric, unit: BaseUnit) -> Number {
        let unit = Dimensionality::base_unit(unit);
        Number { value: num, unit }
    }

    /// Computes the reciprocal (1/x) of the value.
    pub fn invert(&self) -> Number {
        Number {
            value: &Numeric::one() / &self.value,
            unit: self
                .unit
                .iter()
                .map(|(k, &power)| (k.clone(), -power))
                .collect::<Dimensionality>(),
        }
    }

    /// Raises a value to a dimensionless integer power.
    pub fn powi(&self, exp: i32) -> Number {
        let unit = self
            .unit
            .iter()
            .map(|(k, &power)| (k.clone(), power * exp as i64))
            .collect::<Dimensionality>();
        Number {
            value: self.value.pow(exp),
            unit,
        }
    }

    /// Computes the nth root of a value iff all of its units have
    /// powers divisible by n.
    pub fn root(&self, exp: i32) -> Result<Number, String> {
        if self.value < Numeric::zero() {
            return Err("Complex numbers are not implemented".to_string());
        }
        let mut res = Dimensionality::new();
        for (dim, &power) in self.unit.iter() {
            if power % exp as i64 != 0 {
                return Err("Result must have integer dimensions".to_string());
            } else {
                res.insert(dim.clone(), power / exp as i64);
            }
        }
        Ok(Number {
            value: Numeric::Float(self.value.to_f64().powf(1.0 / exp as f64)),
            unit: res,
        })
    }

    pub fn pow(&self, exp: &Number) -> Result<Number, String> {
        if !exp.dimless() {
            return Err("Exponent must be dimensionless".to_string());
        }
        if exp.value.abs() >= Numeric::from(1 << 31) {
            return Err("Exponent is too large".to_string());
        }
        let (num, den) = exp.value.to_rational();
        let one = BigInt::one();
        if den == one {
            let exp: Option<i64> = num.as_int();
            Ok(self.powi(exp.unwrap() as i32))
        } else if num == one {
            let exp: Option<i64> = den.as_int();
            self.root(exp.unwrap() as i32)
        } else if !self.dimless() {
            Err("Exponentiation must result in integer dimensions".to_string())
        } else {
            let exp = exp.value.to_f64();
            Ok(Number {
                value: Numeric::Float(self.value.to_f64().powf(exp)),
                unit: self.unit.clone(),
            })
        }
    }

    pub fn shl(&self, exp: &Number) -> Result<Number, String> {
        if !exp.dimless() {
            return Err("Right-hand to << must be dimensionless".to_string());
        }
        if exp.value.abs() >= Numeric::from(1 << 31) {
            return Err("Right-hand to << is too large".to_string());
        }
        let (num, den) = exp.value.to_rational();
        if den != BigInt::one() {
            return Err("Right-hand to << must be an integer".to_string());
        }
        let exp = num.as_int().unwrap();
        let two = BigInt::from(2i64);
        let exp = two.pow(exp as u32);
        Ok(Number {
            value: &self.value * &Numeric::from(exp),
            unit: self.unit.clone(),
        })
    }

    pub fn shr(&self, exp: &Number) -> Result<Number, String> {
        if !exp.dimless() {
            return Err("Right-hand to >> must be dimensionless".to_string());
        }
        if exp.value.abs() >= Numeric::from(1 << 31) {
            return Err("Right-hand to >> is too large".to_string());
        }
        let (num, den) = exp.value.to_rational();
        if den != BigInt::one() {
            return Err("Right-hand to >> must be an integer".to_string());
        }
        let exp = num.as_int().unwrap();
        let two = BigInt::from(2i64);
        let exp = two.pow(exp as u32);
        Ok(Number {
            value: &self.value / &Numeric::from(exp),
            unit: self.unit.clone(),
        })
    }

    pub fn rem(&self, rhs: &Number) -> Result<Number, String> {
        if self.unit != rhs.unit {
            return Err("Arguments to `mod` must have matching dimensionality".to_string());
        }
        Ok(Number {
            value: &self.value % &rhs.value,
            unit: self.unit.clone(),
        })
    }

    pub fn and(&self, rhs: &Number) -> Result<Number, String> {
        if !self.dimless() || !rhs.dimless() {
            return Err("Arguments to `and` must be dimensionless".to_string());
        }
        let left = self.value.as_bigint();
        let right = rhs.value.as_bigint();

        if let (Some(left), Some(right)) = (left, right) {
            Ok(Number {
                value: Numeric::from(&left & &right),
                unit: self.unit.clone(),
            })
        } else {
            return Err("Arguments to `and` must be integers".to_string());
        }
    }

    pub fn or(&self, rhs: &Number) -> Result<Number, String> {
        if !self.dimless() || !rhs.dimless() {
            return Err("Arguments to `or` must be dimensionless".to_string());
        }
        let left = self.value.as_bigint();
        let right = rhs.value.as_bigint();

        if let (Some(left), Some(right)) = (left, right) {
            Ok(Number {
                value: Numeric::from(&left | &right),
                unit: self.unit.clone(),
            })
        } else {
            return Err("Arguments to `or` must be integers".to_string());
        }
    }

    pub fn xor(&self, rhs: &Number) -> Result<Number, String> {
        if !self.dimless() || !rhs.dimless() {
            return Err("Arguments to `xor` must be dimensionless".to_string());
        }
        let left = self.value.as_bigint();
        let right = rhs.value.as_bigint();

        if let (Some(left), Some(right)) = (left, right) {
            Ok(Number {
                value: Numeric::from(&left ^ &right),
                unit: self.unit.clone(),
            })
        } else {
            return Err("Arguments to `xor` must be integers".to_string());
        }
    }

    pub fn numeric_value(&self, base: u8, digits: Digits) -> (Option<String>, Option<String>) {
        self.value.string_repr(base, digits)
    }

    pub fn to_parts_simple(&self) -> NumberParts {
        let (exact, approx) = self.numeric_value(10, Digits::Default);
        NumberParts {
            raw_value: Some(self.clone()),
            exact_value: exact,
            approx_value: approx,
            dimensions: Some(Number::unit_to_string(&self.unit)),
            ..Default::default()
        }
    }

    pub fn with_pretty_unit(&self, context: &Context) -> Number {
        let unit = self.pretty_unit(context);
        Number {
            value: self.value.clone(),
            unit,
        }
    }

    /// Convert the units of the number from base units to display
    /// units, and possibly apply SI prefixes.
    pub fn prettify(&self, context: &Context) -> Number {
        let unit = self.pretty_unit(context);
        if let Some(orig) = unit.as_single() {
            use std::collections::HashSet;
            let prefixes = [
                "milli", "micro", "nano", "pico", "femto", "atto", "zepto", "yocto", "kilo",
                "mega", "giga", "tera", "peta", "exa", "zetta", "yotta",
            ]
            .iter()
            .cloned()
            .collect::<HashSet<&'static str>>();
            let (val, orig) = if *orig.0.id == "kg" || *orig.0.id == "kilogram" {
                // kg special case
                let mul = Numeric::from(1000).pow(orig.1 as i32);
                (&self.value * &mul, (BaseUnit::new("gram"), orig.1))
            } else if *orig.0.id == "bit" && orig.1 == 1 {
                // byte special case
                (&self.value / &Numeric::from(8), (BaseUnit::new("byte"), 1))
            } else if *orig.0.id == "radian" && orig.1 == 2 {
                // steradians
                (self.value.clone(), (BaseUnit::new("steradian"), 1))
            } else {
                (self.value.clone(), (orig.0.clone(), orig.1))
            };
            for &(ref p, ref v) in &context.registry.prefixes {
                if !prefixes.contains(&**p) {
                    continue;
                }
                let abs = val.abs();
                if abs >= v.pow(orig.1 as i32)
                    && abs < (v * &Numeric::from(1000)).pow(orig.1 as i32)
                {
                    let res = &val / &v.pow(orig.1 as i32);
                    // tonne special case
                    let unit = if &**(orig.0).id == "gram" && p == "mega" {
                        "tonne".to_string()
                    } else {
                        format!("{}{}", p, orig.0)
                    };
                    let unit = Dimensionality::new_dim(BaseUnit::new(&unit), orig.1);
                    return Number { value: res, unit };
                }
            }
            let unit = Dimensionality::new_dim(orig.0.clone(), orig.1);
            Number { value: val, unit }
        } else {
            Number {
                value: self.value.clone(),
                unit,
            }
        }
    }

    pub fn to_parts(&self, context: &Context) -> NumberParts {
        self.to_parts_digits(context, 10, Digits::Default)
    }

    pub fn to_parts_digits(&self, context: &Context, base: u8, digits: Digits) -> NumberParts {
        let value = if digits == Digits::Default {
            self.prettify(context)
        } else {
            self.with_pretty_unit(context)
        };
        let (exact, approx) = value.numeric_value(base, digits);

        let quantity = context
            .registry
            .quantities
            .get(&self.unit)
            .cloned()
            .or_else(|| {
                if let Some((unit, power)) = self.unit.as_single() {
                    if power == 1 {
                        Some(unit.to_string())
                    } else {
                        Some(format!("{}^{}", unit, power))
                    }
                } else {
                    None
                }
            });

        NumberParts {
            raw_value: Some(self.clone()),
            exact_value: exact,
            approx_value: approx,
            unit: if value.unit != self.unit {
                Some(Number::unit_to_string(&value.unit))
            } else {
                None
            },
            raw_unit: if value.unit != self.unit {
                Some(value.unit)
            } else {
                None
            },
            quantity,
            dimensions: Some(Number::unit_to_string(&self.unit)),
            raw_dimensions: Some(self.unit.clone()),
            ..Default::default()
        }
    }

    pub fn unit_to_string(unit: &Dimensionality) -> String {
        use std::io::Write;

        let mut out = vec![];
        let mut frac = vec![];

        for (dim, &exp) in unit.iter() {
            if exp < 0 {
                frac.push((dim, exp));
            } else {
                write!(out, " {}", dim).unwrap();
                if exp != 1 {
                    write!(out, "^{}", exp).unwrap();
                }
            }
        }
        if !frac.is_empty() {
            write!(out, " /").unwrap();
            for (dim, exp) in frac {
                let exp = -exp;
                write!(out, " {}", dim).unwrap();
                if exp != 1 {
                    write!(out, "^{}", exp).unwrap();
                }
            }
        }

        if !out.is_empty() {
            out.remove(0);
        }
        String::from_utf8(out).unwrap()
    }

    fn pretty_unit(&self, context: &Context) -> Dimensionality {
        let pretty = crate::algorithms::fast_decompose(self, &context.registry.decomposition_units);
        pretty
            .into_iter()
            .map(|(k, p)| {
                (
                    context
                        .registry
                        .base_unit_long_names
                        .get(&*k.id)
                        .map(|x| BaseUnit::new(x))
                        .unwrap_or(k),
                    p,
                )
            })
            .collect::<Dimensionality>()
    }

    pub fn complexity_score(&self) -> i64 {
        self.unit.iter().map(|(_, p)| 1 + p.abs()).sum()
    }

    pub fn dimless(&self) -> bool {
        self.unit.is_dimensionless()
    }
}

impl fmt::Debug for Number {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        let parts = self.to_parts_simple();
        write!(fmt, "{}", parts)
    }
}

impl Show for Number {
    fn show(&self, context: &Context) -> String {
        let parts = self.to_parts(context);
        parts.to_string()
    }
}

impl<'a, 'b> Add<&'b Number> for &'a Number {
    type Output = Option<Number>;

    fn add(self, other: &Number) -> Self::Output {
        if self.unit != other.unit {
            return None;
        }
        Some(Number {
            value: &self.value + &other.value,
            unit: self.unit.clone(),
        })
    }
}

impl<'a, 'b> Sub<&'b Number> for &'a Number {
    type Output = Option<Number>;

    fn sub(self, other: &Number) -> Self::Output {
        if self.unit != other.unit {
            return None;
        }
        Some(Number {
            value: &self.value - &other.value,
            unit: self.unit.clone(),
        })
    }
}

impl<'a> Neg for &'a Number {
    type Output = Option<Number>;

    fn neg(self) -> Self::Output {
        Some(Number {
            value: -&self.value,
            unit: self.unit.clone(),
        })
    }
}

impl<'a, 'b> Mul<&'b Number> for &'a Number {
    type Output = Option<Number>;

    fn mul(self, other: &Number) -> Self::Output {
        Some(Number {
            value: &self.value * &other.value,
            unit: &self.unit * &other.unit,
        })
    }
}

impl<'a, 'b> Div<&'b Number> for &'a Number {
    type Output = Option<Number>;

    #[allow(clippy::suspicious_arithmetic_impl)]
    fn div(self, other: &Number) -> Self::Output {
        if other.value == Numeric::zero() || other.value == Numeric::Float(0.0) {
            None
        } else {
            self * &other.invert()
        }
    }
}
