use std::env;
use std::fmt;
use std::fs::File;
use std::io::Write;
use std::path::Path;

mod op;
mod tests;

pub enum UIntCode {
    Term,
    Zero(Box<UIntCode>),
    One(Box<UIntCode>),
}

pub enum IntCode {
    Zero,
    Pos(Box<UIntCode>),
    Neg(Box<UIntCode>),
}

impl fmt::Display for UIntCode {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            UIntCode::Term => write!(f, "UTerm"),
            UIntCode::Zero(ref inner) => write!(f, "UInt<{}, B0>", inner),
            UIntCode::One(ref inner) => write!(f, "UInt<{}, B1>", inner),
        }
    }
}

impl fmt::Display for IntCode {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            IntCode::Zero => write!(f, "Z0"),
            IntCode::Pos(ref inner) => write!(f, "PInt<{}>", inner),
            IntCode::Neg(ref inner) => write!(f, "NInt<{}>", inner),
        }
    }
}

pub fn gen_uint(u: u64) -> UIntCode {
    let mut result = UIntCode::Term;
    let mut x = 1u64 << 63;
    while x > u {
        x >>= 1
    }
    while x > 0 {
        result = if x & u > 0 {
            UIntCode::One(Box::new(result))
        } else {
            UIntCode::Zero(Box::new(result))
        };
        x >>= 1;
    }
    result
}

pub fn gen_int(i: i64) -> IntCode {
    use std::cmp::Ordering::{Equal, Greater, Less};

    match i.cmp(&0) {
        Greater => IntCode::Pos(Box::new(gen_uint(i as u64))),
        Less => IntCode::Neg(Box::new(gen_uint(i.abs() as u64))),
        Equal => IntCode::Zero,
    }
}

#[cfg_attr(
    feature = "no_std",
    deprecated(
        since = "1.3.0",
        note = "the `no_std` flag is no longer necessary and will be removed in the future"
    )
)]
pub fn no_std() {}

// fixme: get a warning when testing without this
#[allow(dead_code)]
fn main() {
    let highest: u64 = 1024;

    // Use hardcoded values to avoid issues with cross-compilation.
    // See https://github.com/paholg/typenum/issues/162
    let first2: u32 = 11; // (highest as f64).log(2.0).round() as u32 + 1;
    let first10: u32 = 4; // (highest as f64).log(10.0) as u32 + 1;
    let uints = (0..(highest + 1))
        .chain((first2..64).map(|i| 2u64.pow(i)))
        .chain((first10..20).map(|i| 10u64.pow(i)));

    let out_dir = env::var("OUT_DIR").unwrap();
    let dest = Path::new(&out_dir).join("consts.rs");
    println!("cargo:rustc-env=TYPENUM_BUILD_CONSTS={}", dest.display());

    let mut f = File::create(&dest).unwrap();

    no_std();

    // Header stuff here!
    write!(
        f,
        "
/**
Type aliases for many constants.

This file is generated by typenum's build script.

For unsigned integers, the format is `U` followed by the number. We define aliases for

- Numbers 0 through {highest}
- Powers of 2 below `u64::MAX`
- Powers of 10 below `u64::MAX`

These alias definitions look like this:

```rust
use typenum::{{B0, B1, UInt, UTerm}};

# #[allow(dead_code)]
type U6 = UInt<UInt<UInt<UTerm, B1>, B1>, B0>;
```

For positive signed integers, the format is `P` followed by the number and for negative
signed integers it is `N` followed by the number. For the signed integer zero, we use
`Z0`. We define aliases for

- Numbers -{highest} through {highest}
- Powers of 2 between `i64::MIN` and `i64::MAX`
- Powers of 10 between `i64::MIN` and `i64::MAX`

These alias definitions look like this:

```rust
use typenum::{{B0, B1, UInt, UTerm, PInt, NInt}};

# #[allow(dead_code)]
type P6 = PInt<UInt<UInt<UInt<UTerm, B1>, B1>, B0>>;
# #[allow(dead_code)]
type N6 = NInt<UInt<UInt<UInt<UTerm, B1>, B1>, B0>>;
```

# Example
```rust
# #[allow(unused_imports)]
use typenum::{{U0, U1, U2, U3, U4, U5, U6}};
# #[allow(unused_imports)]
use typenum::{{N3, N2, N1, Z0, P1, P2, P3}};
# #[allow(unused_imports)]
use typenum::{{U774, N17, N10000, P1024, P4096}};
```

We also define the aliases `False` and `True` for `B0` and `B1`, respectively.
*/
#[allow(missing_docs)]
pub mod consts {{
    use crate::uint::{{UInt, UTerm}};
    use crate::int::{{PInt, NInt}};

    pub use crate::bit::{{B0, B1}};
    pub use crate::int::Z0;

    pub type True = B1;
    pub type False = B0;
",
        highest = highest
    )
    .unwrap();

    for u in uints {
        writeln!(f, "    pub type U{} = {};", u, gen_uint(u)).unwrap();
        if u <= ::std::i64::MAX as u64 && u != 0 {
            let i = u as i64;
            writeln!(
                f,
                "    pub type P{i} = PInt<U{i}>; pub type N{i} = NInt<U{i}>;",
                i = i
            )
            .unwrap();
        }
    }
    write!(f, "}}").unwrap();

    tests::build_tests().unwrap();

    op::write_op_macro().unwrap();
}
