/*
 *  ******************************************************************************
 *  *
 *  *
 *  * This program and the accompanying materials are made available under the
 *  * terms of the Apache License, Version 2.0 which is available at
 *  * https://www.apache.org/licenses/LICENSE-2.0.
 *  *
 *  *  See the NOTICE file distributed with this work for additional
 *  *  information regarding copyright ownership.
 *  * Unless required by applicable law or agreed to in writing, software
 *  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 *  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 *  * License for the specific language governing permissions and limitations
 *  * under the License.
 *  *
 *  * SPDX-License-Identifier: Apache-2.0
 *  *****************************************************************************
 */

package org.nd4j.common.util;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;

class Rational implements Cloneable {

    /* The maximum and minimum value of a standard Java integer, 2^31.
     */
    static BigInteger MAX_INT = BigInteger.valueOf(Integer.MAX_VALUE);
    static BigInteger MIN_INT = BigInteger.valueOf(Integer.MIN_VALUE);
    static Rational ONE = new Rational(1, 1);
    static Rational ZERO = new Rational();
    /**
     * numerator
     */
    BigInteger a;
    /**
     * denominator
     */
    BigInteger b;

    /**
     * Default ctor, which represents the zero.
     */
    public Rational() {
        a = BigInteger.ZERO;
        b = BigInteger.ONE;
    }

    /**
     * ctor from a numerator and denominator.
     *
     * @param a the numerator.
     * @param b the denominator.
     */
    public Rational(BigInteger a, BigInteger b) {
        this.a = a;
        this.b = b;
        normalize();
    }

    /**
     * ctor from a numerator.
     *
     * @param a the BigInteger.
     */
    public Rational(BigInteger a) {
        this.a = a;
        b = BigInteger.valueOf(1);
    }

    /**
     * ctor from a numerator and denominator.
     *
     * @param a the numerator.
     * @param b the denominator.
     */
    public Rational(int a, int b) {
        this(BigInteger.valueOf(a), BigInteger.valueOf(b));
    }

    /**
     * ctor from a string representation.
     *
     * @param str the string.
     *            This either has a slash in it, separating two integers, or, if there is no slash,
     *            is representing the numerator with implicit denominator equal to 1.
     * @warning this does not yet test for a denominator equal to zero
     */
    public Rational(String str) throws NumberFormatException {
        this(str, 10);
    }

    /**
     * ctor from a string representation in a specified base.
     *
     * @param str   the string.
     *              This either has a slash in it, separating two integers, or, if there is no slash,
     *              is just representing the numerator.
     * @param radix the number base for numerator and denominator
     * @warning this does not yet test for a denominator equal to zero
     * 5
     */
    public Rational(String str, int radix) throws NumberFormatException {
        int hasslah = str.indexOf("/");
        if (hasslah == -1) {
            a = new BigInteger(str, radix);
            b = new BigInteger("1", radix);
            /* no normalization necessary here */
        } else {
            /* create numerator and denominator separately
             */
            a = new BigInteger(str.substring(0, hasslah), radix);
            b = new BigInteger(str.substring(hasslah + 1), radix);
            normalize();
        }
    }

    /**
     * binomial (n choose m).
     *
     * @param n the numerator. Equals the size of the set to choose from.
     * @param m the denominator. Equals the number of elements to select.
     * @return the binomial coefficient.
     */
    public static Rational binomial(Rational n, BigInteger m) {
        if (m.compareTo(BigInteger.ZERO) == 0) {
            return Rational.ONE;
        }
        Rational bin = n;
        for (BigInteger i = BigInteger.valueOf(2); i.compareTo(m) != 1; i = i.add(BigInteger.ONE)) {
            bin = bin.multiply(n.subtract(i.subtract(BigInteger.ONE))).divide(i);
        }
        return bin;
    } /* Rational.binomial */

