package math.kombi;

import math.bigs.BigsUtil;

import java.math.BigDecimal;
import java.math.BigInteger;

/**
 * Klasa do wykonywania obliczeń na ułamkach
 *
 * @author Jacek
 */
public class Fraction implements Comparable<Fraction>, Cloneable {
    public static final BigInteger TEN = BigInteger.valueOf(10);
    public static final Fraction ZERO = new Fraction(BigInteger.ZERO, BigInteger.ONE);
    public static final Fraction ONE = new Fraction(BigInteger.ONE, BigInteger.ONE);
    private BigInteger numerator;
    private BigInteger denominator;

    public Fraction(BigInteger num, BigInteger den) {
        if (den.equals(BigInteger.ZERO)) {
            throw new IllegalArgumentException("Nie można dzielić przez zero");
        }
        if (BigsUtil.mniejszy(den, BigInteger.ZERO)) {
            num = num.negate();
            den = den.negate();
        }
        BigInteger nwd = num.gcd(den);
        if (!nwd.equals(BigInteger.ONE)) {
            num = num.divide(nwd);
            den = den.divide(nwd);
        }
        numerator = num;
        denominator = den;
    }

    public Fraction(int num, int den) {
        BigInteger numer = new BigInteger(String.valueOf(num));
        BigInteger denom = new BigInteger(String.valueOf(den));
        if (denom.equals(BigInteger.ZERO)) {
            throw new IllegalArgumentException("Nie można dzielić przez zero");
        }
        if (BigsUtil.mniejszy(denom, BigInteger.ZERO)) {
            numer = numer.negate();
            denom = denom.negate();
        }
        BigInteger nwd = numer.gcd(denom);
        if (!nwd.equals(BigInteger.ONE)) {
            numer = numer.divide(nwd);
            denom = denom.divide(nwd);
        }
        numerator = numer;
        denominator = denom;
    }

    public Fraction(long num, long den) {
        BigInteger numer = new BigInteger(String.valueOf(num));
        BigInteger denom = new BigInteger(String.valueOf(den));
        if (denom.equals(BigInteger.ZERO)) {
            throw new IllegalArgumentException("Nie można dzielić przez zero");
        }
        if (BigsUtil.mniejszy(denom, BigInteger.ZERO)) {
            numer = numer.negate();
            denom = denom.negate();
        }
        BigInteger nwd = numer.gcd(denom);
        if (!nwd.equals(BigInteger.ONE)) {
            numer = numer.divide(nwd);
            denom = denom.divide(nwd);
        }
        numerator = numer;
        denominator = denom;
    }

    //num i den muszą być liczbami całkowitymi, den - nie może być zerem
    public Fraction(String num, String den) {
        BigInteger numer = new BigInteger(num);
        BigInteger denom = new BigInteger(den);
        if (denom.equals(BigInteger.ZERO)) {
            throw new IllegalArgumentException("Nie można dzielić przez zero");
        }
        if (BigsUtil.mniejszy(denom, BigInteger.ZERO)) {
            numer = numer.negate();
            denom = denom.negate();
        }
        BigInteger nwd = numer.gcd(denom);
        if (!nwd.equals(BigInteger.ONE)) {
            numer = numer.divide(nwd);
            denom = denom.divide(nwd);
        }
        numerator = numer;
        denominator = denom;
    }

    //ulamek zwykly: 1.26
    //ulamek okresowy 1.26(27)
    public Fraction(String fractDecimal) {
        BigInteger[] tabl = fractDecimToNormal(fractDecimal);
        BigInteger numer = tabl[0];
        BigInteger denom = tabl[1];
        if (denom.equals(BigInteger.ZERO)) {
            throw new IllegalArgumentException("Nie można dzielić przez zero");
        }
        if (BigsUtil.mniejszy(denom, BigInteger.ZERO)) {
            numer = numer.negate();
            denom = denom.negate();
        }
        BigInteger nwd = numer.gcd(denom);
        if (!nwd.equals(BigInteger.ONE)) {
            numer = numer.divide(nwd);
            denom = denom.divide(nwd);
        }
        numerator = numer;
        denominator = denom;
    }

