//import org.jgrapht.Graph;
import org.jgrapht.alg.interfaces.MatchingAlgorithm;
import org.jgrapht.alg.matching.MaximumWeightBipartiteMatching;
import org.jgrapht.generate.SimpleWeightedBipartiteGraphMatrixGenerator;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.SimpleWeightedGraph;

import java.math.BigDecimal;
import java.math.MathContext;
import java.text.NumberFormat;
import java.time.Instant;
import java.time.Duration;
import java.util.*;
import java.util.stream.IntStream;

public class Simulator {
    /*
    * Old instance fields from before object-oriented refactor
    *
    private static Integer numDistinctCells = 2_000_000;
    private static double stdDeviation = 200; //square root of numDistCells would approximate poisson dist
    private static int numWells;
    private static int numConcentrations = 1;
    private static double errorRate = 0.1;
    private static Integer[] concentrations = {500};
    private static int lowThreshold = 2; //min number of shared wells to attempt pairing
    private static int highThreshold = numWells - 3; //max number of shared wells to attempt pairing
    private static boolean use2DArrayForGraph = true; //Doing this is much faster for larger graphs
    private static boolean useJGraphTGraphMatrixGenerator = true; //fastest option
    */

    public static CellSample generateCellSample(Integer numDistinctCells, Integer cdr1Freq) {
        //In real T cells, CDR1s have about one third the diversity of CDR3s
        //previous sim was only CDR3s
        List<Integer> numbersCDR3 = new ArrayList<>();
        List<Integer> numbersCDR1 = new ArrayList<>();
        Integer numDistCDR3s = 2 * numDistinctCells + 1;
        IntStream.range(1, numDistCDR3s + 1).forEach(i -> numbersCDR3.add(i));
        IntStream.range(numDistCDR3s + 1, numDistCDR3s + 1 + (numDistCDR3s / cdr1Freq) + 1).forEach(i -> numbersCDR1.add(i));
        Collections.shuffle(numbersCDR3);
        Collections.shuffle(numbersCDR1);

        //Each cell represented by 4 values
        //two CDR3s, and two CDR1s. First two values are CDR3s, second two are CDR1s
        List<Integer[]> distinctCells = new ArrayList<>();
        for(int i = 0; i < numbersCDR3.size() - 1; i = i + 2){
            Integer tmpCDR3a = numbersCDR3.get(i);
            Integer tmpCDR3b = numbersCDR3.get(i+1);
            Integer tmpCDR1a = numbersCDR1.get(i % numbersCDR1.size());
            Integer tmpCDR1b = numbersCDR1.get((i+1) % numbersCDR1.size());
            Integer[] tmp = {tmpCDR3a, tmpCDR3b, tmpCDR1a, tmpCDR1b};
            distinctCells.add(tmp);
        }
        return new CellSample(distinctCells, cdr1Freq);
    }