    /**
     * binomial (n choose m).
     *
     * @param n the numerator. Equals the size of the set to choose from.
     * @param m the denominator. Equals the number of elements to select.
     * @return the binomial coefficient.
     */
    public static Rational binomial(Rational n, int m) {
        if (m == 0) {
            return Rational.ONE;
        }
        Rational bin = n;
        for (int i = 2; i <= m; i++) {
            bin = bin.multiply(n.subtract(i - 1)).divide(i);
        }
        return bin;
    } /* Rational.binomial */

    /**
     * Create a copy.
     */
    @Override
    public Rational clone() {
        /* protected access means this does not work
         * return new Rational(a.clone(), b.clone()) ;
         */
        BigInteger aclon = new BigInteger("" + a);
        BigInteger bclon = new BigInteger("" + b);
        return new Rational(aclon, bclon);
    } /* Rational.clone */

    /**
     * Multiply by another fraction.
     *
     * @param val a second rational number.
     * @return the product of this with the val.
     */
    public Rational multiply(final Rational val) {
        BigInteger num = a.multiply(val.a);
        BigInteger deno = b.multiply(val.b);
        /* Normalization to an coprime format will be done inside
         * the ctor() and is not duplicated here.
         */
        return (new Rational(num, deno));
    } /* Rational.multiply */

    /**
     * Multiply by a BigInteger.
     *
     * @param val a second number.
     * @return the product of this with the value.
     */
    public Rational multiply(final BigInteger val) {
        Rational val2 = new Rational(val, BigInteger.ONE);
        return (multiply(val2));
    } /* Rational.multiply */

    /**
     * Multiply by an integer.
     *
     * @param val a second number.
     * @return the product of this with the value.
     */
    public Rational multiply(final int val) {
        BigInteger tmp = BigInteger.valueOf(val);
        return multiply(tmp);
    } /* Rational.multiply */

    /**
     * Power to an integer.
     *
     * @param exponent the exponent.
     * @return this value raised to the power given by the exponent.
     * If the exponent is 0, the value 1 is returned.
     */
    public Rational pow(int exponent) {
        if (exponent == 0) {
            return new Rational(1, 1);
        }
        BigInteger num = a.pow(Math.abs(exponent));
        BigInteger deno = b.pow(Math.abs(exponent));
        if (exponent > 0) {
            return (new Rational(num, deno));
        } else {
            return (new Rational(deno, num));
        }
    } /* Rational.pow */

    /**
     * Power to an integer.
     *
     * @param exponent the exponent.
     * @return this value raised to the power given by the exponent.
     * If the exponent is 0, the value 1 is returned.
     */
    public Rational pow(BigInteger exponent) throws NumberFormatException {
        /* test for overflow */
        if (exponent.compareTo(MAX_INT) == 1) {
            throw new NumberFormatException("Exponent " + exponent.toString() + " too large.");
        }
        if (exponent.compareTo(MIN_INT) == -1) {
            throw new NumberFormatException("Exponent " + exponent.toString() + " too small.");
        }
        /* promote to the simpler interface above */
        return pow(exponent.intValue());
    } /* Rational.pow */

    /**
     * Divide by another fraction.
     *
     * @param val A second rational number.
     * @return The value of this/val
     */
    public Rational divide(final Rational val) {
        BigInteger num = a.multiply(val.b);
        BigInteger deno = b.multiply(val.a);
        /* Reduction to a coprime format is done inside the ctor,
         * and not repeated here.
         */
        return (new Rational(num, deno));
    } /* Rational.divide */

    /**
     * Divide by an integer.
     *
     * @param val a second number.
     * @return the value of this/val
     */
    public Rational divide(BigInteger val) {
        Rational val2 = new Rational(val, BigInteger.ONE);
        return (divide(val2));
    } /* Rational.divide */

    /**
     * Divide by an integer.
     *
     * @param val A second number.
     * @return The value of this/val
     */
    public Rational divide(int val) {
        Rational val2 = new Rational(val, 1);
        return (divide(val2));
    } /* Rational.divide */

