import java.util.Arrays;
import java.util.Locale;
import java.util.Random;

/**
 * Reproducible test of the 5-by-4 amino-acid arrangement in 20amino.htm.
 *
 * All values are stored in thousandths.  This permits the two half-cent row
 * calculations (.025 and .975) without floating-point rounding or truncation.
 *
 * Usage:
 *   java amino [trials] [seed]
 *
 * Defaults:
 *   trials = 1,000,000
 *   seed   = 20260805
 */
public class amino {
    private static final int DEFAULT_TRIALS = 1_000_000;
    private static final long DEFAULT_SEED = 20_260_805L;

    // Molecular weights from the displayed table, multiplied by 1,000.
    private static final int[] ORIGINAL = {
        133_100, 132_120,  89_090, 131_170,
        174_200, 165_190, 115_130, 131_170,
        146_190, 181_190, 119_120, 149_210,
        155_150, 204_230, 105_090, 121_160,
        147_130, 146_140,  75_070, 117_150
    };

    /*
     * Required decimal endings, expressed as the final three digits.
     * Entry 0 is the grand total; entries 1-8 are the displayed partitions.
     */
    private static final int[] GOD_PRIME_PAIR_ENDINGS = {
        0, 630, 370, 270, 730, 550, 450, 25, 975
    };

    private static final int[] SECULAR_PRIME_PAIR_ENDINGS = {
        0, 750, 250, 480, 520, 385, 615, 25, 975
    };

    private static final class Results {
        long godPrimePairMatches;
        long secularPrimePairMatches;
    }

    public static void main(String[] args) {
        int trials = args.length > 0 ? parseTrials(args[0]) : DEFAULT_TRIALS;
        long seed = args.length > 1 ? parseSeed(args[1]) : DEFAULT_SEED;

        printTotalIdentities();
        printOriginalArrangement();

        Results results = runTrials(trials, seed);
        System.out.println();
        System.out.printf(Locale.US, "Randomized test: %,d trials; seed = %d%n",
                          trials, seed);
        printFrequency("37/73 ending pattern", results.godPrimePairMatches, trials);
        printFrequency("25/52 control pattern", results.secularPrimePairMatches, trials);
        System.out.println("Each trial uses an unbiased Fisher-Yates shuffle.");
        System.out.println("These are empirical frequencies, not a calculation using 20!.");
    }

    private static Results runTrials(int trials, long seed) {
        Random random = new Random(seed);
        int[] arrangement = new int[ORIGINAL.length];
        Results results = new Results();

        for (int trial = 0; trial < trials; trial++) {
            System.arraycopy(ORIGINAL, 0, arrangement, 0, ORIGINAL.length);
            shuffle(arrangement, random);

            int[] values = calculateValues(arrangement);
            if (matches(values, GOD_PRIME_PAIR_ENDINGS)) {
                results.godPrimePairMatches++;
            }
            if (matches(values, SECULAR_PRIME_PAIR_ENDINGS)) {
                results.secularPrimePairMatches++;
            }
        }
        return results;
    }

    private static void shuffle(int[] values, Random random) {
        for (int i = values.length - 1; i > 0; i--) {
            int j = random.nextInt(i + 1);
            int temporary = values[i];
            values[i] = values[j];
            values[j] = temporary;
        }
    }

    /** Returns the grand total followed by the eight displayed partitions. */
    private static int[] calculateValues(int[] w) {
        int[] value = new int[9];

        for (int weight : w) {
            value[0] += weight;
        }

        for (int i = 0; i < w.length; i += 4) {
            value[1] += w[i] + w[i + 3];
            value[2] += w[i + 1] + w[i + 2];
            value[3] += w[i] + w[i + 2];
            value[4] += w[i + 1] + w[i + 3];
        }

        value[5] = w[1] + w[3] + w[4] + w[6] + w[9]
                 + w[11] + w[12] + w[14] + w[17] + w[19];
        value[6] = w[0] + w[2] + w[5] + w[7] + w[8]
                 + w[10] + w[13] + w[15] + w[16] + w[18];

        int rowA = w[0] + w[1] + w[2] + w[3];
        int rowB = w[4] + w[5] + w[6] + w[7];
        int rowC = w[8] + w[9] + w[10] + w[11];
        int rowD = w[12] + w[13] + w[14] + w[15];
        int rowE = w[16] + w[17] + w[18] + w[19];

        // Work in doubled thousandths so division by two remains exact.
        value[7] = (2 * rowA + 2 * rowB + rowC) / 2;
        value[8] = (2 * rowD + 2 * rowE + rowC) / 2;
        return value;
    }

    private static boolean matches(int[] values, int[] requiredEndings) {
        for (int i = 0; i < values.length; i++) {
            if (Math.floorMod(values[i], 1000) != requiredEndings[i]) {
                return false;
            }
        }
        return true;
    }

    private static void printOriginalArrangement() {
        int[] values = calculateValues(ORIGINAL);
        System.out.println();
        System.out.println("Original arrangement:");
        for (int i = 0; i < values.length; i++) {
            System.out.printf(Locale.US, "%d: %.3f  ending %03d%n",
                              i + 1, values[i] / 1000.0,
                              Math.floorMod(values[i], 1000));
        }
        System.out.println("Matches complete 37/73 ending pattern: "
                           + matches(values, GOD_PRIME_PAIR_ENDINGS));
        System.out.println("Four complementary pairs all sum to 2738.000:");
        System.out.println("  2 + 3, 4 + 5, 6 + 7, and 8 + 9");
    }

    private static void printTotalIdentities() {
        int total = Arrays.stream(ORIGINAL).sum();
        System.out.printf(Locale.US, "Total molecular weight: %.3f%n", total / 1000.0);
        System.out.println("2738 = 37 * 73 + 37 = 2 * 37 * 37");
    }

    private static void printFrequency(String label, long matches, int trials) {
        System.out.printf(Locale.US, "%s: %,d matches", label, matches);
        if (matches == 0) {
            System.out.printf(Locale.US, " (none observed in %,d trials)%n", trials);
        } else {
            double rate = matches / (double) trials;
            System.out.printf(Locale.US, "; rate = %.10g; about 1 in %,.2f%n",
                              rate, trials / (double) matches);
        }
    }

    private static int parseTrials(String text) {
        try {
            int trials = Integer.parseInt(text);
            if (trials < 1) {
                throw new IllegalArgumentException("trials must be at least 1");
            }
            return trials;
        } catch (NumberFormatException exception) {
            throw new IllegalArgumentException("invalid trial count: " + text, exception);
        }
    }

    private static long parseSeed(String text) {
        try {
            return Long.parseLong(text);
        } catch (NumberFormatException exception) {
            throw new IllegalArgumentException("invalid seed: " + text, exception);
        }
    }
}