    public static MatchingResult matchCDR3s(List<Integer[]> distinctCells,
                                            Plate samplePlate, Integer lowThreshold, Integer highThreshold){
        System.out.println("Cells: " + distinctCells.size());
        Instant start = Instant.now();

        int numWells = samplePlate.getSize();
        System.out.println("Making cell maps");
        //HashMap keyed to Alphas, values Betas
        Map<Integer, Integer> distCellsMapAlphaKey = new HashMap<>();
        for (Integer[] cell : distinctCells) {
            distCellsMapAlphaKey.put(cell[0], cell[1]);
        }
        //HashMap keyed to Betas, values Alphas
        Map<Integer, Integer> distCellsMapBetaKey = new HashMap<>();
        for (Integer[] cell : distinctCells) {
            distCellsMapBetaKey.put(cell[1], cell[0]);
        }
        System.out.println("Cell maps made");

        System.out.println("Making well maps");
        Map<Integer, Integer> allAlphas = samplePlate.assayWellsCDR3Alpha();
        Map<Integer, Integer> allBetas = samplePlate.assayWellsCDR3Beta();
        int alphaCount = allAlphas.size();
        System.out.println("all alphas count: " + alphaCount);
        int betaCount = allBetas.size();
        System.out.println("all betas count: " + betaCount);

        System.out.println("Well maps made");

        //Remove saturating-occupancy peptides because they have no signal value.
        //Remove below-minimum-overlap-threshold peptides because they can't possibly have an overlap with another
        //peptide that's above the threshold.
        System.out.println("Removing below-minimum-overlap-threshold and saturating-occupancy peptides");
        List<Integer> noiseAlphas = new ArrayList<>();
        List<Integer> noiseBetas = new ArrayList<>();
        for(Integer k: allAlphas.keySet()){
            if((allAlphas.get(k)>numWells - 1) || (allAlphas.get(k) < lowThreshold)){
               noiseAlphas.add(k);
            }
        }
        for(Integer k: allBetas.keySet()){
            if((allBetas.get(k)> numWells - 1 ) || (allBetas.get(k) < lowThreshold)){
                noiseBetas.add(k);
            }
        }
        for(Integer p: noiseAlphas){
            allAlphas.remove(p);
        }
        for(Integer p: noiseBetas){
            allBetas.remove(p);
        }
        System.out.println("Peptides removed");
        int pairableAlphaCount = allAlphas.size();
        System.out.println("Remaining alpha count: " + pairableAlphaCount);
        int pairableBetaCount = allBetas.size();
        System.out.println("Remaining beta count: " + pairableBetaCount);

        System.out.println("Making vertex maps");
        //Using Integers instead of Strings to label vertices so I can do clever stuff with indices if I need to
        // when I refactor to use a 2d array to make the graph
        //For the autogenerator, all vertices must have distinct numbers associated with them
        Integer vertexStartValue = 0;
        //keys are sequential integer vertices, values are alphas
        Map<Integer, Integer> plateVtoAMap = getVertexToPeptideMap(allAlphas, vertexStartValue);
        //New start value for vertex to beta map should be one more than final vertex value in alpha map
        vertexStartValue += plateVtoAMap.size();
        //keys are sequential integers vertices, values are betas
        Map<Integer, Integer> plateVtoBMap = getVertexToPeptideMap(allBetas, vertexStartValue);
        //keys are alphas, values are sequential integer vertices from previous map
        Map<Integer, Integer> plateAtoVMap = invertVertexMap(plateVtoAMap);
        //keys are betas, values are sequential integer vertices from previous map
        Map<Integer, Integer> plateBtoVMap = invertVertexMap(plateVtoBMap);
        System.out.println("Vertex maps made");

        System.out.println("Creating adjacency matrix");
        //Count how many wells each alpha appears in
        Map<Integer, Integer> alphaWellCounts = new HashMap<>();
        //count how many wells each beta appears in
        Map<Integer, Integer> betaWellCounts = new HashMap<>();
        //add edges, where weights are number of wells the peptides share in common.
        //If this is too slow, can make a 2d array and use the SimpleWeightedGraphMatrixGenerator class
        Map<Integer, Integer> wellNAlphas = null;
        Map<Integer, Integer> wellNBetas = null;
        SimpleWeightedGraph<Integer, DefaultWeightedEdge> graph =
                new SimpleWeightedGraph<>(DefaultWeightedEdge.class);
        double[][] weights = new double[plateVtoAMap.size()][plateVtoBMap.size()];
        for (int n = 0; n < numWells; n++) {
            wellNAlphas = samplePlate.assayWellsCDR3Alpha(n);
            for (Integer a : wellNAlphas.keySet()) {
                if(allAlphas.containsKey(a)){
                    alphaWellCounts.merge(a, 1, (oldValue, newValue) -> oldValue + newValue);
                }
            }
            wellNBetas = samplePlate.assayWellsCDR3Beta(n);
            for (Integer b : wellNBetas.keySet()) {
                if(allBetas.containsKey(b)){
                    betaWellCounts.merge(b, 1, (oldValue, newValue) -> oldValue + newValue);
                }
            }
            for (Integer i : wellNAlphas.keySet()) {
                if(allAlphas.containsKey(i)){
                    for (Integer j : wellNBetas.keySet()) {
                        if(allBetas.containsKey(j)){
                            weights[plateAtoVMap.get(i)][plateBtoVMap.get(j) - vertexStartValue] += 1.0;
                        }
                    }
                }
            }
        }
        System.out.println("matrix created");

        System.out.println("creating graph");

        SimpleWeightedBipartiteGraphMatrixGenerator graphGenerator = new SimpleWeightedBipartiteGraphMatrixGenerator();
        List<Integer> alphaVertices = new ArrayList<>();
        alphaVertices.addAll(plateVtoAMap.keySet()); //This will work because LinkedHashMap preserves order of entry
        graphGenerator.first(alphaVertices);
        List<Integer> betaVertices = new ArrayList<>();
        betaVertices.addAll(plateVtoBMap.keySet());
        graphGenerator.second(betaVertices); //This will work because LinkedHashMap preserves order of entry
        graphGenerator.weights(weights);
        graphGenerator.generateGraph(graph);
        System.out.println("Graph created");

        System.out.println("Eliminating edges with weights outside threshold values");
        for(DefaultWeightedEdge e: graph.edgeSet()){
            if ((graph.getEdgeWeight(e) > highThreshold) || (graph.getEdgeWeight(e) < lowThreshold)){
                graph.setEdgeWeight(e, 0.0);
            }
        }
        System.out.println("Over- and under-weight edges set to 0.0");


        System.out.println("Finding maximum weighted matching");
        MaximumWeightBipartiteMatching maxWeightMatching =
                new MaximumWeightBipartiteMatching(graph, plateVtoAMap.keySet(), plateVtoBMap.keySet());
        MatchingAlgorithm.Matching<String, DefaultWeightedEdge> graphMatching = maxWeightMatching.getMatching();
        System.out.println("Matching completed");
        Instant stop = Instant.now();

        //Header for CSV file
        List<String> header = new ArrayList<>();
        header.add("Alpha");
        header.add("Alpha well count");
        header.add("Beta");
        header.add("Beta well count");
        header.add("Overlap well count");
        header.add("Matched correctly?");
        header.add("P-value");


        //Results for csv file
        List<List<String>> allResults = new ArrayList<>();
        NumberFormat nf = NumberFormat.getInstance(Locale.US);
        MathContext mc = new MathContext(3);
        Iterator<DefaultWeightedEdge> weightIter = graphMatching.iterator();
        DefaultWeightedEdge e = null;
        int trueCount = 0;
        int falseCount = 0;
        boolean check = false;
        Map<Integer, Integer> matchMap = new HashMap<>();
        while(weightIter.hasNext()) {
            e = weightIter.next();
            Integer source = graph.getEdgeSource(e);
            Integer target = graph.getEdgeTarget(e);
            //The match map is all matches found, not just true matches!
            matchMap.put(plateVtoAMap.get(source), plateVtoBMap.get(target));
            check = plateVtoBMap.get(target).equals(distCellsMapAlphaKey.get(plateVtoAMap.get(source)));
            if(check) {
                trueCount++;
            }
            else {
                falseCount++;
            }
            List<String> result = new ArrayList<>();
            result.add(plateVtoAMap.get(source).toString());
            //alpha well count
            result.add(alphaWellCounts.get(plateVtoAMap.get(source)).toString());
            result.add(plateVtoBMap.get(target).toString());
            //beta well count
            result.add(betaWellCounts.get(plateVtoBMap.get(target)).toString());
            //overlap count
            result.add(Double.toString(graph.getEdgeWeight(e)));
            result.add(Boolean.toString(check));
            double pValue = Equations.pValue(numWells, alphaWellCounts.get(plateVtoAMap.get(source)),
                    betaWellCounts.get(plateVtoBMap.get(target)), graph.getEdgeWeight(e));
            BigDecimal pValueTrunc = new BigDecimal(pValue, mc);
            result.add(pValueTrunc.toString());
            allResults.add(result);
        }

        //Metadate comments for CSV file
        int min = alphaCount > betaCount ? betaCount : alphaCount;
        double attemptRate = (double) (trueCount + falseCount) / min;
        BigDecimal attemptRateTrunc = new BigDecimal(attemptRate, mc);
        double pairingErrorRate = (double) falseCount / (trueCount + falseCount);
        BigDecimal pairingErrorRateTrunc = new BigDecimal(pairingErrorRate, mc);

        List<String> comments = new ArrayList<>();
        comments.add("Total alphas found: " + alphaCount);
        comments.add("Total betas found: " + betaCount);
        comments.add("High overlap threshold: " + highThreshold);
        comments.add("Low overlap threshold: " + lowThreshold);
        comments.add("Pairing attempt rate: " + attemptRateTrunc);
        comments.add("Correct pairings: " + trueCount);
        comments.add("Incorrect pairings: " + falseCount);
        comments.add("Pairing error rate: " + pairingErrorRateTrunc);
        Duration time = Duration.between(start, stop);
        comments.add("Simulation time: " + nf.format(time.toSeconds()) + " seconds");
        System.out.println("Simulation time: " + nf.format(time.toSeconds()) + " seconds");

        return new MatchingResult(comments, header, allResults, matchMap, time);
    }

