package com.chyzman.chowl.util;

import com.ibm.icu.impl.Pair;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Collectors;

public class BiggerInteger extends Number implements Comparable<BiggerInteger>{

    final int signum;

    final int[][] mag;

    private BigInteger bitCountPlusOne;

    private BigInteger bitLenthPlusOne;

    private BigInteger lowestSetBitPlusTwo;

    static final long LONG_MASK = 0xffffffffL;

    private static final BigInteger MAX_MAG_LENGTH = BigInteger.valueOf(Integer.MAX_VALUE / Integer.SIZE + 1).pow(2); // (1 << 26)

    private static final BigInteger PRIME_SEARCH_BIT_LENGTH_LIMIT = BigInteger.valueOf(500000000).pow(2);

    private static final int KARATSUBA_THRESHOLD = 80*80;

    private static final int TOOM_COOK_THRESHOLD = 240*240;

    private static final int KARATSUBA_SQUARE_THRESHOLD = 128*128;

    private static final int TOOM_COOK_SQUARE_THRESHOLD = 216*216;

    static final int BURNIKEL_ZIEGLER_THRESHOLD = 80*80;

    static final int BURNIKEL_ZIEGLER_OFFSET = 40*40;

    static final int SCHOENHAGE_BASE_CONVERSION_THRESHOLD = 20*20;

    private static final int MULTIPLY_SQUARE_THRESHOLD = 20*20;

    private static final int MONTGOMERY_INTRINSIC_THRESHOLD = 512*512;

    //TODO maybe do public BigInteger(byte[] val, int off, int len) constructor

    //TODO maybe do public BigInteger(byte[] val) constructor

    private BiggerInteger(BigInteger[] val) {
        if (val.length == 0)
            throw new NumberFormatException("Zero length BiggerInteger");

        if (val[0].compareTo(BigInteger.ZERO) < 0) {
            mag = makePositive(val);
            signum = -1;
        } else {
            mag = trustedStripLeadingZeroInts(val);
            signum = (mag.length == 0 ? 0 : 1);
        }
        if (mag.bitLength().compareTo(MAX_MAG_LENGTH) >= 0) {
            checkRange();
        }
    }

    //TODO maybe do public BigInteger(int signum, byte[] magnitude, int off, int len) constructor

    //TODO maybe do public BigInteger(int signum, byte[] magnitude) constructor

    //TODO maybe do private BigInteger(int signum, int[] magnitude) constructor

    public BiggerInteger(String[] val, int radix) {
        int cursor = 0;
        int cursorIndex = 0;
        BigInteger numDigits;
        final BigInteger len = Arrays.stream(val).map(s -> BigInteger.valueOf(s.length())).reduce(BigInteger.ZERO, BigInteger::add);

        if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) throw new NumberFormatException("Radix out of range");
        if (len.compareTo(BigInteger.ZERO) == 0) throw new NumberFormatException("Zero length BiggerInteger");

        // Check for at most one leading sign
        int sign = 1;
        int index1 = val[0].lastIndexOf('-');
        int index2 = val[0].lastIndexOf('+');
        if (index1 >= 0) {
            if (index1 != 0 || index2 >= 0) {
                throw new NumberFormatException("Illegal embedded sign character");
            }
            sign = -1;
            cursorIndex = 1;
        } else if (index2 >= 0) {
            if (index2 != 0) {
                throw new NumberFormatException("Illegal embedded sign character");
            }
            cursorIndex = 1;
        }
        if (cursor == len)
            throw new NumberFormatException("Zero length BiggerInteger");

        // Skip leading zeros and compute number of digits in magnitude
        while (cursor < len &&
                Character.digit(val.charAt(cursor), radix) == 0) {
            cursor++;
        }

        if (cursor == len) {
            signum = 0;
            mag = ZERO.mag;
            return;
        }

        numDigits = len - cursor;
        signum = sign;

        // Pre-allocate array of expected size. May be too large but can
        // never be too small. Typically exact.
        long numBits = ((numDigits * bitsPerDigit[radix]) >>> 10) + 1;
        if (numBits + 31 >= (1L << 32)) {
            reportOverflow();
        }
        int numWords = (int) (numBits + 31) >>> 5;
        int[] magnitude = new int[numWords];

        // Process first (potentially short) digit group
        int firstGroupLen = numDigits % digitsPerInt[radix];
        if (firstGroupLen == 0)
            firstGroupLen = digitsPerInt[radix];
        String group = val.substring(cursor, cursor += firstGroupLen);
        magnitude[numWords - 1] = Integer.parseInt(group, radix);
        if (magnitude[numWords - 1] < 0)
            throw new NumberFormatException("Illegal digit");

        // Process remaining digit groups
        int superRadix = intRadix[radix];
        int groupVal = 0;
        while (cursor < len) {
            group = val.substring(cursor, cursor += digitsPerInt[radix]);
            groupVal = Integer.parseInt(group, radix);
            if (groupVal < 0)
                throw new NumberFormatException("Illegal digit");
            destructiveMulAdd(magnitude, superRadix, groupVal);
        }
        // Required for cases where the array was overallocated.
        mag = trustedStripLeadingZeroInts(magnitude);
        if (mag.length >= MAX_MAG_LENGTH) {
            checkRange();
        }
    }
}