    public Fraction(BigDecimal x) {
        BigInteger[] tabl = fractDecimToNormal(x);
        BigInteger numer = tabl[0];
        BigInteger denom = tabl[1];
        if (denom.equals(BigInteger.ZERO)) {
            throw new IllegalArgumentException("Nie można dzielić przez zero");
        }
        if (BigsUtil.mniejszy(denom, BigInteger.ZERO)) {
            numer = numer.negate();
            denom = denom.negate();
        }
        BigInteger nwd = numer.gcd(denom);
        if (!nwd.equals(BigInteger.ONE)) {
            numer = numer.divide(nwd);
            denom = denom.divide(nwd);
        }
        numerator = numer;
        denominator = denom;
    }

    public Fraction(double x) {
        BigInteger[] tabl = fractDecimToNormal(String.valueOf(x));
        BigInteger numer = tabl[0];
        BigInteger denom = tabl[1];
        if (denom.equals(BigInteger.ZERO)) {
            throw new IllegalArgumentException("Nie można dzielić przez zero");
        }
        if (BigsUtil.mniejszy(denom, BigInteger.ZERO)) {
            numer = numer.negate();
            denom = denom.negate();
        }
        BigInteger nwd = numer.gcd(denom);
        if (!nwd.equals(BigInteger.ONE)) {
            numer = numer.divide(nwd);
            denom = denom.divide(nwd);
        }
        numerator = numer;
        denominator = denom;
    }

    public Fraction(Fraction x) {
        this.numerator = x.numerator;
        this.denominator = x.denominator;
    }

    @Override
    public int compareTo(Fraction o) {
        BigInteger a = numerator.multiply(o.denominator);
        BigInteger b = o.numerator.multiply(denominator);
        if (BigsUtil.mniejszy(a, b)) {
            return -1;
        } else if (BigsUtil.wiekszy(a, b)) {
            return 1;
        } else {
            return 0;
        }
    }

    @Override
    public String toString() {
        return "[" + numerator + ", " + denominator + "]";
    }

    @SuppressWarnings("InfiniteRecursion")
    @Override
    public boolean equals(Object other) {
        if (this.equals(other)) {
            return true;
        }
        if (other.equals(null)) {
            return false;
        }
        if (!getClass().equals(other.getClass())) {
            return false;
        }
        if (!(other instanceof Fraction)) {
            return false;
        }
        Fraction a = (Fraction) other;
        return (numerator.equals(a.numerator))
                && (denominator.equals(a.denominator));
    }

    public static boolean equals(Fraction x, Fraction y) {
        return (x.numerator.equals(y.numerator))
                && (x.denominator.equals(y.denominator));
    }

    @Override
    public int hashCode() {
        return 17 * numerator.hashCode() + 19 * denominator.hashCode();
    }

    public BigInteger getNumerator() {
        return numerator;
    }

    public BigInteger getDenominator() {
        return denominator;
    }

    public BigInteger[] getAsTable() {
        return new BigInteger[]{numerator, denominator};
    }

    public BigDecimal getAsBigDecimal(int precision) {
        return BigsUtil.divide(numerator, denominator, precision);
    }

    public static Fraction zero() {
        return ZERO;
    }

    public static Fraction one() {
        return ONE;
    }

    public static Fraction add(Fraction x, Fraction y) {
        return new Fraction(
                (x.numerator.multiply(y.denominator))
                        .add(x.denominator.multiply(y.numerator)),
                x.denominator.multiply(y.denominator));
    }

    public Fraction add(Fraction x) {
        Fraction result = new Fraction(
                (x.numerator.multiply(this.denominator))
                        .add(x.denominator.multiply(this.numerator)),
                x.denominator.multiply(this.denominator));
        this.numerator = result.numerator;
        this.denominator = result.denominator;
        return result;
    }

    public static Fraction subtract(Fraction x, Fraction y) {
        return new Fraction(
                (x.numerator.multiply(y.denominator))
                        .subtract(x.denominator.multiply(y.numerator)),
                x.denominator.multiply(y.denominator));
    }