    /**
     * Add another fraction.
     *
     * @param val The number to be added
     * @return this+val.
     */
    public Rational add(Rational val) {
        BigInteger num = a.multiply(val.b).add(b.multiply(val.a));
        BigInteger deno = b.multiply(val.b);
        return (new Rational(num, deno));
    } /* Rational.add */

    /**
     * Add another integer.
     *
     * @param val The number to be added
     * @return this+val.
     */
    public Rational add(BigInteger val) {
        Rational val2 = new Rational(val, BigInteger.ONE);
        return (add(val2));
    } /* Rational.add */

    /**
     * Compute the negative.
     *
     * @return -this.
     */
    public Rational negate() {
        return (new Rational(a.negate(), b));
    } /* Rational.negate */

    /**
     * Subtract another fraction.
     * 7
     *
     * @param val the number to be subtracted from this
     * @return this - val.
     */
    public Rational subtract(Rational val) {
        Rational val2 = val.negate();
        return (add(val2));
    } /* Rational.subtract */

    /**
     * Subtract an integer.
     *
     * @param val the number to be subtracted from this
     * @return this - val.
     */
    public Rational subtract(BigInteger val) {
        Rational val2 = new Rational(val, BigInteger.ONE);
        return (subtract(val2));
    } /* Rational.subtract */

    /**
     * Subtract an integer.
     *
     * @param val the number to be subtracted from this
     * @return this - val.
     */
    public Rational subtract(int val) {
        Rational val2 = new Rational(val, 1);
        return (subtract(val2));
    } /* Rational.subtract */

    /**
     * Get the numerator.
     *
     * @return The numerator of the reduced fraction.
     */
    public BigInteger numer() {
        return a;
    }

    /**
     * Get the denominator.
     *
     * @return The denominator of the reduced fraction.
     */
    public BigInteger denom() {
        return b;
    }

    /**
     * Absolute value.
     *
     * @return The absolute (non-negative) value of this.
     */
    public Rational abs() {
        return (new Rational(a.abs(), b.abs()));
    }

    /**
     * floor(): the nearest integer not greater than this.
     *
     * @return The integer rounded towards negative infinity.
     */
    public BigInteger floor() {
        /* is already integer: return the numerator
         */
        if (b.compareTo(BigInteger.ONE) == 0) {
            return a;
        } else if (a.compareTo(BigInteger.ZERO) > 0) {
            return a.divide(b);
        } else {
            return a.divide(b).subtract(BigInteger.ONE);
        }
    } /* Rational.floor */


    /**
     * Remove the fractional part.
     *
     * @return The integer rounded towards zero.
     */
    public BigInteger trunc() {
        /* is already integer: return the numerator
         */
        if (b.compareTo(BigInteger.ONE) == 0) {
            return a;
        } else {
            return a.divide(b);
        }
    } /* Rational.trunc */


    /**
     * Compares the value of this with another constant.
     *
     * @param val the other constant to compare with
     * @return -1, 0 or 1 if this number is numerically less than, equal to,
     * or greater than val.
     */
    public int compareTo(final Rational val) {
        /* Since we have always kept the denominators positive,
         * simple cross-multiplying works without changing the sign.
         */
        final BigInteger left = a.multiply(val.b);
        final BigInteger right = val.a.multiply(b);
        return left.compareTo(right);
    } /* Rational.compareTo */


    /**
     * Compares the value of this with another constant.
     *
     * @param val the other constant to compare with
     * @return -1, 0 or 1 if this number is numerically less than, equal to,
     * or greater than val.
     */
    public int compareTo(final BigInteger val) {
        final Rational val2 = new Rational(val, BigInteger.ONE);
        return (compareTo(val2));
    } /* Rational.compareTo */


    /**
     * Return a string in the format number/denom.
     * If the denominator equals 1, print just the numerator without a slash.
     *
     * @return the human-readable version in base 10
     */
    @Override
    public String toString() {
        if (b.compareTo(BigInteger.ONE) != 0) {
            return (a.toString() + "/" + b.toString());
        } else {
            return a.toString();
        }
    } /* Rational.toString */


