package math.prime;

import math.bigs.BigsUtil;
import math.utils.FrequencyMap;
import math.utils.MathUtil;
import math.utils.Tuple2L;

import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Objects;

public class PrimeUtil {
    private static final BigInteger ONE = BigInteger.valueOf(1);
    private static final BigInteger TWO = BigInteger.valueOf(2);

    private PrimeUtil() {
    }


    public static boolean isPrime(long n) {
        for (long j = 2; Math.pow(j, 2) <= n; j++) {
            if (n % j == 0) {
                return false;
            }
        }
        return true;
    }

    public static boolean isPrime(BigInteger n) {
        for (BigInteger j = TWO; BigsUtil.mniejszy(j.pow(2), n)
                || BigsUtil.rowny(j.pow(2), n); j = j.add(ONE)) {
            if (n.mod(j).equals(BigInteger.ZERO)) {
                return false;
            }
        }
        return true;
    }

    /**
     * zwraca pierwszą liczbę pierwszą większą od podanej liczby
     *
     * @param min int - liczba progowa
     * @return int - pierwsza liczba pierwsza większa od min
     */
    public static long getPrime(long min) {
        for (long j = min + 1; true; j++) {
            if (isPrime(j)) {
                return j;
            }
        }
    }

    public static BigInteger getPrime(BigInteger min) {
        for (BigInteger j = min.add(ONE); true; j = j.add(ONE)) {
            if (isPrime(j)) {
                return j;
            }
        }
    }

    /**
     * oblicza liczby pierwsze w podanym zakresie
     *
     * @param min long - pierwsza liczba
     * @param max long - większa liczba
     * @return ArrayList<Integer> - arraylista liczb pierwszych
     * leżących między min, a max
     */
    public static ArrayList<Long> getPrimes(long min, long max) {
        ArrayList<Long> list = new ArrayList<>();
        for (long j = min + 1; j < max; j++) {
            if (isPrime(j)) {
                list.add(j);
            }
        }
        return list;
    }

    public static ArrayList<BigInteger> getPrimes(BigInteger min, BigInteger max) {
        ArrayList<BigInteger> list = new ArrayList<>();
        for (BigInteger j = min.add(ONE); BigsUtil.mniejszy(j, max); j = j.add(ONE)) {
            if (isPrime(j)) {
                list.add(j);
            }
        }
        return list;
    }

    /**
     * Oblicza liczbę Mersenna = Math.pow(2, exp)-1, ale
     * robi to szybciej
     */
    public static BigInteger generateMersenneNumber(int exp) {
        BigInteger p = ONE;
        for (int i = 1; i <= exp; ++i) {
            p = p.add(p);
        }
        p = p.subtract(ONE);
        return p;
    }


    public static long generateMersenneNumber2(int exp) {
        return (long) Math.pow(2L, exp) - 1L;
    }

    /**
     * Generuje liczbę pierwszą Eulera
     *
     * @param n - liczba wyjściowa
     * @return - liczba Eulera
     */

    public static long euler(long n) {
        return (long) Math.pow(n, 2L) - 79L * n + 1601L;
    }

    public static BigInteger euler(BigInteger n) {
        return n.pow(2).subtract(BigInteger.valueOf(79).multiply(n))
                .add(BigInteger.valueOf(1601));
    }