    public Fraction subtract(Fraction x) {
        Fraction result = new Fraction(
                (x.numerator.multiply(this.denominator))
                        .subtract(x.denominator.multiply(this.numerator)),
                x.denominator.multiply(this.denominator));
        this.numerator = result.numerator;
        this.denominator = result.denominator;
        return result;
    }

    public static Fraction mult(Fraction x, Fraction y) {
        return new Fraction(x.numerator.multiply(y.numerator),
                x.denominator.multiply(y.denominator));
    }

    public Fraction mult(Fraction x) {
        Fraction result = new Fraction(x.numerator.multiply(this.numerator),
                x.denominator.multiply(this.denominator));
        this.numerator = result.numerator;
        this.denominator = result.denominator;
        return result;
    }

    public static Fraction div(Fraction dividend, Fraction divisor) {
        return new Fraction(
                dividend.numerator.multiply(divisor.denominator),
                dividend.denominator.multiply(divisor.numerator));
    }

    public Fraction div(Fraction divisor) {
        Fraction result = new Fraction(
                this.numerator.multiply(divisor.denominator),
                this.denominator.multiply(divisor.numerator));
        this.numerator = result.numerator;
        this.denominator = result.denominator;
        return result;
    }

    public static Fraction add(Fraction fract, int num, int den) {
        Fraction result = new Fraction(num, den);
        return add(fract, result);
    }

    public Fraction add(int num, int den) {
        Fraction result = new Fraction(num, den);
        return this.add(result);
    }

    public static Fraction subtract(Fraction fract, int num, int den) {
        Fraction result = new Fraction(num, den);
        return subtract(fract, result);
    }

    public Fraction subtract(int num, int den) {
        Fraction result = new Fraction(num, den);
        return this.subtract(result);
    }

    public static Fraction mult(Fraction fract, int num, int den) {
        Fraction result = new Fraction(num, den);
        return mult(fract, result);
    }

    public Fraction mult(int num, int den) {
        Fraction result = new Fraction(num, den);
        return this.mult(result);
    }

    public static Fraction div(Fraction fract, int num, int den) {
        Fraction result = new Fraction(num, den);
        return div(fract, result);
    }

    public Fraction div(int num, int den) {
        Fraction result = new Fraction(num, den);
        return this.div(result);
    }

    public static Fraction add(Fraction fract, long num, long den) {
        Fraction result = new Fraction(num, den);
        return add(fract, result);
    }

    public Fraction add(long num, long den) {
        Fraction result = new Fraction(num, den);
        return this.add(result);
    }

    public static Fraction subtract(Fraction fract, long num, long den) {
        Fraction result = new Fraction(num, den);
        return subtract(fract, result);
    }

    public Fraction subtract(long num, long den) {
        Fraction result = new Fraction(num, den);
        return this.subtract(result);
    }

    public static Fraction mult(Fraction fract, long num, long den) {
        Fraction result = new Fraction(num, den);
        return mult(fract, result);
    }

    public Fraction mult(long num, long den) {
        Fraction result = new Fraction(num, den);
        return this.mult(result);
    }

    public static Fraction div(Fraction fract, long num, long den) {
        Fraction result = new Fraction(num, den);
        return div(fract, result);
    }

    public Fraction div(long num, long den) {
        Fraction result = new Fraction(num, den);
        return this.div(result);
    }

    public static Fraction add(Fraction fract, String num, String den) {
        Fraction result = new Fraction(num, den);
        return add(fract, result);
    }

    public Fraction add(String num, String den) {
        Fraction result = new Fraction(num, den);
        return this.add(result);
    }

    public static Fraction subtract(Fraction fract, String num, String den) {
        Fraction result = new Fraction(num, den);
        return subtract(fract, result);
    }

    public Fraction subtract(String num, String den) {
        Fraction result = new Fraction(num, den);
        return this.subtract(result);
    }

    public static Fraction mult(Fraction fract, String num, String den) {
        Fraction result = new Fraction(num, den);
        return mult(fract, result);
    }

    public Fraction mult(String num, String den) {
        Fraction result = new Fraction(num, den);
        return this.mult(result);
    }