    public static MatchingResult[] matchCDR1s(List<Integer[]> distinctCells,
                                  Plate samplePlate, Integer lowThreshold,
                                            Integer highThreshold, Map<Integer, Integer> previousMatches, Duration previousTime){
        Instant start = Instant.now();

        int numWells = samplePlate.getSize();
        System.out.println("Making previous match maps");
        Map<Integer, Integer> CDR3AtoBMap = previousMatches;
        Map<Integer, Integer> CDR3BtoAMap = invertVertexMap(CDR3AtoBMap);
        System.out.println("Previous match maps made");

        System.out.println("Making cell maps");
        Map<Integer, Integer> alphaCDR3toCDR1Map = new HashMap<>();
        for (Integer[] cell : distinctCells) {
            alphaCDR3toCDR1Map.put(cell[0], cell[2]);
        }
        //HashMap keyed to Betas, values Alphas
        Map<Integer, Integer> betaCDR3toCDR1Map = new HashMap<>();
        for (Integer[] cell : distinctCells) {
            betaCDR3toCDR1Map.put(cell[1], cell[3]);
        }
        System.out.println("Cell maps made");

        System.out.println("Making well maps");
        Map<Integer, Integer> allCDR3s = samplePlate.assayWellsCDR3();
        Map<Integer, Integer> allCDR1s = samplePlate.assayWellsCDR1();
        int CDR3Count = allCDR3s.size();
        System.out.println("all CDR3s count: " + CDR3Count);
        int CDR1Count = allCDR1s.size();
        System.out.println("all CDR1s count: " + CDR1Count);
        System.out.println("Well maps made");

        //NEW FILTERING CODE TO TEST
        System.out.println("Removing unpaired CDR3s from well maps");
        List<Integer> unpairedCDR3s = new ArrayList<>();
        for(Integer i: allCDR3s.keySet()){
            if(!(CDR3AtoBMap.containsKey(i) || CDR3BtoAMap.containsKey(i))){
                unpairedCDR3s.add(i);
            }
        }
        for(Integer i: unpairedCDR3s){
            allCDR3s.remove(i);
        }
        System.out.println("Unpaired CDR3s removed.");
        System.out.println("Remaining CDR3 count: " + allCDR3s.size());

        System.out.println("Removing below-minimum-overlap-threshold and saturating-occupancy CDR1s");
        List<Integer> noiseCDR1s = new ArrayList<>();
        for(Integer i: allCDR1s.keySet()){
            if((allCDR1s.get(i) < lowThreshold) || (allCDR1s.get(i)) > numWells - 1){
                noiseCDR1s.add(i);
            }
        }
        for(Integer i: noiseCDR1s){
            allCDR1s.remove(i);
        }
        System.out.println("CDR1s removed.");
        System.out.println("Remaining CDR1 count: " + allCDR1s.size());
        //END NEW CODE

        System.out.println("Making vertex maps");
        //Using Integers instead of Strings to label vertices so I can do clever stuff with indices if I need to
        // when I refactor to use a 2d array to make the graph
        //For the autogenerator, all vertices must have distinct numbers associated with them
        Integer vertexStartValue = 0;
        //keys are sequential integer vertices, values are CDR3s
        Map<Integer, Integer> plateVtoCDR3Map = getVertexToPeptideMap(allCDR3s, vertexStartValue);
        //New start value for vertex to beta map should be one more than final vertex value in alpha map
        vertexStartValue += plateVtoCDR3Map.size();
        //keys are sequential integers vertices, values are CDR1s
        Map<Integer, Integer> plateVtoCDR1Map = getVertexToPeptideMap(allCDR1s, vertexStartValue);
        //keys are CDR3s, values are sequential integer vertices from previous map
        Map<Integer, Integer> plateCDR3toVMap = invertVertexMap(plateVtoCDR3Map);
        //keys are CDR1s, values are sequential integer vertices from previous map
        Map<Integer, Integer> plateCDR1toVMap = invertVertexMap(plateVtoCDR1Map);
        System.out.println("Vertex maps made");

        System.out.println("Creating Graph");
        //Count how many wells each CDR3 appears in
        Map<Integer, Integer> CDR3WellCounts = new HashMap<>();
        //count how many wells each CDR1 appears in
        Map<Integer, Integer> CDR1WellCounts = new HashMap<>();
        //add edges, where weights are number of wells the peptides share in common.
        //If this is too slow, can make a 2d array and use the SimpleWeightedGraphMatrixGenerator class
        Map<Integer, Integer> wellNCDR3s = null;
        Map<Integer, Integer> wellNCDR1s = null;
        SimpleWeightedGraph<Integer, DefaultWeightedEdge> graph =
                new SimpleWeightedGraph<>(DefaultWeightedEdge.class);
        double[][] weights = new double[plateVtoCDR3Map.size()][plateVtoCDR1Map.size()];
        for (int n = 0; n < numWells; n++) {
            wellNCDR3s = samplePlate.assayWellsCDR3(n);
            for (Integer a : wellNCDR3s.keySet()) {
                CDR3WellCounts.merge(a, 1, (oldValue, newValue) -> oldValue + newValue);
            }
            wellNCDR1s = samplePlate.assayWellsCDR1(n);
            for (Integer b : wellNCDR1s.keySet()) {
                CDR1WellCounts.merge(b, 1, (oldValue, newValue) -> oldValue + newValue);
            }
            for (Integer i : wellNCDR3s.keySet()) {
                if(allCDR3s.containsKey(i)){//only consider CDR3s we have matches for - rest filtered out
                    for (Integer j : wellNCDR1s.keySet()) {
                        if(allCDR1s.containsKey(j)){ //only those CDR1s we didn't filter out
                            weights[plateCDR3toVMap.get(i)][plateCDR1toVMap.get(j) - vertexStartValue] += 1.0;
                        }
                    }
                }
            }
        }
        SimpleWeightedBipartiteGraphMatrixGenerator graphGenerator = new SimpleWeightedBipartiteGraphMatrixGenerator();
        List<Integer> CDR3Vertices = new ArrayList<>();
        CDR3Vertices.addAll(plateVtoCDR3Map.keySet()); //This will work because LinkedHashMap preserves order of entry
        graphGenerator.first(CDR3Vertices);
        List<Integer> CDR1Vertices = new ArrayList<>();
        CDR1Vertices.addAll(plateVtoCDR1Map.keySet());
        graphGenerator.second(CDR1Vertices); //This will work because LinkedHashMap preserves order of entry
        graphGenerator.weights(weights);
        graphGenerator.generateGraph(graph);
        System.out.println("Graph created");
        System.out.println("Number of edges: " + graph.edgeSet().size());

        System.out.println("Removing edges outside of weight thresholds");
        for(DefaultWeightedEdge e: graph.edgeSet()){
            if ((graph.getEdgeWeight(e) > highThreshold) || (graph.getEdgeWeight(e) < lowThreshold)){
                graph.setEdgeWeight(e, 0.0);
            }
        }
        System.out.println("Over- and under-weight edges set to 0.0");

        System.out.println("Finding first maximum weighted matching");
        MaximumWeightBipartiteMatching firstMaxWeightMatching =
                new MaximumWeightBipartiteMatching(graph, plateVtoCDR3Map.keySet(), plateVtoCDR1Map.keySet());
        MatchingAlgorithm.Matching<String, DefaultWeightedEdge> graphMatching = firstMaxWeightMatching.getMatching();
        System.out.println("First maximum weighted matching found");


        //first processing run
        Map<Integer, Integer> firstMatchCDR3toCDR1Map = new HashMap<>();
        Iterator<DefaultWeightedEdge> weightIter = graphMatching.iterator();
        DefaultWeightedEdge e = null;
        while(weightIter.hasNext()){
            e = weightIter.next();
            if(graph.getEdgeWeight(e) < lowThreshold || graph.getEdgeWeight(e) > highThreshold) {
                continue;
            }
            Integer source = graph.getEdgeSource(e);
//            if(!(CDR3AtoBMap.containsKey(source) || CDR3BtoAMap.containsKey(source))){
//                continue;
//            }
            Integer target = graph.getEdgeTarget(e);
            firstMatchCDR3toCDR1Map.put(plateVtoCDR3Map.get(source), plateVtoCDR1Map.get(target));
        }

        System.out.println("First pass matches: " + firstMatchCDR3toCDR1Map.size());

        //zero out the edge weights in the matching
        weightIter = graphMatching.iterator();
        while(weightIter.hasNext()){
            graph.removeEdge(weightIter.next());
        }

        //Generate a new matching
        System.out.println("Finding second maximum weighted matching");
        MaximumWeightBipartiteMatching secondMaxWeightMatching =
                new MaximumWeightBipartiteMatching(graph, plateVtoCDR3Map.keySet(), plateVtoCDR1Map.keySet());
        graphMatching = secondMaxWeightMatching.getMatching();
        System.out.println("Second maximum weighted matching found");


        //second processing run
        Map<Integer, Integer> secondMatchCDR3toCDR1Map = new HashMap<>();
        weightIter = graphMatching.iterator();
        while(weightIter.hasNext()){
            e = weightIter.next();
            if(graph.getEdgeWeight(e) < lowThreshold || graph.getEdgeWeight(e) > highThreshold) {
                continue;
            }
            Integer source = graph.getEdgeSource(e);
//            if(!(CDR3AtoBMap.containsKey(source) || CDR3BtoAMap.containsKey(source))){
//                continue;
//            }
            Integer target = graph.getEdgeTarget(e);
            secondMatchCDR3toCDR1Map.put(plateVtoCDR3Map.get(source), plateVtoCDR1Map.get(target));
        }

        System.out.println("Second pass matches: " + secondMatchCDR3toCDR1Map.size());


        //get linked map for first matching attempt
        Map<Integer, Integer> firstMatchesMap = new LinkedHashMap<>();
        for(Integer alphaCDR3: CDR3AtoBMap.keySet()) {
            if (!(firstMatchCDR3toCDR1Map.containsKey(alphaCDR3))) {
                continue;
            }
            Integer betaCDR3 = CDR3AtoBMap.get(alphaCDR3);
            if (!(firstMatchCDR3toCDR1Map.containsKey(betaCDR3))) {
                continue;
            }
            firstMatchesMap.put(alphaCDR3, firstMatchCDR3toCDR1Map.get(alphaCDR3));
            firstMatchesMap.put(betaCDR3, firstMatchCDR3toCDR1Map.get(betaCDR3));
        }


        //Look for matches that simply swapped already-matched alpha and beta CDR3s
        Map<Integer, Integer> dualMatchesMap = new LinkedHashMap<>();
        for(Integer alphaCDR3: CDR3AtoBMap.keySet()) {
            if (!(firstMatchCDR3toCDR1Map.containsKey(alphaCDR3) && secondMatchCDR3toCDR1Map.containsKey(alphaCDR3))) {
                continue;
            }
            Integer betaCDR3 = CDR3AtoBMap.get(alphaCDR3);
            if (!(firstMatchCDR3toCDR1Map.containsKey(betaCDR3) && secondMatchCDR3toCDR1Map.containsKey(betaCDR3))) {
                continue;
            }
            if(firstMatchCDR3toCDR1Map.get(alphaCDR3).equals(secondMatchCDR3toCDR1Map.get(betaCDR3))){
                if(firstMatchCDR3toCDR1Map.get(betaCDR3).equals(secondMatchCDR3toCDR1Map.get(alphaCDR3))){
                    dualMatchesMap.put(alphaCDR3, firstMatchCDR3toCDR1Map.get(alphaCDR3));
                    dualMatchesMap.put(betaCDR3, firstMatchCDR3toCDR1Map.get(betaCDR3));
                }
            }
        }

        Instant stop = Instant.now();


        //results for first map
        System.out.println("Results for first pass");
        List<List<String>> allResults = new ArrayList<>();
        Integer trueCount = 0;
        Iterator iter = firstMatchesMap.keySet().iterator();

        while(iter.hasNext()){
            Boolean proven = false;
            List<String> tmp = new ArrayList<>();
            tmp.add(iter.next().toString());
            tmp.add(iter.next().toString());
            tmp.add(firstMatchesMap.get(Integer.valueOf(tmp.get(0))).toString());
            tmp.add(firstMatchesMap.get(Integer.valueOf(tmp.get(1))).toString());
            if(alphaCDR3toCDR1Map.get(Integer.valueOf(tmp.get(0))).equals(Integer.valueOf(tmp.get(2)))){
                if(betaCDR3toCDR1Map.get(Integer.valueOf(tmp.get(1))).equals(Integer.valueOf(tmp.get(3)))){
                    proven = true;
                }
            }
            else if(alphaCDR3toCDR1Map.get(Integer.valueOf(tmp.get(0))).equals(Integer.valueOf(tmp.get(3)))){
                if(betaCDR3toCDR1Map.get(Integer.valueOf(tmp.get(1))).equals(Integer.valueOf(tmp.get(2)))){
                    proven = true;
                }
            }
            tmp.add(proven.toString());
            allResults.add(tmp);
            if(proven){
                trueCount++;
            }
        }

        List<String> comments = new ArrayList<>();
        comments.add("Plate size: " + samplePlate.getSize() + " wells");
        comments.add("Previous pairs found: " + previousMatches.size());
        comments.add("CDR1 matches attempted: " + allResults.size());
        double attemptRate = (double) allResults.size() / previousMatches.size();
        comments.add("Matching attempt rate: " + attemptRate);
        comments.add("Number of correct matches: " + trueCount);
        double correctRate = (double) trueCount / allResults.size();
        comments.add("Correct matching rate: " + correctRate);
        NumberFormat nf = NumberFormat.getInstance(Locale.US);
        Duration time = Duration.between(start, stop);
        time.plus(previousTime);
        comments.add("Simulation time: " + nf.format(time.toSeconds()) + " seconds");
        for(String s: comments){
            System.out.println(s);
        }



        List<String> headers = new ArrayList<>();
        headers.add("CDR3 alpha");
        headers.add("CDR3 beta");
        headers.add("first matched CDR1");
        headers.add("second matched CDR1");
        headers.add("Correct match?");

        MatchingResult firstTest = new MatchingResult(comments, headers, allResults, dualMatchesMap, time);

        //results for dual map
        System.out.println("Results for second pass");
        allResults = new ArrayList<>();
        trueCount = 0;
        iter = dualMatchesMap.keySet().iterator();
        while(iter.hasNext()){
            Boolean proven = false;
            List<String> tmp = new ArrayList<>();
            tmp.add(iter.next().toString());
            tmp.add(iter.next().toString());
            tmp.add(dualMatchesMap.get(Integer.valueOf(tmp.get(0))).toString());
            tmp.add(dualMatchesMap.get(Integer.valueOf(tmp.get(1))).toString());
            if(alphaCDR3toCDR1Map.get(Integer.valueOf(tmp.get(0))).equals(Integer.valueOf(tmp.get(2)))){
                if(betaCDR3toCDR1Map.get(Integer.valueOf(tmp.get(1))).equals(Integer.valueOf(tmp.get(3)))){
                    proven = true;
                }
            }
            else if(alphaCDR3toCDR1Map.get(Integer.valueOf(tmp.get(0))).equals(Integer.valueOf(tmp.get(3)))){
                if(betaCDR3toCDR1Map.get(Integer.valueOf(tmp.get(1))).equals(Integer.valueOf(tmp.get(2)))){
                    proven = true;
                }
            }
            tmp.add(proven.toString());
            allResults.add(tmp);
            if(proven){
                trueCount++;
            }
        }

        comments = new ArrayList<>();
        comments.add("Plate size: " + samplePlate.getSize() + " wells");
        comments.add("Previous pairs found: " + previousMatches.size());
        comments.add("High overlap threshold: " + highThreshold);
        comments.add("Low overlap threshold: " + lowThreshold);
        comments.add("CDR1 matches attempted: " + allResults.size());
        attemptRate = (double) allResults.size() / previousMatches.size();
        comments.add("Matching attempt rate: " + attemptRate);
        comments.add("Number of correct matches: " + trueCount);
        correctRate = (double) trueCount / allResults.size();
        comments.add("Correct matching rate: " + correctRate);
        comments.add("Simulation time: " + nf.format(time.toSeconds()) + " seconds");

        for(String s: comments){
            System.out.println(s);
        }

        System.out.println("Simulation time: " + nf.format(time.toSeconds()) + " seconds");

        MatchingResult dualTest = new MatchingResult(comments, headers, allResults, dualMatchesMap, time);


        MatchingResult[] output = {firstTest, dualTest};
        return output;
    }


