//! Greatest common divisor.

use crate::ibig::IBig;
use crate::ops::DivRem;
use crate::ubig::UBig;
use core::mem;

impl UBig {
    /// Greatest common divisor.
    ///
    /// # Example
    ///
    /// ```
    /// # use ibig::ubig;
    /// assert_eq!(ubig!(12).gcd(&ubig!(18)), ubig!(6));
    /// ```
    ///
    /// # Panics
    ///
    /// `ubig!(0).gcd(&ubig!(0))` panics.
    pub fn gcd(&self, rhs: &UBig) -> UBig {
        let (mut a, mut b) = (self.clone(), rhs.clone());

        let zeros = match (a.trailing_zeros(), b.trailing_zeros()) {
            (None, None) => panic!("gcd(0, 0)"),
            (None, Some(_)) => return b,
            (Some(_), None) => return a,
            (Some(a_zeros), Some(b_zeros)) => {
                a >>= a_zeros;
                b >>= b_zeros;
                a_zeros.min(b_zeros)
            }
        };

        // One round of Euclidean algorithm.
        if a < b {
            mem::swap(&mut a, &mut b);
        }
        a %= &b;

        // Binary algorithm.
        loop {
            // b is odd
            match a.trailing_zeros() {
                None => break,
                Some(a_zeros) => a >>= a_zeros,
            }
            // a is odd

            if a < b {
                mem::swap(&mut a, &mut b);
            }
            a -= &b;
        }

        b << zeros
    }

    /// Greatest common divisors and the Bézout coefficients.
    ///
    /// If `a.extended_gcd(&b) == (g, x, y)` then:
    /// * `x * a + y * b == g`
    /// * `abs(x) <= max(b, 1)`
    /// * `abs(y) <= max(a, 1)`
    ///
    /// # Example
    /// ```
    /// # use ibig::{ubig, IBig, ops::UnsignedAbs};
    /// let a = ubig!(12);
    /// let b = ubig!(18);
    /// let (g, x, y) = a.extended_gcd(&b);
    /// assert_eq!(&a % &g, ubig!(0));
    /// assert_eq!(&b % &g, ubig!(0));
    /// assert_eq!(&x * IBig::from(&a) + &y * IBig::from(&b), IBig::from(g));
    /// assert!(x.unsigned_abs() <= b);
    /// assert!(y.unsigned_abs() <= a);
    /// ```
    ///
    /// # Panics
    ///
    /// `ubig!(0).extended_gcd(&ubig!(0))` panics.
    pub fn extended_gcd(&self, rhs: &UBig) -> (UBig, IBig, IBig) {
        let zeros = match (self.trailing_zeros(), rhs.trailing_zeros()) {
            (None, None) => panic!("extended_gcd(0, 0)"),
            (None, Some(_)) => return (rhs.clone(), 0u8.into(), 1u8.into()),
            (Some(_), None) => return (self.clone(), 1u8.into(), 0u8.into()),
            (Some(a_zeros), Some(b_zeros)) => a_zeros.min(b_zeros),
        };

        let u = self >> zeros;
        let v = rhs >> zeros;
        let mut a;
        let mut b;
        let mut ax;
        let mut ay;
        let mut bx;
        let mut by;

        // Invariants:
        // gcd(a, b) == gcd(u, v)
        // a = ax * u - ay * v
        // b = bx * u - by * v
        // ax, bx <= v
        // ay, by <= u

        // One round of Euclidean algorithm.
        if u <= v {
            let (q, r) = (&v).div_rem(&u);
            // u = 1 * u - 0 * v
            // r = v - q * u = (v-q) * u - (u-1) * v
            a = u.clone();
            ax = UBig::from_word(1);
            ay = UBig::from_word(0);
            b = r;
            bx = &v - q;
            by = &u - UBig::from_word(1);
        } else {
            let (q, r) = (&u).div_rem(&v);
            // v = 0 * u + 1 * v = v * u - (u-1) * v
            // r = 1 * u - q * v
            a = v.clone();
            ax = v.clone();
            ay = &u - UBig::from_word(1);

            b = r;
            bx = UBig::from_word(1);
            by = q;
        }

        // At least one of a and b is odd (because gcd(u, v) is odd). Make b odd.
        if &b & 1u8 == 0u8 {
            mem::swap(&mut a, &mut b);
            mem::swap(&mut ax, &mut bx);
            mem::swap(&mut ay, &mut by);
        }

        // Binary algorithm.
        while a != UBig::from_word(0) {
            // b is odd
            while &a & 1u8 == 0u8 {
                // a is even
                if &ax & 1u8 != 0u8 || &ay & 1u8 != 0u8 {
                    ax += &v;
                    ay += &u;
                }
                // Now ax, ay are even.
                a >>= 1usize;
                ax >>= 1usize;
                ay >>= 1usize;
                // Again ax <= v, bx <= u.
            }
            // Both a and b are odd.
            if a < b {
                mem::swap(&mut a, &mut b);
                mem::swap(&mut ax, &mut bx);
                mem::swap(&mut ay, &mut by);
            }
            a -= &b;
            if ax < bx {
                ax += &v;
                ay += &u;
            }
            ax -= &bx;
            ay -= &by;
            // ax >= 0 in both cases
            // ax <= v in both cases
            // ax * u - ay * v = a
            // ay * v = ax * u - a <= v * u - 0
            // ay <= u
            // After one round Euclidean, and at least one subtraction, a < min(u,v).
            // ay * v = ax * u - a >= -a > -min(u,v) >= -v
            // ay >= 0
        }

        (b << zeros, IBig::from(bx), -IBig::from(by))
    }
}

impl IBig {
    /// Greatest common divisor.
    ///
    /// # Example
    ///
    /// ```
    /// # use ibig::ibig;
    /// assert_eq!(ibig!(-12).gcd(&ibig!(18)), ibig!(6));
    /// ```
    ///
    /// # Panics
    ///
    /// `ibig!(0).gcd(&ibig!(0))` panics.
    pub fn gcd(&self, rhs: &IBig) -> IBig {
        self.magnitude().gcd(rhs.magnitude()).into()
    }

    /// Greatest common divisors and the Bézout coefficients.
    ///
    /// If `a.extended_gcd(&b) == (g, x, y)` then:
    /// * `x * a + y * b == g`
    /// * `abs(x) <= max(abs(b), 1)`
    /// * `abs(y) <= max(abs(a), 1)`
    ///
    /// # Example
    /// ```
    /// # use ibig::{ibig, IBig, ops::Abs};
    /// let a = ibig!(-12);
    /// let b = ibig!(18);
    /// let (g, x, y) = a.extended_gcd(&b);
    /// assert_eq!(&a % &g, ibig!(0));
    /// assert_eq!(&b % &g, ibig!(0));
    /// assert_eq!(&x * &a + &y * &b, g);
    /// assert!(x.abs() <= b.abs());
    /// assert!(y.abs() <= a.abs());
    /// ```
    ///
    /// # Panics
    ///
    /// `ibig!(0).extended_gcd(&ibig!(0))` panics.
    pub fn extended_gcd(&self, rhs: &IBig) -> (IBig, IBig, IBig) {
        let (g, x, y) = self.magnitude().extended_gcd(rhs.magnitude());
        (IBig::from(g), self.sign() * x, rhs.sign() * y)
    }
}