    /**
     * Return a double value representation.
     *
     * @return The value with double precision.
     */
    public double doubleValue() {
        /* To meet the risk of individual overflows of the exponents of
         * a separate invocation a.doubleValue() or b.doubleValue(), we divide first
         * in a BigDecimal environment and converst the result.
         */
        BigDecimal adivb = (new BigDecimal(a)).divide(new BigDecimal(b), MathContext.DECIMAL128);
        return adivb.doubleValue();
    } /* Rational.doubleValue */


    /**
     * Return a float value representation.
     *
     * @return The value with single precision.
     */
    public float floatValue() {
        BigDecimal adivb = (new BigDecimal(a)).divide(new BigDecimal(b), MathContext.DECIMAL128);
        return adivb.floatValue();
    } /* Rational.floatValue */


    /**
     * Return a representation as BigDecimal.
     *
     * @param mc the mathematical context which determines precision, rounding mode etc
     * @return A representation as a BigDecimal floating point number.
     */
    public BigDecimal BigDecimalValue(MathContext mc) {
        /* numerator and denominator individually rephrased
         */
        BigDecimal n = new BigDecimal(a);
        BigDecimal d = new BigDecimal(b);
        return n.divide(d, mc);
    } /* Rational.BigDecimnalValue */


    /**
     * Return a string in floating point format.
     *
     * @param digits The precision (number of digits)
     * @return The human-readable version in base 10.
     */
    public String toFString(int digits) {
        if (b.compareTo(BigInteger.ONE) != 0) {
            MathContext mc = new MathContext(digits, RoundingMode.DOWN);
            BigDecimal f = (new BigDecimal(a)).divide(new BigDecimal(b), mc);
            return (f.toString());
        } else {
            return a.toString();
        }
    } /* Rational.toFString */


    /**
     * Compares the value of this with another constant.
     *
     * @param val The other constant to compare with
     * @return The arithmetic maximum of this and val.
     */
    public Rational max(final Rational val) {
        if (compareTo(val) > 0) {
            return this;
        } else {
            return val;
        }
    } /* Rational.max */


    /**
     * Compares the value of this with another constant.
     *
     * @param val The other constant to compare with
     * @return The arithmetic minimum of this and val.
     */
    public Rational min(final Rational val) {
        if (compareTo(val) < 0) {
            return this;
        } else {
            return val;
        }
    } /* Rational.min */


    /**
     * Compute Pochhammer's symbol (this)_n.
     *
     * @param n The number of product terms in the evaluation.
     * @return Gamma(this+n)/Gamma(this) = this*(this+1)*...*(this+n-1).
     */
    public Rational Pochhammer(final BigInteger n) {
        if (n.compareTo(BigInteger.ZERO) < 0) {
            return null;
        } else if (n.compareTo(BigInteger.ZERO) == 0) {
            return Rational.ONE;
        } else {
            /* initialize results with the current value
             */
            Rational res = new Rational(a, b);
            BigInteger i = BigInteger.ONE;
            for (; i.compareTo(n) < 0; i = i.add(BigInteger.ONE)) {
                res = res.multiply(add(i));
            }
            return res;
        }
    } /* Rational.pochhammer */


    /**
     * Compute pochhammer's symbol (this)_n.
     *
     * @param n The number of product terms in the evaluation.
     * @return Gamma(this+n)/GAMMA(this).
     */
    public Rational Pochhammer(int n) {
        return Pochhammer(BigInteger.valueOf(n));
    } /* Rational.pochhammer */


    /**
     * Normalize to coprime numerator and denominator.
     * Also copy a negative sign of the denominator to the numerator.
     */
    protected void normalize() {
        /* compute greatest common divisor of numerator and denominator
         */
        final BigInteger g = a.gcd(b);
        if (g.compareTo(BigInteger.ONE) > 0) {
            a = a.divide(g);
            b = b.divide(g);
        }
        if (b.compareTo(BigInteger.ZERO) == -1) {
            a = a.negate();
            b = b.negate();
        }
    } /* Rational.normalize */

} /* Rational */