    /*
    ** Old main method before object-oriented refactor
    *
    public static void Simulate() {
        Instant start = Instant.now();
        //Four things to try to improve this
        //1. Run it on hardware with more memory
        //2. implement p-values and just check exhaustively for strictly-bounded weights
        //     If I do this, can start with highest weights and zero out columns when I find a p-value I like maybe
        //3. hand-implement the maximum weighted matching algorithm to use arrays and be efficient
        //  3.5 Could also do a version where I make the graph one edge at a time, but from a 2D array so I have the
        //      weights already
        //          Tried it. It was way better than doing the graph directly, but slightly slower than the built-in
        //4. Implement a discrete distribution function (along the lines of the alias method)
        //
        //With array method, could also try simply zeroing high-well-count peptides before graph creation

        //stdDeviation = Math.sqrt(numDistinctCells.doubleValue());
        //concentrations = new int[numConcentrations];

        //OUTPUT
        System.out.println("Generating cells");

        List<Integer> numbers = new ArrayList<>();
        IntStream.range(1, (2 * numDistinctCells) + 1).forEach(i -> numbers.add(i));
        Collections.shuffle(numbers);

        //Each cell represented by two numbers from the random permutation
        //These represent unique alpha and beta peptides
        List<Integer[]> distinctCells = new ArrayList<>();
        for(int i = 0; i < numbers.size() - 1; i = i + 2) {
            Integer tmp1 = numbers.get(i);
            Integer tmp2 = numbers.get(i+1);
            Integer[] tmp = {tmp1, tmp2};
            distinctCells.add(tmp);
        }

        //OUTPUT
        System.out.println("Cells generated");
        System.out.println("Filling wells");

        //HashMap keyed to Alphas, values Betas
        Map<Integer, Integer> distCellsMapAlphaKey = new HashMap<>();
        for (Integer[] cell : distinctCells) {
            distCellsMapAlphaKey.put(cell[0], cell[1]);
        }

        //HashMap keyed to Betas, values Alphas
        Map<Integer, Integer> distCellsMapBetaKey = new HashMap<>();
        for (Integer[] cell : distinctCells) {
            distCellsMapBetaKey.put(cell[1], cell[0]);
        }

        Plate samplePlate = new Plate(numWells, errorRate, concentrations, stdDeviation);
        samplePlate.fillWells(distinctCells);

        //OUTPUT
        System.out.println("Wells filled");
        System.out.println("Making maps");

        //All alphas on plate, with
        Map<Integer, Integer> allAlphas = samplePlate.assayWellsAlpha();
        Map<Integer, Integer> allBetas = samplePlate.assayWellsBeta();
        int alphaCount = allAlphas.size();
        int betaCount = allBetas.size();

        //Using Integers instead of Strings to label vertices so I can do clever stuff with indices if I need to
        // when I refactor to use a 2d array to make the graph
        //For the autogenerator, all vertices must have distinct numbers associated with them
        Integer vertexStartValue = 0;
        //keys are sequential integer vertices, values are alphas
        Map<Integer, Integer> plateVtoAMap = getVertexToPeptideMap(allAlphas, vertexStartValue);
        //New start value for vertex to beta map should be one more than final vertex value in alpha map
        vertexStartValue += plateVtoAMap.size();
        //keys are sequential integers vertices, values are betas
        Map<Integer, Integer> plateVtoBMap = getVertexToPeptideMap(allBetas, vertexStartValue);
        //keys are alphas, values are sequential integer vertices from previous map
        Map<Integer, Integer> plateAtoVMap = invertVertexMap(plateVtoAMap);
        //keys are betas, values are sequential integer vertices from previous map
        Map<Integer, Integer> plateBtoVMap = invertVertexMap(plateVtoBMap);

        //OUTPUT
        System.out.println("Maps made");
        System.out.println("Creating Graph");

        //Count how many wells each alpha appears in
        Map<Integer, Integer> alphaWellCounts = new HashMap<>();
        //count how many wells each beta appears in
        Map<Integer, Integer> betaWellCounts = new HashMap<>();
        //add edges, where weights are number of wells the peptides share in common.
        //If this is too slow, can make a 2d array and use the SimpleWeightedGraphMatrixGenerator class
        Map<Integer, Integer> wellNAlphas = null;
        Map<Integer, Integer> wellNBetas = null;
        SimpleWeightedGraph<Integer, DefaultWeightedEdge> graph =
                new SimpleWeightedGraph<Integer, DefaultWeightedEdge>(DefaultWeightedEdge.class);

        //Flag in global variables determines if edges are added to graph one at a time directly, or if a graph
        //is made all at once from a 2D array using the SimpleWeightedBipartiteGraphMatrixGenerator class
        if(use2DArrayForGraph) {
            double[][] weights = new double[plateVtoAMap.size()][plateVtoBMap.size()];
            for (int n = 0; n < numWells; n++) {
                wellNAlphas = samplePlate.assayWellsAlpha(n);
                for(Integer a: wellNAlphas.keySet()){
                    alphaWellCounts.merge(a, 1, (oldValue, newValue) -> oldValue + newValue);
                }
                wellNBetas = samplePlate.assayWellsBeta(n);
                for(Integer b: wellNBetas.keySet()){
                    betaWellCounts.merge(b, 1, (oldValue, newValue) -> oldValue + newValue);
                }

                for(Integer i: wellNAlphas.keySet()) {
                    for(Integer j: wellNBetas.keySet()){
                        weights[plateAtoVMap.get(i)][plateBtoVMap.get(j) - vertexStartValue] += 1.0;
                    }
                }
            }
            if(useJGraphTGraphMatrixGenerator) {
                SimpleWeightedBipartiteGraphMatrixGenerator graphGenerator = new SimpleWeightedBipartiteGraphMatrixGenerator();
                List<Integer> alphaVertices = new ArrayList<>();
                alphaVertices.addAll(plateVtoAMap.keySet()); //This will work because LinkedHashMap preserves order of entry
                graphGenerator.first(alphaVertices);
                List<Integer> betaVertices = new ArrayList<>();
                betaVertices.addAll(plateVtoBMap.keySet());
                graphGenerator.second(betaVertices); //This will work because LinkedHashMap preserves order of entry
                graphGenerator.weights(weights);
                graphGenerator.generateGraph(graph);
            } else {
                for(int i = 0; i < alphaCount; i++) {
                    for (int j = vertexStartValue; j < betaCount + vertexStartValue; j++){
                        graph.addVertex(i);
                        graph.addVertex(j);
                        addEdgeWithWeight(graph, i, (j), weights[i][j - vertexStartValue]);
                    }
                }
            }
        }
        else {
            //construct graph
            //add vertices
            for(Integer v: plateVtoAMap.keySet()) {
                graph.addVertex(v);
            }
            for(Integer v: plateVtoBMap.keySet()) {
                graph.addVertex(v);
            }

            for (int n = 0; n < numWells; n++) {
                wellNAlphas = samplePlate.assayWellsAlpha(n);
                for (Integer a : wellNAlphas.keySet()) {
                    alphaWellCounts.merge(a, 1, (oldValue, newValue) -> oldValue + newValue);
                }
                wellNBetas = samplePlate.assayWellsBeta(n);
                for (Integer b : wellNBetas.keySet()) {
                    betaWellCounts.merge(b, 1, (oldValue, newValue) -> oldValue + newValue);
                }

                for (Integer i : wellNAlphas.keySet()) {
                    for (Integer j : wellNBetas.keySet()) {
                        addEdgeOrWeight(graph, plateAtoVMap.get(i), plateBtoVMap.get(j));
                    }
                }
            }
        }

        //OUTPUT
        System.out.println("Graph created");
        System.out.println("Finding maximum weighted matching");

        MaximumWeightBipartiteMatching maxWeightMatching =
                new MaximumWeightBipartiteMatching(graph, plateVtoAMap.keySet(), plateVtoBMap.keySet());

        MatchingAlgorithm.Matching<String, DefaultWeightedEdge> graphMatching = maxWeightMatching.getMatching();

        Instant stop = Instant.now();

        //OUTPUT
        System.out.println("Matching completed");


        Iterator<DefaultWeightedEdge> weightIter = graphMatching.iterator();
        DefaultWeightedEdge e = null;
        int trueCount = 0;
        int falseCount = 0;
        boolean check = false;
        while(weightIter.hasNext()) {
            e = weightIter.next();
            if(graph.getEdgeWeight(e) < lowThreshold || graph.getEdgeWeight(e) > highThreshold) {
                continue;
            }
            Integer source = graph.getEdgeSource(e);
            Integer target = graph.getEdgeTarget(e);
            //Need map of each peptide and number of wells it appears in.
            check = printData(graph.getEdgeWeight(e), source, alphaWellCounts.get(plateVtoAMap.get(source)), target, betaWellCounts.get(plateVtoBMap.get(target)), distCellsMapAlphaKey, plateVtoAMap, plateVtoBMap);
            if(check) {
                trueCount++;
            }
            else {
                falseCount++;
            }
        }

        //RESULTS SUMMARY
        NumberFormat nf = NumberFormat.getInstance(Locale.US);
        System.out.println("");
        System.out.println("Distinct cell population: " + nf.format(numDistinctCells));
        System.out.println("1 standard deviation of distribution: " + stdDeviation + " most common cells");
        System.out.print("Sample plate: " + numWells +" wells, ");
        System.out.print("divided into " + concentrations.length + " sections of ");
        System.out.println(Arrays.toString(concentrations) + " cells per well.");
        System.out.print("Attempted to match alpha/beta overlaps of at least " + lowThreshold + " wells ");
        System.out.println("and no more than " + highThreshold + " wells.");
        System.out.println(nf.format(alphaCount) + " alpha peptides found");
        System.out.println(nf.format(betaCount) + " beta peptides found");
        MathContext mc = new MathContext(3);
        int min = alphaCount > betaCount ? betaCount : alphaCount;
        double attemptRate = (double) (trueCount + falseCount) / min;
        BigDecimal attR = new BigDecimal(attemptRate, mc);
        System.out.println("Pairing attempt rate: " + attR);
        System.out.println("Correct pairings: " + nf.format(trueCount));
        System.out.println("Incorrect pairings: " + nf.format(falseCount));
        double experimentalError = (double) falseCount / (trueCount + falseCount);
        BigDecimal expErr = new BigDecimal(experimentalError, mc);
        System.out.println("Pairing error rate: " + expErr);
        Duration time = Duration.between(start, stop);
        System.out.println("Simulation time: " + nf.format(time.toSeconds()) + " seconds");
        if(use2DArrayForGraph) {
            System.out.print("Graph generated from 2D array of weights");
            if(useJGraphTGraphMatrixGenerator){
                System.out.print(" using JGraphT's SimpleWeightedBipartiteGraphMatrixGenerator");
            }
            else {
                System.out.print(" directly.");
            }
            System.out.println("");
        }
        else {
            System.out.println("Graph generated directly from individual well maps");
        }
        System.out.println("");


    }
     */