    public static Fraction div(Fraction fract, String num, String den) {
        Fraction result = new Fraction(num, den);
        return div(fract, result);
    }

    public Fraction div(String num, String den) {
        Fraction result = new Fraction(num, den);
        return this.div(result);
    }

    public static Fraction add(Fraction fract1, String fract2) {
        Fraction result = new Fraction(fract2);
        return add(fract1, result);
    }

    public Fraction add(String fract2) {
        Fraction result = new Fraction(fract2);
        return this.add(result);
    }

    public static Fraction subtract(Fraction fract1, String fract2) {
        Fraction result = new Fraction(fract2);
        return subtract(fract1, result);
    }

    public Fraction subtract(String fract2) {
        Fraction result = new Fraction(fract2);
        return this.subtract(result);
    }

    public static Fraction mult(Fraction fract1, String fract2) {
        Fraction result = new Fraction(fract2);
        return mult(fract1, result);
    }

    public Fraction mult(String fract2) {
        Fraction result = new Fraction(fract2);
        return this.mult(result);
    }

    public static Fraction div(Fraction fract1, String fract2) {
        Fraction result = new Fraction(fract2);
        return div(fract1, result);
    }

    public Fraction div(String fract2) {
        Fraction result = new Fraction(fract2);
        return this.div(result);
    }

    //--------------------------------
    public static Fraction add(Fraction fract, BigInteger num, BigInteger den) {
        Fraction result = new Fraction(num, den);
        return add(fract, result);
    }

    public Fraction add(BigInteger num, BigInteger den) {
        Fraction result = new Fraction(num, den);
        return this.add(result);
    }

    public static Fraction subtract(Fraction fract, BigInteger num,
                                    BigInteger den) {
        Fraction result = new Fraction(num, den);
        return subtract(fract, result);
    }

    public Fraction subtract(BigInteger num, BigInteger den) {
        Fraction result = new Fraction(num, den);
        return this.subtract(result);
    }

    public static Fraction mult(Fraction fract, BigInteger num,
                                BigInteger den) {
        Fraction result = new Fraction(num, den);
        return mult(fract, result);
    }

    public Fraction mult(BigInteger num, BigInteger den) {
        Fraction result = new Fraction(num, den);
        return this.mult(result);
    }

    public static Fraction div(Fraction fract, BigInteger num, BigInteger den) {
        Fraction result = new Fraction(num, den);
        return div(fract, result);
    }

    public Fraction div(BigInteger num, BigInteger den) {
        Fraction result = new Fraction(num, den);
        return this.div(result);
    }

    public static Fraction add(Fraction fract1, BigDecimal fract2) {
        Fraction result = new Fraction(fract2);
        return add(fract1, result);
    }

    public Fraction add(BigDecimal fract2) {
        Fraction result = new Fraction(fract2);
        return this.add(result);
    }

    public static Fraction subtract(Fraction fract1, BigDecimal fract2) {
        Fraction result = new Fraction(fract2);
        return subtract(fract1, result);
    }

    public Fraction subtract(BigDecimal fract2) {
        Fraction result = new Fraction(fract2);
        return this.subtract(result);
    }

    public static Fraction mult(Fraction fract1, BigDecimal fract2) {
        Fraction result = new Fraction(fract2);
        return mult(fract1, result);
    }

    public Fraction mult(BigDecimal fract2) {
        Fraction result = new Fraction(fract2);
        return this.mult(result);
    }

    public static Fraction div(Fraction fract1, BigDecimal fract2) {
        Fraction result = new Fraction(fract2);
        return div(fract1, result);
    }

    public Fraction div(BigDecimal fract2) {
        Fraction result = new Fraction(fract2);
        return this.div(result);
    }

    public Fraction neg() {
        Fraction result = new Fraction(this.numerator.negate(),
                this.denominator);
        this.numerator = result.numerator;
        return result;
    }

    public static Fraction neg(Fraction x) {
        return new Fraction(x.numerator.negate(), x.denominator);
    }