    public static void printBlizniacze(long min, long max, String path) {
        PrintWriter pw = null;
        try {
            pw = new PrintWriter(path);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        for (long j = min + 1; j < max; j++) {
            if (isPrime(j) && isPrime(j + 2)) {
                Objects.requireNonNull(pw).println("<p>(" + j + ", " + (j + 2) + ")</p>");
            }
        }
        Objects.requireNonNull(pw).close();
    }

    public static void printCzworacze(long min, long max, String path) {
        PrintWriter pw = null;
        try {
            pw = new PrintWriter(path);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        for (long j = min + 1; j < max; j++) {
            if (isPrime(j) && isPrime(j + 2) && isPrime(j + 6)
                    && isPrime(j + 8)) {
                Objects.requireNonNull(pw).println("<p>(" + j + ", " + (j + 2) + ", " + (j + 6) + ", "
                        + (j + 8) + ")</p>");
            }
        }
        Objects.requireNonNull(pw).close();
    }

    public static void printIzolowane(int min, int max, String path) {
        PrintWriter pw = null;
        try {
            pw = new PrintWriter(path);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        ArrayList<Integer> al = getPrimes(min, max);
        for (int i = 1; i < al.size() - 1; i++) {
            int t1 = al.get(i - 1);
            int t2 = al.get(i);
            int t3 = al.get(i + 1);
            if (((t2 - t1) >= 4) && ((t3 - t2) >= 4)) {
                Objects.requireNonNull(pw).println("<p>" + t2 + "</p>");
            }
        }
        Objects.requireNonNull(pw).close();
    }

    /**
     * oblicza liczby pierwsze w podanym zakresie
     *
     * @param min int - pierwsza liczba
     * @param max int - większa liczba
     * @return ArrayList<Integer> - arraylista liczb pierwszych
     * leżących między min, a max
     */
    public static ArrayList<Integer> getPrimes(int min, int max) {
        ArrayList<Integer> list = new ArrayList<>();
        for (int j = min + 1; j < max; j++) {
            if (isPrime(j)) {
                list.add(j);
            }
        }
        return list;
    }

    public static void printGermain(long min, long max, String path) {
        PrintWriter pw = null;
        try {
            pw = new PrintWriter(path);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        for (long j = min + 1; j < max; j++) {
            if (isPrime(j) && isPrime((j * 2) + 1)) {
                Objects.requireNonNull(pw).println("<p>" + j + " (" + (j * 2 + 1) + ")</p>");
            }
        }
        Objects.requireNonNull(pw).close();
    }

    public static void printLustrzane(int min, int max, String path) {
        PrintWriter pw = null;
        try {
            pw = new PrintWriter(path);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        for (int j = min + 1; j < max; j++) {
            int a = MathUtil.reverteInt(j);
            if (isPrime(j) && isPrime(a) && (j != a)) {
                Objects.requireNonNull(pw).println("<p>(" + j + ", " + a + ")</p>");
            }
        }
        Objects.requireNonNull(pw).close();
    }

    public static void printPalindrom(int min, int max, String path) {
        PrintWriter pw = null;
        try {
            pw = new PrintWriter(path);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        for (int j = min + 1; j < max; j++) {
            int a = MathUtil.reverteInt(j);
            if (isPrime(j) && isPrime(a) && (j == a)) {
                Objects.requireNonNull(pw).println(
                        "<p>(" + j + ", " + MathUtil.reverteInt(j) + ")</p>");
            }
        }
        Objects.requireNonNull(pw).close();
    }

    /**
     * Oblicza liczbę liczb pierwszych w podanym przedziale
     *
     * @param min - początek przedziału
     * @param max - koniec przedziału
     * @return liczba liczb pierwszych w przedziale
     */
    public static long getNPrimes(long min, long max) {
        long sum = 0;
        for (long j = min + 1; j < max; j++) {
            if (PrimeUtil.isPrime(j)) {
                sum++;
            }
        }
        return sum;
    }

    /**
     * Rozkłada liczbe na iloczyn liczb pierwszych
     *
     * @param n - liczba do rozlozenia
     * @return - tablice punktow. W kazdym punkcie x oznacza
     * liczbe, a y liczbe wystąpień tej liczby w iloczynie.
     * Jeśli x = 2, a y = 3 to mamy 2 x 2 x 2 x natępny punkt;
     */
    public static Tuple2L[] factorize1(long n) {
        long sqrt = (long) Math.sqrt(n);
        if (sqrt * sqrt == n) {
            return new Tuple2L[]{new Tuple2L(2, sqrt)};
        }
        if (n < 10) {
            sqrt++;
        }
        if (n < 2) {
            return new Tuple2L[]{new Tuple2L(1, 1)};
        }
        ArrayList<Long> primes = getPrimes(1, sqrt);
        FrequencyMap<Long> fm = new FrequencyMap<>();
        for (long p : primes) {
            while (n % p == 0) {
                fm.add(p);
                n /= p;
            }
            if (n == 1) {
                break;
            }
        }
        if (n > 1) {
            fm.add(n);
        }
        return fm.getAllAsTuplesL();
    }

    /**
     * to samo co factorize1, ale zwraca tablice intów zawierającą
     * liczby, kóre należy pomnożyć przez siebie, (mnożniki) aby
     * otrzymac liczbę rozkladana na czyniki
     */
    public static long[] factorize2(long n) {
        long sqrt = (long) Math.sqrt(n);
        if (sqrt * sqrt == n) {
            return new long[]{sqrt, sqrt};
        }
        if (n < 10) {
            sqrt++;
        }
        if (n < 2) {
            return new long[]{1};
        }
        ArrayList<Long> primes = getPrimes(1, sqrt);
        FrequencyMap<Long> fm = new FrequencyMap<>();
        for (long p : primes) {
            while (n % p == 0) {
                fm.add(p);
                n /= p;
            }
            if (n == 1) {
                break;
            }
        }
        if (n > 1) {
            fm.add(n);
        }
        return fm.getAllAsLongs();
    }
}