    private static Map<Integer, Integer> getVertexToPeptideMap(Map<Integer, Integer> peptides, Integer startValue) {
        Map<Integer, Integer> map = new LinkedHashMap<>(); //LinkedHashMap to preserve order of entry
        Integer index = startValue;
        for (Integer k: peptides.keySet()) {
            map.put(index, k);
            index++;
        }
        return map;
    }

    private static Map<Integer, Integer> invertVertexMap(Map<Integer, Integer> map) {
        Map<Integer, Integer> inverse = new HashMap<>();
        for(Integer k: map.keySet()) {
            inverse.put(map.get(k), k);
        }
        return inverse;
    }

    /*
    * Old methods for graph generation algorithms less efficient than built in matrix generator
    *
    private static void addEdgeWithWeight(Graph g, Integer v1, Integer v2, double weight) {
        g.addEdge(v1, v2);
        g.setEdgeWeight(v1, v2, weight);
    }

    private static void addEdgeOrWeight(Graph g, Integer v1, Integer v2) {
        if (g.containsEdge(v1, v2)) {
            g.setEdgeWeight(v1, v2, g.getEdgeWeight(g.getEdge(v1, v2)) + 1.0);
        }
        else {
            g.addEdge(v1, v2);
        }
    }
    */


    /*
    * Old output method before object-oriented refactor
    *
    private static boolean printData (Double edgeWeight, Integer source, Integer sourceCount, Integer target,
                                      Integer targetCount, Map<Integer,Integer> AtoBMap, Map<Integer,Integer> VtoAMap,
                                      Map<Integer, Integer> VtoBMap) {
        System.out.println("Alpha peptide " + source + " present in " + sourceCount + " wells");
        System.out.println("Beta peptide " + target + " present in " + targetCount + " wells");
        System.out.println("Both alpha and beta present in " + edgeWeight + " wells");
        if(VtoBMap.get(target).equals(AtoBMap.get(VtoAMap.get(source)))){
            System.out.println("True");
            System.out.println("p-value: " + Equations.pValue(numWells, sourceCount, targetCount, edgeWeight));
            return true;
        }
        else {
            System.out.println("False");
            System.out.println("p-value: " + Equations.pValue(numWells, sourceCount, targetCount, edgeWeight));
            System.out.println("The real pair is " + source + " and " + AtoBMap.get(VtoAMap.get(source)));
            return false;
        }
    }*/
}