    public Fraction recip() {
        if (this.numerator.equals(BigInteger.ZERO)) {
            throw new ArithmeticException("nie można dzielić przez zero");
        }
        Fraction result = new Fraction(this.denominator, this.numerator);
        this.numerator = result.numerator;
        this.denominator = result.denominator;
        return result;
    }

    public static Fraction recip(Fraction x) {
        if (x.numerator.equals(BigInteger.ZERO)) {
            throw new ArithmeticException("nie można dzielić przez zero");
        }
        return new Fraction(x.denominator, x.numerator);
    }

    public Fraction pow(int exp) {
        Fraction result = new Fraction((this.numerator.pow(exp)),
                this.denominator.pow(exp));
        this.numerator = result.numerator;
        this.denominator = result.denominator;
        return result;
    }

    public static Fraction pow(Fraction x, int exp) {
        return new Fraction((x.numerator.pow(exp)), x.denominator.pow(exp));
    }

    //Zamienia ulamek dziesiętny na zwykły
    //zwraca tabl
    // tabl[0] licznik
    //tabl[1] mianownik
    public static BigInteger[] fractDecimToNormal(BigDecimal n) {
        BigInteger[] tabl = new BigInteger[2];
        tabl[0] = n.unscaledValue();
        tabl[1] = new BigInteger("10").pow(n.scale());
        BigInteger nwd = tabl[0].gcd(tabl[1]);
        tabl[0] = tabl[0].divide(nwd);
        tabl[1] = tabl[1].divide(nwd);
        return tabl;
    }

    //Zamienia ulamek dziesiętny na zwykły
    //ulamek okresowy zapisac jako 1.92(6)
    //zwraca tabl
    // tabl[0] licznik
    //tabl[1] mianownik
    public static BigInteger[] fractDecimToNormal(String fractDecimal) {
        BigInteger[] tabl = new BigInteger[2];
        int start = fractDecimal.indexOf("(");
        if (start != -1) {
            int end = fractDecimal.indexOf(")");
            int dot = fractDecimal.indexOf(".");
            int fractLen = start - dot - 1;
            int perLen = end - start - 1;
            BigInteger mn = TEN.pow(perLen);
            String nums = fractDecimal.substring(0, start).concat(
                    fractDecimal.substring(start + 1, start + 1 + perLen));
            String x = nums.substring(0, start);
            BigDecimal numsd = new BigDecimal(nums)
                    .multiply(new BigDecimal(mn));
            BigDecimal xd = new BigDecimal(x);
            BigDecimal rozn = numsd.subtract(xd);
            tabl[0] = ((new BigDecimal("10").pow(fractLen)).multiply(rozn))
                    .toBigIntegerExact();
            tabl[1] = TEN.pow(fractLen).multiply(mn.subtract(BigInteger.ONE));
            BigInteger nwd = tabl[0].gcd(tabl[1]);
            tabl[0] = tabl[0].divide(nwd);
            tabl[1] = tabl[1].divide(nwd);
        } else {
            int dot = fractDecimal.indexOf(".");
            int fractLen = fractDecimal.length() - dot - 1;
            tabl[0] = (new BigDecimal(fractDecimal)
                    .multiply(new BigDecimal("10").pow(fractLen)))
                    .toBigIntegerExact();
            tabl[1] = TEN.pow(fractLen);
            BigInteger nwd = tabl[0].gcd(tabl[1]);
            tabl[0] = tabl[0].divide(nwd);
            tabl[1] = tabl[1].divide(nwd);
        }
        return tabl;
    }

    public boolean mniejszy(Fraction a, Fraction b, int precision) {
        BigDecimal c = a.getAsBigDecimal(precision);
        BigDecimal d = b.getAsBigDecimal(precision);
        return (c.compareTo(d) < 0);
    }

    public boolean wiekszy(Fraction a, Fraction b, int precision) {
        BigDecimal c = a.getAsBigDecimal(precision);
        BigDecimal d = b.getAsBigDecimal(precision);
        return (c.compareTo(d) > 0);
    }

    @SuppressWarnings("MethodDoesntCallSuperMethod")
    @Override
    public Fraction clone() {
        return new Fraction(numerator, denominator);
    }
}
