public class GBSX {
    
    public static final boolean DEBUG = false;
    public final static String VERSION = "GBSX v1.3";
    private final static String LICENCE = "GPLv3";
    
    /**
     * @param args 
     */
    public static void main (String[] args){
        if (GBSX.DEBUG){
            String input;
            input = "--DEBUG";
            args = input.split(" ");
        }
        try{
            if (GBSX.DEBUG){
                System.out.println("DEBUG MODUS");
            }
            if (args.length == 0) {
                System.out.println("\n\nNo arguments given.\n\n");
                throw new StopExcecutionException();
            }
            if (args[0].equals("version") || args[0].equals("-version") || args[0].equals("-v")){
                System.out.println(GBSX.VERSION);
                System.out.println("This is " + GBSX.VERSION + ". A toolkit for experimental design and demultiplexing genotyping by \n" +
"sequencing experiments.");
                throw new StopExcecutionException();
            }
            if (args[0].equals("-w")){
                System.out.println("GBSX is distributed in the hope that it will be useful,");
                System.out.println("but WITHOUT ANY WARRANTY; without even the implied warranty of");
                System.out.println("MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the");
                System.out.println("GNU General Public License for more details.");
                throw new StopExcecutionException();
            }
            if (args[0].equals("-c")){
                System.out.println("GBSX is free software: you can redistribute it and/or modify");
                System.out.println("it under the terms of the GNU General Public License as published by");
                System.out.println("the Free Software Foundation, either version 3 of the License, or");
                System.out.println("(at your option) any later version.");
                throw new StopExcecutionException();
            }
            if (args[0].equals("help") || args[0].equals("-help") || args[0].equals("-h")){
                System.out.println("GBSX v1.0  Copyright (C) 2014 Genomics Core\n" +
                    "This program comes with ABSOLUTELY NO WARRANTY; for details type `perl GBSX_digest_v1.0.pl -w'.\n" +
                    "This is free software, and you are welcome to redistribute it under certain conditions; type `perl GBSX_digest_v1.0.pl -c' for details.\n" +
                    "\n" +
                    "####################################################################################");
                System.out.println("");
                System.out.println("GBSX is a collection of the following programs:");
                System.out.println("\t Demultiplexer  \t\t\t --Demultiplexer \n\t\t the inline barcode, gbs and rad demultiplexer \n \t\t " + GBSdemultiplex.VERSION);
                System.out.println("\t BarcodeDiscovery \t\t --BarcodeDiscovery \n\t\t a quick search for possible barcodes in a fastq file \n\t\t " + BarcodeDiscovery.VERSION);
                System.out.println("\t DNAComplement \t\t --DNAComplement \n\t\t a tool to make the complement of a given DNA sequence \n\t\t " + DNAComplement.VERSION);
                System.out.println("\t BarcodeGenerator \t\t --BarcodeGenerator \n\t\t a tool to generate random self-correcting barcodes (the barcodes has a hammings distance of at least 3)\n\t\t " + BarcodeGenerator.VERSION);
                System.out.println("\t GBSsimulator \t\t --GBSsimulator \n\t\t a tool to simulate GBS data (only purpose is testing)\n\t\t " + BarcodeGenerator.VERSION);
                System.out.println();
                System.out.println("For tool specific help use --tool -help");
                System.out.println();
                System.out.println(GBSX.VERSION);
                System.out.println("Developed by the KU Leuven 2014");
                System.out.println("Licenced under " + GBSX.LICENCE);
                System.out.println("For licence information use -licence");
                System.out.println();
                throw new StopExcecutionException();
            }
            if (args[0].equals("licence") || args[0].equals("-licence") || args[0].equals("-l")){
                System.out.println("This file is part of GBSX.\n" +
                    "\n" +
                    "GBSX is free software: you can redistribute it and/or modify\n" +
                    "it under the terms of the GNU General Public License as published by\n" +
                    "the Free Software Foundation, either version 3 of the License, or\n" +
                    "(at your option) any later version.\n" +
                    "\n" +
                    "GBSX is distributed in the hope that it will be useful,\n" +
                    "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" +
                    "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n" +
                    "GNU General Public License for more details.\n" +
                    "\n" +
                    "You should have received a copy of the GNU General Public License\n" +
                    "along with GBSX.  If not, see <http://www.gnu.org/licenses/>.");
                throw new StopExcecutionException();
            }
            String[] newArgs = new String[args.length -1];
            for (int i = 1; i < args.length; i++){
                newArgs[i-1] = args[i];
            }
            if (args[0].toLowerCase().equals("--Demultiplexer".toLowerCase())){
                GBSdemultiplex.main(newArgs);
            }else if (args[0].toLowerCase().equals("--BarcodeDiscovery".toLowerCase())){
                BarcodeDiscovery.main(newArgs);
            }else if (args[0].toLowerCase().equals("--DNAComplement".toLowerCase())){
                DNAComplement.main(newArgs);
            }else if (args[0].toLowerCase().equals("--BarcodeGenerator".toLowerCase())){
                BarcodeGenerator.main(newArgs);
            }else if (args[0].toLowerCase().equals("--GBSsimulator".toLowerCase())){
                GBSsimulate.main(newArgs);
            }else if (args[0].toLowerCase().equals("--DEBUG".toLowerCase())){
                System.out.println("DEBUG is on:");
                System.out.println("Demultiplexer: \t" + GBSdemultiplex.DEBUG);
                System.out.println("BarcodeDiscovery: \t" + BarcodeDiscovery.DEBUG);
                System.out.println("DNAComplement: \t" + DNAComplement.DEBUG);
                System.out.println("BarcodeGenerator: \t" + BarcodeGenerator.DEBUG);
                System.out.println("GBSsimulator: \t" + GBSsimulate.DEBUG);
            }else{
                System.out.println("No valid tool found.");
                System.out.println("Please use -help.");
            }
        } catch (StopExcecutionException ex){
            //End excecution as help of version is given
            if (GBSX.DEBUG){
                ex.printStackTrace();
            }
        } catch (IllegalArgumentException ex){
            //No argument was given
            System.out.println("" + ex.getMessage());
            if (GBSX.DEBUG){
                ex.printStackTrace();
            }
        }
        
        
    }
} public class BarcodeCorrectingAlgorithm {
    
    
    /**
     * 
     * @param barcode String | the barcode to check
     * @return true if the hamming code is correct, false otherwise
     */
    public boolean isCorrectBarcode(String barcode){
        char[] charArray = barcode.toCharArray();
        char d1 = charArray[2];
        char d2 = charArray[4];
        char d3 = charArray[5];
        char d4 = charArray[6];
        char d5 = charArray[8];
        char d6 = charArray[9];
        char d7 = charArray[10];
        char d8 = charArray[11];
        char d9 = charArray[12];
        char d10 = charArray[13];
        char d11 = charArray[14];
        char p1 = charArray[0];
        char p2 = charArray[1];
        char p3 = charArray[3];
        char p4 = charArray[7];
        HashMap<Character, Integer> basesIntMap = new HashMap();
        basesIntMap.put('A', 0);
        basesIntMap.put('C', 1);
        basesIntMap.put('G', 2);
        basesIntMap.put('T', 3);
        int predictedP1 = (4 - (basesIntMap.get(d1) + basesIntMap.get(d2) + basesIntMap.get(d4) + basesIntMap.get(d5) 
                + basesIntMap.get(d7) + basesIntMap.get(d9) + basesIntMap.get(d11)) % 4) % 4;
        int predictedP2 = (4 - (basesIntMap.get(d1) + basesIntMap.get(d3) + basesIntMap.get(d4) + basesIntMap.get(d6) 
                + basesIntMap.get(d7) + basesIntMap.get(d10) + basesIntMap.get(d11)) % 4) % 4;
        int predictedP3 = (4 - (basesIntMap.get(d2) + basesIntMap.get(d3) + basesIntMap.get(d4) + basesIntMap.get(d8) 
                + basesIntMap.get(d9) + basesIntMap.get(d10) + basesIntMap.get(d11)) % 4) % 4;
        int predictedP4 = (4 - (basesIntMap.get(d5) + basesIntMap.get(d6) + basesIntMap.get(d7) + basesIntMap.get(d8) 
                + basesIntMap.get(d9) + basesIntMap.get(d10) + basesIntMap.get(d11)) % 4) % 4;
        char base1 = 'N';
        char base2 = 'N';
        char base3 = 'N';
        char base4 = 'N';
        for (char base : basesIntMap.keySet()){
            if (basesIntMap.get(base) == predictedP1) base1 = base;
            if (basesIntMap.get(base) == predictedP2) base2 = base;
            if (basesIntMap.get(base) == predictedP3) base3 = base;
            if (basesIntMap.get(base) == predictedP4) base4 = base;
        }
        if (base1 == p1 && base2 == p2 && base3 == p3 && base4 == p4){
            return true;
        }
        return false;
    }
    
    /**
     * tries to correct the barcode
     * <br> if the barcode is correct, the barcode is returned
     * <br> if an error is found, this is tried to be fixed
     * <br> barcodes with 1 error are fixed correctly
     * <br> barcodes with more then 1 errors can't be fixed (a wrong fix is returned)
     * @param barcode String
     * @return String | the corrected barcode
     */
    public String correctBarcode(String barcode){
        char[] charArray = barcode.toCharArray();
        char d1 = charArray[2];
        char d2 = charArray[4];
        char d3 = charArray[5];
        char d4 = charArray[6];
        char d5 = charArray[8];
        char d6 = charArray[9];
        char d7 = charArray[10];
        char d8 = charArray[11];
        char d9 = charArray[12];
        char d10 = charArray[13];
        char d11 = charArray[14];
        char p1 = charArray[0];
        char p2 = charArray[1];
        char p3 = charArray[3];
        char p4 = charArray[7];
        HashMap<Character, Integer> basesIntMap = new HashMap();
        basesIntMap.put('A', 0);
        basesIntMap.put('C', 1);
        basesIntMap.put('G', 2);
        basesIntMap.put('T', 3);
        int predictedP1 = (4 - (basesIntMap.get(d1) + basesIntMap.get(d2) + basesIntMap.get(d4) + basesIntMap.get(d5) 
                + basesIntMap.get(d7) + basesIntMap.get(d9) + basesIntMap.get(d11)) % 4) % 4;
        int predictedP2 = (4 - (basesIntMap.get(d1) + basesIntMap.get(d3) + basesIntMap.get(d4) + basesIntMap.get(d6) 
                + basesIntMap.get(d7) + basesIntMap.get(d10) + basesIntMap.get(d11)) % 4) % 4;
        int predictedP3 = (4 - (basesIntMap.get(d2) + basesIntMap.get(d3) + basesIntMap.get(d4) + basesIntMap.get(d8) 
                + basesIntMap.get(d9) + basesIntMap.get(d10) + basesIntMap.get(d11)) % 4) % 4;
        int predictedP4 = (4 - (basesIntMap.get(d5) + basesIntMap.get(d6) + basesIntMap.get(d7) + basesIntMap.get(d8) 
                + basesIntMap.get(d9) + basesIntMap.get(d10) + basesIntMap.get(d11)) % 4) % 4;
        char base1 = 'N';
        char base2 = 'N';
        char base3 = 'N';
        char base4 = 'N';
        for (char base : basesIntMap.keySet()){
            if (basesIntMap.get(base) == predictedP1) base1 = base;
            if (basesIntMap.get(base) == predictedP2) base2 = base;
            if (basesIntMap.get(base) == predictedP3) base3 = base;
            if (basesIntMap.get(base) == predictedP4) base4 = base;
        }
        if (base1 == p1 && base2 == p2 && base3 == p3 && base4 == p4){
            return barcode;
        }else{
            //calculates the position of the error
            int place = 0;
            if (predictedP4 != 0){
                place += 8;
            }
            if (predictedP3 != 0){
                place += 4;
            }
            if (predictedP2 != 0){
                place += 2;
            }
            if (predictedP1 != 0){
                place += 1;
            }
            //calculates the right value at that position
            int errorType = Math.max(Math.max(predictedP1, predictedP2), Math.max(predictedP3, predictedP4));
            int correcting = (basesIntMap.get(charArray[place - 1]) - errorType) % 4;
            if (correcting < 0){
                correcting = 4 + correcting;
            }
            char correctBase = 'N';
            for (char base : basesIntMap.keySet()){
                if (basesIntMap.get(base) == correcting) correctBase = base;
            }
            charArray[place - 1] = correctBase;
            String newBarcode = "";
            for (int i = 0; i < charArray.length; i++){
                newBarcode += charArray[i];
            }
            barcode = newBarcode;
        }
        return barcode;
    }
} public class GBSdemultiplex {
    
    public static final boolean DEBUG = false;
    public final static String VERSION = "GBSX demultiplex v3";
    public final static String LICENCE = "GPLv3";
    
    /*
     * 
     * 
     */
    public static void main (String[] args){
//        if (GBSdemultiplex.DEBUG){
//            String input = "-help";
//            args = input.split(" ");
//        }
        GBSdemultiplex gbsdemultiplex = new GBSdemultiplex();
        gbsdemultiplex.runDemultiplex(args);
    }
            
    
    /**
     * The demultiplex
     * @param args 
     */
    public void runDemultiplex(String[] args){
        try{
            FastqDemultiplex demultiplex = new FastqDemultiplex(args);
            System.out.println("Start the demultiplexing.");
            demultiplex.processFastqFiles();
            System.out.println("Demultiplexing ended.");
        } catch (StopExcecutionException ex){
            //End excecution as help of version is given
            if (GBSdemultiplex.DEBUG){
                ex.printStackTrace();
            }
        } catch (Exception e){
            if (GBSdemultiplex.DEBUG){
                e.printStackTrace();
            }
            try {
                LoggerFile loggerFile = new LoggerFile(System.getProperty("user.dir"));
                loggerFile.addToLog("Arguments where: " + args.toString());
                loggerFile.addToLog("\n \n \n \n \n");
                loggerFile.addToLog(e.getMessage());
                loggerFile.closeLog();
            } catch (ErrorInLogException ex) {
                ex.printStackTrace();
                Logger.getLogger(GBSdemultiplex.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IOException ex) {
                ex.printStackTrace();
                Logger.getLogger(GBSdemultiplex.class.getName()).log(Level.SEVERE, null, ex);
            } finally{
                System.out.println("GBS demultiplexing ended with Major Errors");
            }
        }
    }
    
    
    
    
    
    
    
    
} public class FindingDistanceAlgorithm {
    
    
    private final FindingsAlgorithms algorithm;
    
    public FindingDistanceAlgorithm(FindingsAlgorithms findingsAlgorithm){
        this.algorithm = findingsAlgorithm;
    }
    
    /**
     * 
     * @return FindingsAlgorithms | the used algorithm
     */
    public FindingsAlgorithms getFindingsAlgorithm(){
        return this.algorithm;
    }
    
    /**
     * returns false if the length of both sequences is different
     * <br> returns false if the distance is higher than the maxMismatch
     * <br> (not the whole distance is calculated, except when true is returned, or the last char is also a mismatch)
     * @param sequence1 | String the sequence to search in
     * @param sequence2 | String the original (like barcode, enzyme site)
     * @param maxMismatch | int the max amount of mismatches, where 0 is equals to
     * @return true if the distance of the search algorithm is lower or equals the maxMismatch, false otherwise
     */
    public boolean isEquivalent(String sequence1, String sequence2, int maxMismatch){
        if (this.algorithm == FindingsAlgorithms.HAMMINGS_DISTANCE){
            return HammingsDistance.isEquivalent(sequence1, sequence2, maxMismatch);
        }
        if (this.algorithm == FindingsAlgorithms.INDEL_MISMATCH_DISTANCE){
            return IndelMismatchDistance.isEquivalent(sequence1, sequence2, maxMismatch);
        }
        if (this.algorithm == FindingsAlgorithms.KNUTH_MORRIS_PRATT_DISTANCE){
            return KnuthMorrisPrattDistance.isEquivalent(sequence1, sequence2, maxMismatch);
        }
        if (this.algorithm == FindingsAlgorithms.MISMATCH_INDEL_DISTANCE){
            return MismatchIndelDistance.isEquivalent(sequence1, sequence2, maxMismatch);
        }
        return false;
    }
    
    /**
     * returns -1 if the length of both sequences is different
     * <br> returns -1 if the distance is higher than the maxMismatch
     * <br> the distance is calculated, but when the current distance is higher then the maxMismatch, -1 is returned
     * <br> (not the whole distance is calculated, except when a positive int is returned, or the last char is also a mismatch)
     * @param sequence1 | String the sequence to search in
     * @param sequence2 | String the original (like barcode, enzyme site)
     * @param maxMismatch | int the max amount of mismatches, where 0 is equals to
     * @return int[distance, length] | distance: the distance if the distance is lower or equals the maxMismatch, -1 otherwise, length: length of sequence2 (with indels)
     */
    public int[] calculateEquivalentDistance(String sequence1, String sequence2, int maxMismatch){
        if (this.algorithm == FindingsAlgorithms.HAMMINGS_DISTANCE){
            return new int[] {HammingsDistance.calculateEquivalentDistance(sequence1.substring(0, sequence2.length()), sequence2, maxMismatch), sequence2.length()};
        }
        if (this.algorithm == FindingsAlgorithms.INDEL_MISMATCH_DISTANCE){
            return IndelMismatchDistance.calculateEquivalentDistance(sequence1, sequence2, maxMismatch);
        }
        if (this.algorithm == FindingsAlgorithms.KNUTH_MORRIS_PRATT_DISTANCE){
            return new int[] {KnuthMorrisPrattDistance.calculateEquivalentDistance(sequence1.substring(0, sequence2.length()), sequence2, maxMismatch), sequence2.length()};
        }
        if (this.algorithm == FindingsAlgorithms.MISMATCH_INDEL_DISTANCE){
            return MismatchIndelDistance.calculateEquivalentDistance(sequence1, sequence2, maxMismatch);
        }
        return new int[]{-1, 0};
    }
    
    /**
     * checks if sequence2 is a substring of sequence1, but allows mismatches.
     * <br> this function is equivalent to the String indexOf, but allows mismatches.
     * <br> the location of sequence2 in sequence1 is returned, if sequence2 (with eventual mismatches) is found in sequence1
     * <br> the mismatches must be under the allowed mismatches (maxMismatch)
     * <br> -1 is returned if sequence2 isn't found
     * @param sequence1 String | the longest string, where sequence2 must be found in
     * @param sequence2 String | the shortest string, where is looked for in sequence1
     * @param maxMismatch int | the maximum allowed mismatches in the comparison
     * @return int[location, length] | location: the location of the sequence2 in sequence1 (first char is 0), length: length of sequence2 found (with indels)
     */
    public int[] indexOf(String sequence1, String sequence2, int maxMismatch){
        if (this.algorithm == FindingsAlgorithms.HAMMINGS_DISTANCE){
            return new int[] {HammingsDistance.indexOf(sequence1, sequence2, maxMismatch), sequence2.length()};
        }
        if (this.algorithm == FindingsAlgorithms.INDEL_MISMATCH_DISTANCE){
            return IndelMismatchDistance.indexOf(sequence1, sequence2, maxMismatch);
        }
        if (this.algorithm == FindingsAlgorithms.KNUTH_MORRIS_PRATT_DISTANCE){
            return new int[] {KnuthMorrisPrattDistance.indexOf(sequence1, sequence2, maxMismatch), sequence2.length()};
        }
        if (this.algorithm == FindingsAlgorithms.MISMATCH_INDEL_DISTANCE){
            return MismatchIndelDistance.indexOf(sequence1, sequence2, maxMismatch);
        }
        return new int[] {-1, 0};
    }
    
    /**
     * checks if sequence2 is a substring of sequence1, but allows mismathces
     * and if sequence3 is a substring of sequence1, right afther sequence2, allowing mismatches
     * @param sequence1 String | original sequence
     * @param sequence2 String | enzyme cut site
     * @param sequence3 String | adaptor or barcode
     * @param maxMismatch2 int | mismatch for sequence2 (enzyme cut site)
     * @param maxMismatch3 int | mismatch for sequence3 (adaptor)
     * @return int[location, length] | location: -1 if not found, the location if sequence2 + sequence3 are found or the location if sequence2 is found and sequence1 is to short to find sequence3 length: the length of both sequences when found (or with indels)
     */
    public int[] indexOf(String sequence1, String sequence2, String sequence3, int maxMismatch2, int maxMismatch3){
        if (this.algorithm == FindingsAlgorithms.HAMMINGS_DISTANCE){
            return new int[]{HammingsDistance.indexOf(sequence1, sequence2, sequence3, maxMismatch2, maxMismatch3), sequence2.length() + sequence3.length()};
        }
        if (this.algorithm == FindingsAlgorithms.INDEL_MISMATCH_DISTANCE){
            return IndelMismatchDistance.indexOf(sequence1, sequence2, sequence3, maxMismatch2, maxMismatch3);
        }
        if (this.algorithm == FindingsAlgorithms.KNUTH_MORRIS_PRATT_DISTANCE){
            return new int[] {KnuthMorrisPrattDistance.indexOf(sequence1, sequence2, sequence3, maxMismatch2, maxMismatch3), sequence2.length() + sequence3.length()};
        }
        if (this.algorithm == FindingsAlgorithms.MISMATCH_INDEL_DISTANCE){
            return MismatchIndelDistance.indexOf(sequence1, sequence2, sequence3, maxMismatch2, maxMismatch3);
        }
        return new int[]{-1, 0};
    }
    
    
} public class MismatchIndelDistance {
   
    
    
    
    
    /**
     * Calculates the distance between 2 sequences, a mismatch is a distancepoint, same as an insert or deletion.
     * @param sequence1 String | first sequence
     * @param sequence2 String | second sequence
     * @return int the distance between the 2 sequences
     */
    public static int calculateIndelMismatchDistance(String sequence1, String sequence2){
        int distance = 0;
        if (sequence1.length() == 1 || sequence2.length() == 1){
            return Math.max(sequence1.length(), sequence2.length());
        }
        int regularDistance = IndelMismatchDistance.calculateIndelMismatchDistance(sequence1.substring(1), sequence2.substring(1));
        if (sequence1.charAt(0) != sequence2.charAt(0)){
            regularDistance++;
        }
        int insertDistance = IndelMismatchDistance.calculateIndelMismatchDistance(sequence1, sequence2.substring(1)) + 1;
        int deleteDistance = IndelMismatchDistance.calculateIndelMismatchDistance(sequence1.substring(1), sequence2) + 1;
        distance = Math.min(regularDistance, insertDistance);
        distance = Math.min(distance, deleteDistance);
        return distance;
    }
      
    /**
     * returns true if the calculateEquivalentDistance function doesn't return -1
     * <br> so sequence2 is completely found in sequence1 (starting from the begining)
     * <br> with maximum mismatch times a mismatch
     * @param sequence1 String | the original sequence (for the indels, longer than the sequence2)
     * @param sequence2 String | the search sequence (like the barcode, enzyme or adaptor)
     * @param mismatch int | number of mismatches that may occure
     * @return true if sequence2 can be found in the beginning of sequence1
     */
    public static boolean isEquivalent(String sequence1, String sequence2, int mismatch){
        if (IndelMismatchDistance.calculateEquivalentDistance(sequence1, sequence2, mismatch)[0] != -1){
            return true;
        }else{
            return false;
        }
    }
    
    
    /**
     * Compare the begining of sequence1 with sequence2, in sequence1 can occure indels to match sequence2
     * <br> there can only be mismatch number of mismatches, then -1 is returned.
     * @param sequence1 String | the original sequence (possible much longer then sequence2)
     * @param sequence2 String | the search sequence (like the barcode, enzyme, adaptor)
     * @param mismatch int | number of mismatches that may occure
     * @return int [distance, length] | for distance: -1 if the number of mismatches > mismatch, else the number of mismatches found for length: the found length
     * @see FindingAlgorithm#calculateEquivalentDistance(java.lang.String, java.lang.String, int) 
     */
    public static int[] calculateEquivalentDistance(String sequence1, String sequence2, int mismatch){
        if (mismatch < 0){
            //mismatch is lower then zero
            return new int[]{-1, 0};
        }
        if (sequence2.length() == 0){
            //end of search sequence
            return new int[]{0, 0};
        }
        if (sequence1.length() == 0){
            if (sequence2.length() < mismatch){
                return new int[] {sequence2.length(), 0};
            }
            //end of original sequence
            return new int[] {-1, 0};
        }
        //end of strings
        if (sequence2.length() == 1){
            //end of strings
            if (sequence1.charAt(0) == sequence2.charAt(0)){
                //equal => distance is 0
                return new int[]{0, 1};
            }else{
                //not equal => mismatch
                mismatch--;
                if (mismatch < 0){
                    //more mismatches than may occure
                    return new int[]{-1, 0};
                }else{
                    //one mismatch
                    return new int[]{1, 1};
                }
            }
        }
        //not end of string
        int distance = -1;
        int length = 0;
        //calculate regular distance
        int regularMismatch = mismatch;
        int regularDistance = 0;
        if (sequence1.charAt(0) != sequence2.charAt(0)){
            regularDistance = 1;
            regularMismatch--;
        }
        int[] regularOutput = IndelMismatchDistance.calculateEquivalentDistance(sequence1.substring(1), sequence2.substring(1), regularMismatch);
        if (regularOutput[0] == -1){
            regularDistance = -1;
        }else{
            regularDistance += regularOutput[0];
        }
        int regularLength = regularOutput[1] + 1;
        
        
        //if a regular distance is found, do not look for a better indel distance
        if (regularDistance != -1 && regularDistance <= mismatch){
            distance = regularDistance;
            length = regularLength;
        }else{
            //calculate insert distance
            int insertDistance = -1;
            int insertMismatch = mismatch - 1;
            int[] insertOuput = IndelMismatchDistance.calculateEquivalentDistance(sequence1.substring(1), sequence2, insertMismatch);
            int insertLenght = insertOuput[1] + 1;
            if (insertOuput[0] != -1) insertDistance = 1 + insertOuput[0];

            //calculate delete distance
            int deleteDistance = -1;
            int deleteMismatch = mismatch - 1;
            int[] deleteOutput = IndelMismatchDistance.calculateEquivalentDistance(sequence1, sequence2.substring(1), deleteMismatch);
            int deleteLength = deleteOutput[1];
            if (deleteOutput[0] != -1) deleteDistance = deleteOutput[0] + 1;
            
            if (insertDistance != -1){
                distance = insertDistance;
                length = insertLenght;
            }
            if (deleteDistance != -1){
                if (distance == -1){
                    distance = deleteDistance;
                    length = deleteLength;
                }else{
                    if (distance > deleteDistance){
                        distance = deleteDistance;
                        length = deleteLength;
                    }
                }
            }
        }
        
        if (distance > mismatch){
            return new int[]{-1,0};
        }
        return new int[]{distance, length};
    }
    
    
    /**
     * 
     * @param sequence1 String | the original sequence
     * @param sequence2 String | the sequence where is searched for
     * @param mismatch int | the number of mismatches that may occure
     * @return int[distance, length] | distance: -1 if no possible match is found, else the index of sequence2 in sequence1 length: the length of the found piece
     */
    public static int[] indexOf(String sequence1, String sequence2, int mismatch){
        //go over every possible location in the first string
        for (int location = 0; location <= sequence1.length() - sequence2.length(); location++){
            //calculate distance
            int[] distanceLenght = IndelMismatchDistance.calculateEquivalentDistance(sequence1.substring(location), sequence2, mismatch);
            if (distanceLenght[0] != -1){
                return new int[]{location, distanceLenght[1]};
            }
        }
        return new int[]{-1, 0};
    }
    
    /**
     * 
     * @param sequence1 String | the original sequence
     * @param sequence2 String | the first sequence to find in the original
     * @param sequence3 String | the second sequence to find right after sequence2 in the original sequence
     * @param mismatch2 int | number of mismatches possible in sequence2
     * @param mismatch3 int | number of mismatches possible in sequence3
     * @return int[location, lenght] | location: -1 if not found, else start location, length: the length of the found sequences (sequence2.length + sequence3.length, longer when inserts, shorter when deletes)
     */
    public static int[] indexOf(String sequence1, String sequence2, String sequence3, int mismatch2, int mismatch3){
        //go over every possible location in the first string
        for (int location = 0; location <= sequence1.length() - sequence2.length(); location++){
            //calculate distance
            int[] distanceLength = IndelMismatchDistance.calculateEquivalentDistance(sequence1.substring(location), sequence2, mismatch2);
            if (distanceLength[0] != -1){
                //sequence2 found
                if ((sequence1.length() - (location + sequence2.length())) < sequence3.length()){
                    //sequence1 is not long enough to find sequence3
                    return new int[]{location, distanceLength[1]};
                }else{
                    int[] distanceLength3 = IndelMismatchDistance.calculateEquivalentDistance(sequence1.substring(location + distanceLength[1]), sequence3, mismatch3);
                    if (distanceLength3[0] != -1){
                        //sequence3 found
                        return new int[]{location, distanceLength[1] + distanceLength3[1]};
                    }
                }
            }
        }
        return new int[]{-1,0};
    }
    
    
    
    
} public class ErrorInLogException extends Exception{
    
    private final String log;
    private final Exception exeption;
    
    public ErrorInLogException(String log, Exception exeption){
        this.log = log;
        this.exeption = exeption;
    }
    
    /**
     * 
     * @return the log tried to insert
     */
    public String getLog(){
        return this.log;
    }
    
    /**
     * 
     * @return Exception | the exception throwed in the LoggerFile 
     */
    public Exception getException(){
        return this.exeption;
    }
    
} public enum InvalidReadEnum {
    /**
     * invalid read was read1
     */
    READ1,
    /**
     * invalid read was read2
     */
    READ2;
    
} public class InvalidReadException extends Exception{
    
    private final FastqRead read1;
    private final FastqRead read2;
    private final InvalidReadEnum invalidRead;
    
    /**
     * creates a new invalid read (pair-end)
     * @param read1 FastqRead | the first read
     * @param read2 FastqRead | the second read
     * @param invalidRead InvalidReadEnum | the invalid read
     */
    public InvalidReadException(FastqRead read1, FastqRead read2, InvalidReadEnum invalidRead){
        this.read1 = read1;
        this.read2 = read2;
        this.invalidRead = invalidRead;
    }
    
    /**
     * creates a new invalid read (single end)
     * @param read1 FastqRead | the first read
     * @param invalidRead InvalidReadEnum | the invalid read
     */
    public InvalidReadException(FastqRead read1, InvalidReadEnum invalidRead){
        this.read1 = read1;
        this.read2 = null;
        this.invalidRead = invalidRead;
    }
    
    /**
     * 
     * @return InvalidReadEnum | returns witch read was invalid 
     */
    public InvalidReadEnum getInvalidRead(){
        return this.invalidRead;
    }
    
    /**
     * 
     * @return FastqRead | the first read (read from R1)
     */
    public FastqRead getRead1(){
        return this.read1;
    }
    
    /**
     * 
     * @return FastqRead | the second read (read from R2)
     */
    public FastqRead getRead2(){
        return this.read2;
    }
    
    /**
     * 
     * @return String | Read1 as full String
     */
    public String getRun1(){
        return this.read1.getDescription() + "\n" + this.read1.getSequence() + "\n" + "+" + "\n" + this.read1.getQuality() + "\n";
    }
    
    /**
     * 
     * @return String | Read2 as full String
     */
    public String getRun2(){
        return this.read2.getDescription() + "\n" + this.read2.getSequence() + "\n" + "+" + "\n" + this.read2.getQuality() + "\n";
    }
    
} public class StopExcecutionException extends Exception{
    
    
    
} public class EnzymeFileParser {
    
    
    
    public EnzymeFileParser(){
        
    }
    
    
    /**
     * opens the given file and search all samples
     * <br> the file has no header, and the following columns:
     * <br> enzyme name
     * <br> cutsites (comma separated)
     * @param filename String | the name of the file
     * @return an arrayList of all samples found in the file. Non found enzymes are skiped
     * @throws FileNotFoundException
     * @throws IOException 
     */
    public ArrayList<Enzyme> parseEnzymeFile(String filename) throws FileNotFoundException, IOException{
        ArrayList<Enzyme> enzymeArrayList = new ArrayList();
        File file = new File(filename);
        BufferedReader reader = new BufferedReader(new FileReader(file));
        String line;
        while ((line = reader.readLine()) != null){
            String[] splitedLine = line.split("\t");
            String enzymeName = splitedLine[0];
            String[] cutsites = splitedLine[1].split(",");
            EnzymeClass enzymeClass = new EnzymeClass(enzymeName, cutsites);
            enzymeArrayList.add(enzymeClass);
        }
        return enzymeArrayList;
    }
    
} public class FastqBufferedReader {
    
    private final BufferedReader fastqBufferedReader;
    private ReentrantLock lock = new ReentrantLock();
    
    /**
     * creates a new FastqBufferedReader of the given file.
     * @param file | the file that must be read
     * @param ziped | boolean if the fastq file is ziped (gz)
     * @throws FileNotFoundException | if the given file isn't found
     * @throws IOException | if an error occures while opening the file
     */
    public FastqBufferedReader(File file, boolean ziped) throws FileNotFoundException, IOException{
        if (ziped){
            this.fastqBufferedReader = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(file))));
        }else{
            this.fastqBufferedReader = new BufferedReader(new InputStreamReader(new DataInputStream(new FileInputStream(file))));
        }
    }
    
    
    /**
     * reads the next lines in the fastq file. and returns it as an fastqread
     * @return null if there are no fastq files anymore, else a FastqRead
     * @throws IOException | if any error occures while reading the file
     */
    public FastqRead next() throws IOException{
        try{
            lock.lock();
            String descriptionLine = this.fastqBufferedReader.readLine();
            if (descriptionLine == null){
                return null;
            }

            String sequenceLine = this.fastqBufferedReader.readLine();
            if (sequenceLine == null){
                return null;
            }
            //the + line
            this.fastqBufferedReader.readLine();
            String qualityLine = this.fastqBufferedReader.readLine();
            if (qualityLine == null){
                return null;
            }
            FastqRead fastqRead = new FastqRead(descriptionLine, sequenceLine, qualityLine);
            return fastqRead;
        }finally{
            lock.unlock();
        }
    }  
    
    /**
     * closes this buffered reader
     * @throws IOException 
     */
    public void close() throws IOException{
        try{
            lock.lock();
            this.fastqBufferedReader.close();
        }finally{
            lock.unlock();
        }
    }
    
    
    /**
     * closes first the file
     * then execute finilize()
     * @throws Throwable
     */
    @Override
    public void finalize() throws Throwable{
        try {
            this.fastqBufferedReader.close();
        } catch (IOException ex) {
            Logger.getLogger(FastqBufferedReader.class.getName()).log(Level.SEVERE, null, ex);
        }finally{
            super.finalize();
        }
    }
    
} public class FastqBufferedWriter {
    
    private final BufferedWriter fastqBufferedWriter;
    private ReentrantLock lock = new ReentrantLock();
    
    /**
     * creates a new FastqBufferedWriter of the given file.
     * @param file | the file that must be writen
     * @param ziped | true if the file must be zipped
     * @throws FileNotFoundException | if the given file isn't found
     * @throws IOException | if an error occures while opening the file
     */
    public FastqBufferedWriter(File file, boolean ziped) throws FileNotFoundException, IOException{
        if (ziped){
            this.fastqBufferedWriter = new BufferedWriter(new OutputStreamWriter(new GZIPOutputStream(new FileOutputStream(file))));
        }else{
            this.fastqBufferedWriter = new BufferedWriter(new OutputStreamWriter(new DataOutputStream(new FileOutputStream(file))));
        }
    }
    
    /**
     * creates a new FastqBufferedWriter of the given file.
     * @param file | the file that must be writen
     * @param ziped | true if the file must be zipped
     * @param append | true if the file must append an existing file
     * @throws FileNotFoundException | if the given file isn't found
     * @throws IOException | if an error occures while opening the file
     */
    public FastqBufferedWriter(File file, boolean ziped, boolean append) throws FileNotFoundException, IOException{
        if (ziped){
            this.fastqBufferedWriter = new BufferedWriter(new OutputStreamWriter(new GZIPOutputStream(new FileOutputStream(file, append))));
        }else{
            this.fastqBufferedWriter = new BufferedWriter(new OutputStreamWriter(new DataOutputStream(new FileOutputStream(file, append))));
        }
    }
    
    
    /**
     * writes the next lines in the fastq file. 
     * @param fastq | a fastqRead
     * @throws IOException | if any error occures while writing the file
     */
    public void write(FastqRead fastq) throws IOException{
        try{
            lock.lock();
            this.fastqBufferedWriter.write(fastq.getDescription());
            this.fastqBufferedWriter.write("\n");
            this.fastqBufferedWriter.write(fastq.getSequence());
            this.fastqBufferedWriter.write("\n");
            this.fastqBufferedWriter.write("+");
            this.fastqBufferedWriter.write("\n");
            this.fastqBufferedWriter.write(fastq.getQuality());
            this.fastqBufferedWriter.write("\n");
        }catch(IOException e){
            throw new IOException("FastqBuffer closed");
        }finally{
            lock.unlock();
        }
    }
    
    
    /**
     * closes this bufferedWriter
     * @throws IOException 
     */
    public void close() throws IOException{
        try{
            lock.lock();
            this.fastqBufferedWriter.close();
        }finally{
            lock.unlock();
        }
    }
    
    
    /**
     * closes first the file
     * then execute finilize()
     * @throws Throwable
     */
    @Override
    public void finalize() throws Throwable{
        try {
            this.fastqBufferedWriter.close();
        } catch (IOException ex) {
            Logger.getLogger(FastqBufferedWriter.class.getName()).log(Level.SEVERE, null, ex);
        }finally{
            super.finalize();
        }
    }
    
} public class InfoFileParser {
    
    private EnzymeCollection enzymeCollection;
    
    /**
     * creates a new InfoFileParser for the parsing of the info file. Uses the given enzyme collection to detect the possible enzymes
     * @param enzymeCollection 
     */
    public InfoFileParser(EnzymeCollection enzymeCollection){
        this.enzymeCollection = enzymeCollection;
    }
    
    
    /**
     * opens the given file and search all samples
     * <br> the file has no header, and the following columns:
     * <br> sampleID
     * <br> barcode
     * <br> enzyme
     * @param filename String | the name of the file
     * @return an arrayList of all samples found in the file. Non found enzymes are skiped
     * @throws FileNotFoundException
     * @throws IOException 
     */
    public ArrayList<Sample> parseInfoFile(String filename) throws FileNotFoundException, IOException{
        ArrayList<Sample> sampleArrayList = new ArrayList();
        File file = new File(filename);
        BufferedReader reader = new BufferedReader(new FileReader(file));
        String line;
        while ((line = reader.readLine()) != null){
            String[] splitedLine = line.split("\t");
            Enzyme enzyme = this.enzymeCollection.getEnzyme(splitedLine[2]);
            if (enzyme != null){
                if (splitedLine.length > 3){
                    if (splitedLine.length == 4){
                        Enzyme enzyme2 = enzyme;
                        if (! splitedLine[3].equals("")){
                            enzyme2 = this.enzymeCollection.getEnzyme(splitedLine[3]);
                        }
                        String barcode2 = null;
                        Sample newSample = new Sample(splitedLine[0], enzyme, enzyme2, splitedLine[1], barcode2);
                        sampleArrayList.add(newSample);
                    }else if (splitedLine.length == 5){
                        Enzyme enzyme2 = enzyme;
                        if (! splitedLine[3].equals("")){
                            enzyme2 = this.enzymeCollection.getEnzyme(splitedLine[3]);
                        }
                        String barcode2 = null;
                        if (! splitedLine[4].equals("")){
                            barcode2 = splitedLine[4];
                        }
                        Sample newSample = new Sample(splitedLine[0], enzyme, enzyme2, splitedLine[1], barcode2);
                        sampleArrayList.add(newSample);
                    }else{
                        int mismatch = Integer.parseInt(splitedLine[5]);
                        String barcode2 = null;
                        if (! splitedLine[2].equals("")){
                            barcode2 = splitedLine[2];
                        }
                        Enzyme enzyme2 = enzyme;
                        if (! splitedLine[4].equals("")){
                            enzyme2 = this.enzymeCollection.getEnzyme(splitedLine[4]);
                        }
                        Sample newSample = new Sample(splitedLine[0], enzyme, enzyme2, splitedLine[1], barcode2, mismatch);
                        sampleArrayList.add(newSample);
                    }
                }else{
                    Sample newSample = new Sample(splitedLine[0], enzyme, splitedLine[1]);
                    sampleArrayList.add(newSample);
                }
            }
        }
        return sampleArrayList;
    }
    
} public class LoggerFile {
    
    private Writer logwriter;
    private ReentrantLock lock = new ReentrantLock();
    
    /**
     * creates and opens a logfile at the given location
     * @param outputFile String | the location of the logfile (directory)
     * @throws IOException | when a error occures
     */
    public LoggerFile(String outputFile) throws IOException{
        File file = new File(outputFile + System.getProperty("file.separator") + "gbsDemultiplex.log");
        file.createNewFile();
        this.logwriter = new BufferedWriter(new FileWriter(file));
    }
    
    public LoggerFile(DemultiplexParameters parameters) throws IOException, ErrorInLogException{
        File file = new File(parameters.getOutputDirectory() + System.getProperty("file.separator") + "gbsDemultiplex.log");
        file.createNewFile();
        this.logwriter = new BufferedWriter(new FileWriter(file));
        Date now = new Date();
        this.addToLog("Start GBSX demultiplex on " + now.toString());
        this.addToLog("\n");
        this.addToLog("Toolkit Version: " + GBSX.VERSION + "\t GBS demultiplexer Version: " + GBSdemultiplex.VERSION);
        this.addToLog("\n");
        this.addToLog(parameters.getParametersLogString());
    }
    
    public LoggerFile(Parameters parameters) throws IOException, ErrorInLogException{
        File file = new File(parameters.getOutputDirectory() + System.getProperty("file.separator") + "GBSX.log");
        file.createNewFile();
        this.logwriter = new BufferedWriter(new FileWriter(file));
        Date now = new Date();
        this.addToLog("Start tool on " + now.toString());
        this.addToLog("\n");
        this.addToLog("Toolkit Version: " + GBSX.VERSION);
        this.addToLog("\n");
        this.addToLog(parameters.getParametersLogString());
    }
    
    /**
     * add the given log to the logfile
     * @param log String | the log to be writen to the logfile
     * @throws ErrorInLogException 
     */
    public void addToLog(String log) throws ErrorInLogException{
        try {
            lock.lock();
            this.logwriter.write(log);
        } catch (IOException ex) {
            Logger.getLogger(LoggerFile.class.getName()).log(Level.SEVERE, null, ex);
            throw new ErrorInLogException(log, ex);
        }finally{
            lock.unlock();
        }
    }
    
    /**
     * close the logfile
     * @throws ErrorInLogException 
     */
    public void closeLog() throws ErrorInLogException{
        try {
            lock.lock();
            Date now2 = new Date();
            this.addToLog("Ended on " + now2.toString());
        } catch (ErrorInLogException e){
            
        }finally{
            lock.unlock();
            try {
                this.logwriter.close();
            } catch (IOException ex) {
                Logger.getLogger(LoggerFile.class.getName()).log(Level.SEVERE, null, ex);
                throw new ErrorInLogException("closed failed", ex);
            }
        }
    }
    
    @Override
    public void finalize() throws Throwable{
        this.logwriter.close();
        super.finalize();
    }
    
} public class BasePair {
    
    /**
     * returns true if the the found base can be the espected base
     * @param base1 char | the espected base (A, C, G or T)
     * @param base2 char | the found base (A,C,G,T,R,Y,K,M,S,W,B,D,H,V,N)
     * @return true if base2 possible is base1
     */
    public static boolean canBeEqual(char base1, char base2){
        base1 = Character.toUpperCase(base1);
        for (char possibleBase : BasePair.getPossibleBasePairs(base2)){
            if (possibleBase == base1){
                return true;
            }
        }
        return false;
    }
    
    /**
     * transforms the given base to a list of possible bases (usefull for R,Y,...)
     * @param base char | the base to transform
     * @return arraylist of character | the possible corresponding bases.
     */
    private static ArrayList<Character> getPossibleBasePairs(char base){
        base = Character.toUpperCase(base);
        ArrayList<Character> base1List = new ArrayList();
        switch (base){
            case 'A' : base1List.add('A');
                break;
            case 'C' : base1List.add('C');
                break;
            case 'G' : base1List.add('G');
                break;
            case 'T' : base1List.add('T');
                break;
            case 'R' : base1List.add('A');
                base1List.add('G');
                break;
            case 'Y' : base1List.add('T');
                base1List.add('G');
                break;
            case 'K' : base1List.add('G');
                base1List.add('T');
                break;
            case 'M' : base1List.add('A');
                base1List.add('C');
                break;
            case 'S' : base1List.add('G');
                base1List.add('C');
                break;
            case 'W' : base1List.add('A');
                base1List.add('T');
                break;
            case 'B' : base1List.add('G');
                base1List.add('T');
                base1List.add('C');
                break;
            case 'D' : base1List.add('G');
                base1List.add('A');
                base1List.add('T');
                break;
            case 'H' : base1List.add('A');
                base1List.add('C');
                base1List.add('T');
                break;
            case 'V' : base1List.add('G');
                base1List.add('C');
                base1List.add('A');
                break;
            case 'N' : base1List.add('A');
                base1List.add('G');
                base1List.add('C');
                base1List.add('T');
                break;
            default : base1List.add('A');
                base1List.add('G');
                base1List.add('C');
                base1List.add('T');
                break;
        }
        return base1List;
    }
    
    /**
     * creates the complement of the given DNA
     * @param sequence String | the sequence of DNA where the complement must be searched
     * @return the complement of the given DNA
     */
    public static String getComplementSequence(String sequence){
        String complementDNApiece = "";
        //sequence = sequence.toUpperCase();
        for (char base : sequence.toCharArray()){
            switch (base){
                case 'A': complementDNApiece = 'T' + complementDNApiece;
                    break;
                case 'T' : complementDNApiece = 'A' + complementDNApiece;
                    break;
                case 'G' : complementDNApiece = 'C' + complementDNApiece;
                    break;
                case 'C': complementDNApiece = 'G' + complementDNApiece;
                    break;
                case 'a': complementDNApiece = 't' + complementDNApiece;
                    break;
                case 't' : complementDNApiece = 'a' + complementDNApiece;
                    break;
                case 'g' : complementDNApiece = 'c' + complementDNApiece;
                    break;
                case 'c': complementDNApiece = 'g' + complementDNApiece;
                    break;
                case '[' : complementDNApiece = ']' + complementDNApiece;
                    break;
                case ']' : complementDNApiece = '[' + complementDNApiece;
                    break;
                default : complementDNApiece = base + complementDNApiece;
                    break;
            }
        }
        return complementDNApiece;
    }
} public interface Enzyme{    
    /**
     * 
     * @return String the name of the enzyme
     */
    public String getName();
    
    /**
     * 
     * @return collection of String the sites after the cut
     */
    public Collection<String> getInitialCutSiteRemnant();
    
    /**
     * 
     * @return collection of String the complement sites after the cut
     */
    public Collection<String> getComplementCutSiteRemnant();
    
} public enum FastqParts {
    
    /**
     * description line
     */
    DESCRIPTION,
    
    /**
     * sequence line
     */
    SEQUENCE,
    
    /**
     * quality line
     */
    QUALITY;
    
} public enum DemultiplexArguments implements Arguments{
    
    
    /**
     * Looks if there occured an adapter ligase
     */
    ADAPTOR_LIGASE ("-al"),
    /**
     * The common adaptor wich must be used
     */
    COMMON_ADAPTOR ("-ca"),
    /**
     * The common adaptor compare size
     */
    COMMON_ADAPTOR_COMPARE_SIZE ("-cas"),
    /**
     * Checks the complete read for the enzyme (if false, stops at the first possible enzyme cutsite)
     */
    COMPLETE_CHECK ("-cc"),
    /**
     * Add enzymes from the given file
     */
    ENZYME_ADD ("-ea"),
    /**
     * Replace systems enzyme with the given file
     */
    ENZYME_REPLACE ("-er"),
    /**
     * The filename of the first read
     * Mandatory
     */
    FILENAME1 ("-f1"),
    /**
     * The filename of the second read
     * Optional
     */
    FILENAME2 ("-f2"),
    /**
     * The info file (tab delimited file:
     * <br> first col: sample
     * <br> second col: barcode
     * <br> third col: enzyme
     * Mandatory
     */
    INFO_FILE ("-i"),
    /**
     * Keep the enzyme cut sites
     */
    KEEP_ENZYME_CUTSITES ("-kc"),
    /**
     * The used algorithm to search for mismatches
     */
    MISMATCH_ALGORITHM ("-malg"),
    /**
     * The allowed mismatches in a barcode enzyme combination
     */
    ALLOWED_MISMATCHES_BARCODE ("-mb"),
    /**
     * The allowed mismatches in the enzyme
     */
    ALLOWED_MISMATCHES_ENZYME ("-me"),
    /**
     * The minimum length of the sequence
     */
    MINIMUM_SEQUENCE_LENGTH ("-minsl"),
    /**
     * Keep sequences with N as nucleotide
     */
    KEEP_SEQUENCES_WITH_N ("-n"),
    /**
     * The place of the output
     * Optional
     */
    OUTPUT_DIRECTORY ("-o"),
    /**
     * The type of the quality score
     * @see FastqScores
     */
    QUALITY_SCORE ("-q"),
    /**
     * If the data is RAD data
     */
    IS_RAD ("-rad"),
    /**
     * The maximum number of basepairs between the start of the sequence and the barcode
     * Optional
     */
    START_BARCODE_DISTANCE ("-s"),
    /**
     * Use gzip files
     * Optional
     */
    USE_GZIP_FILES ("-gzip"),
    /**
     * use long file names
     */
    LONG_FILE_NAMES ("-lf"),
    /**
     * use self correcting barcodes (hamming code)
     */
    USE_SELF_CORRECTING_BARCODES ("-scb"),
    /**
     * number of threads
     */
    THREADS ("-t"),
    /**
     * If the given argument was invalid
     */
    INVALID_ARGUMENT ("ERROR");
    
    /**
     * returns the argument for the given name (-name), or invalid if no argument exists for the given name
     * @param sortName String | the name of the argument
     * @return GBSargument if any exists for the given name, else GBSaguments.INVALID_ARGUMENT
     */
    public DemultiplexArguments getArgument(String sortName){
        for (DemultiplexArguments arg : DemultiplexArguments.values()){
            if (sortName.equals(arg.getSortName())){
                return arg;
            }
        }
        return DemultiplexArguments.INVALID_ARGUMENT;
    }
    
    /**
     * the name of the argument (-name)
     */
    private final String sortname; 
    
    /**
     * 
     * @param sortname String | name of the argument
     */
    private DemultiplexArguments(String sortname){
        this.sortname = sortname;
    }
    
    /**
     * 
     * @return String | the option name of the argument
     */
    public String getSortName(){
        return this.sortname;
    }
    
} public class DemultiplexParameters implements Parameters{
    
    private HashMap<DemultiplexArguments, String> arguments;
    //standard barcode adaptor (can be changed by using parameter -ca)
    private final static String COMMON_AND_BARCODED_ADAPTOR = "AGATCGGAAGAGCG";
    private EnzymeCollection enzymeCollection;
    private boolean doubleBarcodes;
    
    public DemultiplexParameters(){
        this.arguments = new HashMap();
        //standard parameters
        this.arguments.put(DemultiplexArguments.OUTPUT_DIRECTORY, System.getProperty("user.dir"));
        this.arguments.put(DemultiplexArguments.ALLOWED_MISMATCHES_BARCODE, "1");
        this.arguments.put(DemultiplexArguments.ALLOWED_MISMATCHES_ENZYME, "1");
        this.arguments.put(DemultiplexArguments.START_BARCODE_DISTANCE, "0");
        this.arguments.put(DemultiplexArguments.COMPLETE_CHECK, "true");
        this.arguments.put(DemultiplexArguments.QUALITY_SCORE, FastqScores.ILLUMINA_1_8.getType());
        this.arguments.put(DemultiplexArguments.ADAPTOR_LIGASE, "1");
        this.arguments.put(DemultiplexArguments.MINIMUM_SEQUENCE_LENGTH, "0");
        this.arguments.put(DemultiplexArguments.KEEP_SEQUENCES_WITH_N, "true");
        this.arguments.put(DemultiplexArguments.COMMON_ADAPTOR, DemultiplexParameters.COMMON_AND_BARCODED_ADAPTOR);
        this.arguments.put(DemultiplexArguments.USE_GZIP_FILES, "false");
        this.arguments.put(DemultiplexArguments.KEEP_ENZYME_CUTSITES, "true");
        this.arguments.put(DemultiplexArguments.IS_RAD, "false");
        this.arguments.put(DemultiplexArguments.ENZYME_ADD, "false");
        this.arguments.put(DemultiplexArguments.ENZYME_REPLACE, "false");
        this.arguments.put(DemultiplexArguments.LONG_FILE_NAMES, "false");
        this.arguments.put(DemultiplexArguments.USE_SELF_CORRECTING_BARCODES, "false");
        this.arguments.put(DemultiplexArguments.COMMON_ADAPTOR_COMPARE_SIZE, "14");
        this.arguments.put(DemultiplexArguments.THREADS, "1");
        //this will automaticaly return the standard algorithm
        this.arguments.put(DemultiplexArguments.MISMATCH_ALGORITHM, FindingsAlgorithms.getStandard().getAlgorithmName());
        this.enzymeCollection = new EnzymeCollection();
        this.doubleBarcodes = false;
    }
    
    /**
     * Changes the double barcodes setting to true
     */
    public void setDoubleBarcodes(){
        this.doubleBarcodes = true;
    }
    
    /**
     * 
     * @return the double barcodes status, true if double barcodes are used
     */
    public boolean useDoubleBarcodes(){
        return this.doubleBarcodes;
    }
    
    /**
     * 
     * @param argument DemultiplexArguments
     * @return true if the given argument is a parameter
     */
    public boolean containsParameter(DemultiplexArguments argument){
        return this.arguments.containsKey(argument);
    }
    
    /**
     * 
     * @param argument DemultiplexArguments
     * @return the asked argument as a String
     */
    public String getParameter(DemultiplexArguments argument){
        return this.arguments.get(argument);
    }
    
    /**
     * adds the given parameter for the given argument
     * @param argument GBSargument | the argument
     * @param parameter String | the parameter
     * @throws RuntimeException when the given argument is an invalid argument
     */
    public void setParameter(DemultiplexArguments argument, String parameter){
        if (argument.equals(DemultiplexArguments.INVALID_ARGUMENT)){
            throw new RuntimeException("Contains invalid arguments");
        }else if(argument == DemultiplexArguments.QUALITY_SCORE){
            FastqScores fastqScores = FastqScores.getFastqScores(parameter);
            this.arguments.put(DemultiplexArguments.QUALITY_SCORE, fastqScores.getType());
        }else if (argument == DemultiplexArguments.COMMON_ADAPTOR){
            if (parameter.length() < 0){
                throw new RuntimeException("Invalid Common Adaptor");
            }else{
                this.arguments.put(argument, parameter);
                this.arguments.put(DemultiplexArguments.COMMON_ADAPTOR_COMPARE_SIZE, "" + parameter.length());
            }
        }else{
            this.arguments.put(argument, parameter);
        }
    }
    
    /**
     * checks if all required parameters are set
     * @return true if all parameters are set, else false
     */
    @Override
    public boolean areRequiredParametersSet(){
        if (! this.arguments.containsKey(DemultiplexArguments.FILENAME1)){
            return false;
        }
        if (! this.arguments.containsKey(DemultiplexArguments.INFO_FILE)){
            return false;
        }
        return true;
    }
    
    /**
     * checks if all required parameters are set
     * @return the String 'All values are set' if all required parameters are set, else an error message
     */
    @Override
    public String getErrorRequiredParametersSet(){
        String error = "";
        if (! this.arguments.containsKey(DemultiplexArguments.FILENAME1)){
            error += "Value required for option " + DemultiplexArguments.FILENAME1.getSortName() + "\n";
        }
        if (! this.arguments.containsKey(DemultiplexArguments.INFO_FILE)){
            error += "Value required for option " + DemultiplexArguments.INFO_FILE.getSortName() + "\n";
        }
        if (error.equals("")){
            error = "All values are set";
        }
        return error;
    }
    
    
    /**
     * 
     * @return int | the allowed mismatches for the barcode
     */
    public int getAllowedMismatchesBarcode(){
        return Integer.parseInt(this.arguments.get(DemultiplexArguments.ALLOWED_MISMATCHES_BARCODE));
    }
    
    /**
     * returns the sample specific mismatches
     * <br> if the sample barcode is 0 or positif, the sample barcode is returned
     * <br> else the standard or universal defined mismatches are used
     * @param sample Sample | the sample for the mismatches
     * @return int | the allowed mismatches for the barcode
     * @see DemultiplexParameters#getAllowedMismatchesBarcode() 
     */
    public int getAllowedMismatchesBarcode(Sample sample){
        if (sample.getBarcodeMismatches() >= 0){
            return sample.getBarcodeMismatches();
        }else{
            return this.getAllowedMismatchesBarcode();
        }
    }
    
    /**
     * returns true if the input is a gziped file, and the output must be a gziped file
     * @return true if the input file is gziped (.gz) and the output must be ziped
     */
    public boolean mustBeZipped(){
        if (this.arguments.get(DemultiplexArguments.USE_GZIP_FILES).toLowerCase().equals("true")){
            return true;
        }else{
            return false;
        }
    }
    
    /**
     * returns true if the enzyme cutsites remains must be kept, false otherwise
     * @return true if the enzyme cutsites must be kept
     */
    public boolean keepCutSites(){
        if (this.arguments.get(DemultiplexArguments.KEEP_ENZYME_CUTSITES).toLowerCase().equals("true")){
            return true;
        }else{
            return false;
        }
    }
    
    /**
     * return the extension of the input and output files 
     * <br> this is .fastq.gz for gziped files
     * <br> this is .fastq for regular files
     * @return String | the extension of the file
     */
    public String getFileExtension(){
        if (this.mustBeZipped()){
            return ".fastq.gz";
        }else{
            return ".fastq";
        }
    }
    
    /**
     * 
     * @return true if long file names must be used
     */
    public boolean useLongFileNames(){
        if (this.arguments.get(DemultiplexArguments.LONG_FILE_NAMES).toLowerCase().equals("true")){
            return true;
        }else{
            return false;
        }
    }
    
    /**
     * 
     * @return int | the allowed mismatches for the enzyme
     */
    public int getAllowedMismatchesEnzyme(){
        return Integer.parseInt(this.arguments.get(DemultiplexArguments.ALLOWED_MISMATCHES_ENZYME));
    }
    
    /**
     * 
     * @return int | the max distance of the startsite (distance between the start of the read sequence and the barcode)
     */
    public int getStartDistance(){
        return Integer.parseInt(this.arguments.get(DemultiplexArguments.START_BARCODE_DISTANCE));
    }
    
    /**
     * 
     * @return String | the location of the output directory
     */
    public String getOutputDirectory(){
        return this.arguments.get(DemultiplexArguments.OUTPUT_DIRECTORY);
    }
    
    /**
     * 
     * @return String | the name of the info file
     */
    public String getInfoFile(){
        return this.arguments.get(DemultiplexArguments.INFO_FILE);
    }
    
    /**
     * 
     * @return String | the name of the first fastq file
     */
    public String getFastqFile1(){
        return this.arguments.get(DemultiplexArguments.FILENAME1);
    }
    
    /**
     * 
     * @return String | the name of the second fastq file, or null if only 1 file was given
     */
    public String getFastqFile2(){
        return this.arguments.get(DemultiplexArguments.FILENAME2);
    }
    
    /**
     * 
     * @return true if the complete check option is used
     */
    public boolean completeCheck(){
        if (this.arguments.get(DemultiplexArguments.COMPLETE_CHECK).toLowerCase().equals("true")) {
            return true;
        }
        return false;
    }
    
    /**
     * 
     * @return true if the sequences where N occurs as a nucleotide must be kept. 
     */
    public boolean keepSequencesWithN(){
        if (this.arguments.get(DemultiplexArguments.KEEP_SEQUENCES_WITH_N).toLowerCase().equals("true")){
            return true;
        }else{
            return false;
        }
    }
    
    /**
     * 
     * @return FastqScores | the used fastq score
     */
    public FastqScores getFastqQualityScore(){
        return FastqScores.getFastqScores(this.arguments.get(DemultiplexArguments.QUALITY_SCORE));
    }
    
    /**
     * 
     * @return int | the number of mismatches in the adaptor
     */
    public int getAdaptorLigaseMismatches(){
        if (this.arguments.get(DemultiplexArguments.ADAPTOR_LIGASE).equals("no")){
            return -1;
        }else{
            return Integer.parseInt(this.arguments.get(DemultiplexArguments.ADAPTOR_LIGASE));
        }
    }
    
    /**
     * 
     * @return String | the common adaptor that is used in the GBS
     */
    public String getCommonAdaptor(){
        return this.arguments.get(DemultiplexArguments.COMMON_ADAPTOR);
    }
    
    /**
     * 
     * @return int | the minimum amount of basepairs in a sequence
     */
    public int getMinimumSequenceLength(){
        return Integer.parseInt(this.arguments.get(DemultiplexArguments.MINIMUM_SEQUENCE_LENGTH));
    }
    
    /**
     * 
     * @return int | the size of the common adapter to compare to
     */
    public int getAdaptorCompareSize(){
        return Integer.parseInt(this.arguments.get(DemultiplexArguments.COMMON_ADAPTOR_COMPARE_SIZE));
    }
    
    /**
     * returns true if the data is RAD data, returns false if the data is gbs data
     * @return true if the data is RAD data
     */
    public boolean isRadData(){
        if (this.arguments.get(DemultiplexArguments.IS_RAD).toLowerCase().equals("true")){
            return true;
        }else{
            return false;
        }
    }
    
    /**
     * returns the findings algorithm that must be used for the mismatches and indels
     * @return 
     */
    public FindingsAlgorithms getFindingsAlgorithm(){
        return FindingsAlgorithms.getAlgorithm(this.arguments.get(DemultiplexArguments.MISMATCH_ALGORITHM));
    }
    
    /**
     * returns all known enzymes
     * @return a collection of all enzymes
     * @see EnzymeCollection#getAllEnzymes() 
     */
    public Collection<Enzyme> getAllEnzymes(){
        return this.enzymeCollection.getAllEnzymes();
    }
    
    /**
     * returns the neutral enzyme
     * @return the neutral enzyme
     * @see EnzymeCollection#getNeutralEnzyme() 
     */
    public Enzyme getNeutralEnzyme(){
        return this.enzymeCollection.getNeutralEnzyme();
    }
    
    /**
     * returns the enzyme with the given name
     * @param enzymeName String | the name of the enzyme
     * @return Enzyme
     */
    public Enzyme getEnzyme(String enzymeName){
        return this.enzymeCollection.getEnzyme(enzymeName);
    }
    
    /**
     * returns the enzyme collection
     * @return EnzymeCollection
     */
    public EnzymeCollection getEnzymeCollection(){
        return this.enzymeCollection;
    }
    
    /**
     * 
     * @return false if there aren't enzymes to be added, otherwise true
     */
    public boolean mustAddEnzymes(){
        if (this.arguments.get(DemultiplexArguments.ENZYME_ADD).toLowerCase().equals("false")){
            return false;
        }else{
            return true;
        }
    }
    
    /**
     * 
     * @return String | the file name where the enzymes must be added from to the enzymeCollection
     */
    public String getAddEnzymeFile(){
        return this.arguments.get(DemultiplexArguments.ENZYME_ADD);
    }
    
    /**
     * 
     * @return false if there aren't enzymes to be replaced, otherwise true
     */
    public boolean mustReplaceEnzymes(){
        if (this.arguments.get(DemultiplexArguments.ENZYME_REPLACE).toLowerCase().equals("false")){
            return false;
        }else{
            return true;
        }
    }
    
    /**
     * 
     * @return true if the barcodes are self correcting
     */
    public boolean useSelfCorrectingBarcodes(){
        if (this.arguments.get(DemultiplexArguments.USE_SELF_CORRECTING_BARCODES).toLowerCase().equals("true")){
            return true;
        }else{
            return false;
        }
    }
    
    /**
     * 
     * @return String | the file name where the enzymes must be added from to the enzymeCollection
     */
    public String getReplaceEnzymeFile(){
        return this.arguments.get(DemultiplexArguments.ENZYME_REPLACE);
    }
    
    /**
     * 
     * @return integer | the number of threads
     */
    public int getThreadNumber(){
        return Integer.parseInt(this.arguments.get(DemultiplexArguments.THREADS));
    }
    
    /**
     * configures a log file for all known parameters
     * @return String to put in the log file
     */
    @Override
    public String getParametersLogString(){
        String toLog = "";
        toLog += "Started to open the first files, parsing of the parameters succeded." + "\n";
        toLog += "Parameters: " + "\n";
        //-f1
        toLog += "\t First fastq file: \t" + this.getFastqFile1() + "\n";
        if (this.getFastqFile2() != null){
            //-f2
            toLog += "\t Second fastq file: \t" + this.getFastqFile2() + "\n";
        }
        //-i
        toLog += "\t Info-file: \t" + this.getInfoFile() + "\n";
        //-o
        toLog += "\t Output directory: \t" + this.getOutputDirectory() + "\n";
        //-gzip
        toLog += "\t In- and output file are gziped: \t" + this.mustBeZipped() + "\n";
        //-lf
        toLog += "\t Use long file names: \t" + this.useLongFileNames()+ "\n";
        //-rad
        toLog += "\t Data type: ";
        if (this.isRadData()){
            toLog += "\t RAD" + "\n";
        }else{
            toLog += "\t GBS" + "\n";
        }
        //-malg
        toLog += "\t Used algorithm to detect mismatches and/or indels: \t" + this.getFindingsAlgorithm().getAlgorithmName() + "\n";
        //-scb
        toLog += "\t Used self correcting barcodes: \t" + this.useSelfCorrectingBarcodes() + "\n";
        //-m or -mb
        toLog += "\t Allowed mismatches in the barcode: \t" + this.getAllowedMismatchesBarcode() + "\n";
        //-m or -me
        toLog += "\t Allowed mismatches in the enzyme: \t" + this.getAllowedMismatchesEnzyme() + "\n";
        //-p
        toLog += "\t Must check reads completely: \t" + this.completeCheck() + "\n";
        //-kc
        toLog += "\t Must keep the cutsites: \t" + this.keepCutSites() + "\n";
        //-al
        if (this.getAdaptorLigaseMismatches() == -1){
            toLog += "\t Check for Adaptor ligase: \t" + "no" + "\n";
        }else{
            toLog += "\t Allowed mismatches to check the adaptor ligase: \t" + this.getAdaptorLigaseMismatches() + "\n";
            //-ca
            toLog += "\t Used common adaptor: \t" + this.getCommonAdaptor() + "\n";
        }
        //-s
        toLog += "\t Maximum distance between start of sequence and start of barcode: \t" + this.getStartDistance() + "\n";
        //-q
        toLog += "\t Used scale for the quality: \t" + this.getFastqQualityScore().getType() + "\n";
        //-minsl
        toLog += "\t Minimum length of the sequence: \t" + this.getMinimumSequenceLength() + "\n";
        //-n
        toLog += "\t Keep sequences with N as nucleotide: \t" + this.keepSequencesWithN() + "\n";
        if (this.mustAddEnzymes()){
            toLog += "\t Added enzymes of file: \t" + this.getAddEnzymeFile() + "\n";
        }
        if (this.mustReplaceEnzymes()){
            toLog += "\t Replaced enzymes with file: \t" + this.getReplaceEnzymeFile() + "\n";
        }
        toLog += "\n";
        if (this.getFastqFile2() == null){
            toLog += "Single read demultiplexing" + "\n";
        }else{
            toLog += "Paired end demultiplexing" + "\n";
        }
        toLog += "Use double barcodes " + this.useDoubleBarcodes() + "\n";
        toLog += "\n";
        return toLog;
    }
    
    
    /**
     * makes a help for all known parameters
     * @return String | the help page
     */
    @Override
    public String getParametersHelp(){
        String toHelp = "";
        toHelp += "These parameters are mandatory: " + "\n";
        toHelp += "\t -f1 \t the name and path of the fastq or fastq.gz file to demultiplex" + "\n";
        toHelp += "\t -i \t the name and path of the info file. This is a tab delimeted file without headings, "
                + "with three (or more) columns: sample, sequence of the barcode, name of the enzyme, "
                + "name of the second enzyme (optional, can be an empty string), the second barcode (optional, can be an empty string),"
                + "mismatches for the barcode (optional)" + "\n";
        toHelp += "\n";
        toHelp += "These parameters are optional: " + "\n";
        toHelp += "\t -f2 \t the name of the second fastq or fastq.gz file (only with paired-end sequencing)" + "\n";
        toHelp += "\t -o \t the name of the output directory (standard the directory of the call)" + "\n";
        toHelp += "\t -lf \t use long file names (standard false) filename is standard the sample name, long file names is sample name _ barcode _ enzyme" + "\n";
        toHelp += "\t -rad \t if the data is rad data or not (-rad true for RAD data, -rad false for GBS data) standard false (GBS)" + "\n";
        toHelp += "\t -gzip \t the input and output are/must be gziped (.gz) (standard false: input and output are .fastq, if true this is .fastq.gz)" + "\n";
        toHelp += "\t -t \t the number of threads to use (standard 1)" + "\n";
        toHelp += "\t -mb \t the allowed mismatches in the barcodes (overrides the option -m)" + "\n";
        toHelp += "\t -me \t the allowed mismatches in the enzymes (overrides the option -m)" + "\n";
        toHelp += "\t -minsl \t the minimum allowed length for the sequences (standard 0, rejected sequences are found in the stats for each sample in the rejected.count column. The sequences self are found untrimmed in the undetermined file.)" + "\n";
        toHelp += "\t -n \t keep sequences where N occurs as nucleotide (standard true)" + "\n";
        toHelp += "\t -ca \t the common adaptor used in the sequencing (standard (only first piece) AGATCGGAAGAGCG) currently only used for adaptor ligase see -al and when -rad is true) (minimum length is 10)" + "\n";
        toHelp += "\t -s \t the posible distance of the start. This is the distance count from the start of the read to the first basepair of the barcode or enzyme (standard 0, maximum 20)" + "\n";
        //toHelp += "\t -cc \t Checks the complete read for the enzyme (if false, stops at the first possible enzyme cutsite) (use values true or false, standard is true) if used, the sequence after the enzyme site is compared to the adaptors, if the first basepairs of the sequence are compaired to the first basepairs of the adaptor" + "\n";
        toHelp += "\t -kc \t Keep the enzyme cut-site remains (standard true)" + "\n";
        toHelp += "\t -ea \t Add enzymes from the given file (keeps the standard enzymes, and add the new) (enzyme file: no header, enzyme name tab cutsites (multiple cutsites are comma separeted)) (only use once, not use -er)" + "\n";
        toHelp += "\t -er \t Replace enzymes from the given file (don't keep the standard enzymes) (enzyme file: no header, enzyme name tab cutsites (multiple cutsites are comma separeted)) (only use once, not use -ea)" + "\n";
        //toHelp += "\t -al \t check for adaptor ligase: no (for no check) or a positive integer (starts at 0), for the number of mismatches (only checks 10 basepairs of the adaptor), standard 1" + "\n";
        toHelp += "\t -scb \t Use self correcting barcodes (barcodes created by the barcodeGenerator) (standard false)" + "\n";
        toHelp += "\t -malg \t the used algorithm to find mismatches and indels, possible algorithms (see README): " + "\n";
        for (FindingsAlgorithms algorithm : FindingsAlgorithms.values()){
            toHelp += "\t \t \t" + algorithm.getAlgorithmName();
            if (algorithm == FindingsAlgorithms.getStandard()){
                toHelp += " (Standard)";
            }
            toHelp += "\t" + algorithm.getDescription();
            toHelp += "\n";
        }
        toHelp += "\n";
        toHelp += "\t -q \t the kind of quality scores used in the fastq file (including how phred scores are encoded): " + "\n";
        for (FastqScores fastqScores : FastqScores.values()){
            toHelp += "\t \t \t" + fastqScores.getType();
            if (fastqScores == FastqScores.getStandard()){
                toHelp += " (Standard)";
            }
            toHelp += "\n";
        }
        toHelp += "\n";
        return toHelp;
    }

    @Override
    public boolean containsParameter(Arguments argument) {
        if (argument instanceof DemultiplexArguments){
            return this.containsParameter((DemultiplexArguments) argument);
        }else{
            return false;
        }
    }

    @Override
    public String getParameter(Arguments argument) {
        if (argument instanceof DemultiplexArguments){
            return this.getParameter((DemultiplexArguments) argument);
        }else{
            return "ERROR";
        }
    }

    @Override
    public void setParameter(Arguments argument, String parameter) {
        if (argument instanceof DemultiplexArguments){
            this.setParameter((DemultiplexArguments) argument, parameter);
        }
    }

   
    
    
    
} public class ProcessedFragment {

    /**
     * all information of the processed fragment:
     * <br> the sample of the read, 
     * <br> read1 (as HashMap of FastqParts and String)
     * <br> read2 (only pair-end) (as HashMap of FastqParts and String)
     * <br> mismatch occured in finding the barcode/enzyme
     */
        
    private Sample sample;
    private FastqRead read1;
    private FastqRead read2;
    private int mismatch;
    private String sequenceComment = "";

    /**
     * create a new ProcessedFragment (pair-end)
     * @param sample Sample | the sample of the fragment
     * @param read1 FastqRead | the first read of the fragment
     * @param read2 FastqRead | the second read of the fragment (only pair-end)
     * @param mismatch int | number of mismatches in the barcode/enzyme
     * @param sequenceComment String | the comment on the cut of the sequence
     */
    public ProcessedFragment(Sample sample, FastqRead read1, FastqRead read2, int mismatch, String sequenceComment){
        this.sample = sample;
        this.read1 = read1;
        this.read2 = read2;
        this.mismatch = mismatch;
        this.sequenceComment = sequenceComment;
    }

    /**
     * create a new ProcessedFragment (pair-end)
     * @param sample Sample | the sample of the fragment
     * @param read1 FastqRead | the first read of the fragment
     * @param read2 FastqRead | the second read of the fragment (only pair-end)
     * @param mismatch int | number of mismatches in the barcode/enzyme
     */
    public ProcessedFragment(Sample sample, FastqRead read1, FastqRead read2, int mismatch){
        this.sample = sample;
        this.read1 = read1;
        this.read2 = read2;
        this.mismatch = mismatch;
    }

    /**
     * create a new ProcessedFragment (single read)
     * @param sample Sample | the sample of the fragment
     * @param read1 FastqRead | the only read of the fragment
     * @param mismatch int | number of mismatches in the barcode/enzyme
     */
    public ProcessedFragment(Sample sample, FastqRead read1, int mismatch){
        this.sample = sample;
        this.read1 = read1;
        this.read2 = null;
        this.mismatch = mismatch;
    }

    /**
     * 
     * @return Sample | the sample of this fragment
     */
    public Sample getSample(){
        return this.sample;
    }

    /**
     * 
     * @return HashMap of FastqParts and String | the first read
     */
    public FastqRead getRead1(){
        return this.read1;
    }

    /**
     * 
     * @return HashMap of FastqParts and String | the second read (only by pair-end)
     */
    public FastqRead getRead2(){
        return this.read2;
    }

    /**
     * 
     * @return int | number of mismatches in barcode/enzyme
     */
    public int getMismatch(){
        return this.mismatch;
    }

    /**
     * 
     * @return String | the comment on the cut of the reads
     */
    public String getComment(){
        return this.sequenceComment;
    }

} public class SampleBarcodeCombination {

    /**
     * a combination of a sample, a barcode and enzyme, the mismatches (between sequence and barcode/enzyme) and the start location of the barcode in the sequence
     */
        
    private final Sample sample;
    private final String enzymeCutSite;
    private final int location;
    private final int mismatches;
    private final int lengthFoundBarcode;
    private final int lengthFoundEnzyme;
    /**
     * 
     * @param sample Sample | the sample of this combination
     * @param enzymeCutSite String | the used enzyme cutsite
     * @param location int | the start location of the barcodeEnzyme
     * @param mismatches int | the amount of mismatches
     * @param lengthFoundBarcode int | length of the found barcode
     * @param lengthFoundEnzyme int | length of the found enzyme
     */
    public SampleBarcodeCombination(Sample sample, String enzymeCutSite, int location, int mismatches, int lengthFoundBarcode, int lengthFoundEnzyme){
        this.sample = sample;
        this.enzymeCutSite = enzymeCutSite;
        this.location = location;
        this.mismatches = mismatches;
        this.lengthFoundBarcode = lengthFoundBarcode;
        this.lengthFoundEnzyme = lengthFoundEnzyme;
    }

    /**
     * 
     * @return Sample | the sample of this combination
     */
    public Sample getSample(){
        return this.sample;
    }

    /**
     * 
     * @return String | the enzymeCutsite
     */
    public String getEnzymeCutsite(){
        return this.enzymeCutSite;
    }

    /**
     * 
     * @return int | the location of the barcode + enzyme in a sequence
     */
    public int getLocation(){
        return this.location;
    }

    /**
     * 
     * @return int | the amount of mismatches between the barcode + enzyme and the sequence
     */
    public int getMismatches(){
        return this.mismatches;
    }

    /**
     * 
     * @return int | the length of the found enzyme
     */
    public int getLengthFoundEnzyme(){
        return this.lengthFoundEnzyme;
    }

    /**
     * 
     * @return int | the lenght of the found barcode
     */
    public int getLengthFoundBarcode(){
        return this.lengthFoundBarcode;
    }

    /**
     * 
     * @param s SampleBarcodeCombination
     * @return true if the same sample is used
     */
   public boolean isSameSample(SampleBarcodeCombination s){
       if (s.getSample().equals(this.getSample())){
           return true;
       }else{
           return false;
       }
   }
   
   /**
    * 
    * @param s SampleBarcodeCombination
    * @return true is this sampleBarcodeCombination is better then s:
    *   the location of this must be lower
    *   when equal location this mismatches must be lower or equal
    */
   public boolean isThisBetter(SampleBarcodeCombination s){
       if(isSameSample(s)){
           if(s.getLocation() < this.getLocation()){
               return false;
           }else if(s.getLocation() > this.getLocation()){
               return true;
           }else{
               if(s.getMismatches() >= this.getMismatches()){
                   return true;
               }else{
                   return false;
               }
           }
       }else{
           return false;
       }
   }
} public class EnzymeComparator implements Comparator<Enzyme>{

    
    /**
     * 
     * @param enzyme1 Enzyme
     * @param enzyme2 Enzyme
     * @return a negative int if the first enzyme is less than the second,
     *          zero when both enzymes are equal
     *          a positive int when the first enzyme is greater than the second
     */
    public int compare(Enzyme enzyme1, Enzyme enzyme2) {
        return enzyme1.getName().compareTo(enzyme2.getName());
    }
    
    
} public enum EnzymeEnum implements Enzyme{
    
    /*
     * TO ADD ENZYMES:
     * first name of the enzyme (caps) then the name, and the cutsites
     * copy:
     * NAME ("Name", new String[] {"first cutsite remains", "second cutsite remains", "third cutsite remains", ...}),
     */
    
    APEKI ("ApeKI", new String[] {"CAGC", "CTGC"}),
    
    PSTI ("PstI", new String[] {"TGCAG"}),
    
    ECOT22I ("EcoT22I", new String[] {"TGCAT"}),
    
    PASI ("PasI", new String[] {"CAGGG", "CTGGG"}),
    
    HPAII ("HpaII", new String[] {"CGG"}),
    
    MSPI ("MspI", new String[]{"CGG"}),
    
    PST1_ECOT22I ("PstI-EcoT22I", new String[]{"TGCAG", "TGCAT"}),
    
    PSTI_MSPI ("PstI-MspI", new String[]{"TGCAG"}),
    
    PSTI_TAQI ("PstI-TaqI", new String[]{"TGCAG"}),
    
    SBFI_MSPI ("SbfI-MspI", new String[]{"TGCAGG"}),
    
    ASISI_MSPI ("AsiSI-MspI", new String[]{"ATCGC"}),
    
    BSSHII_MSPI ("BssHII-MspI", new String[]{"CGCGC"}),
    
    FSEI_MSPI ("FseI-MspI", new String[]{"CCGGCC"}),
    
    SALI_MSPI ("SalI-MspI", new String[]{"TCGAC"}),
    
    APOI ("ApoI", new String[]{"AATTC", "AATTT"}),
    
    BAMHI ("BamHI", new String[]{"GATCC"}),
    
    MSEI ("MseI", new String[]{"TAA"}),
    
    SAU3AI ("Sau3AI", new String[]{"GATC"}),
    
    RBSTA ("RBSTA", new String[]{"TA"}),
    
    RBSCG ("RBSCG", new String[]{"CG"}),
    
    NSPI ("NspI", new String[]{"CATGT", "CATGC"}),
    
    AVAII ("AvaII", new String[]{"GACC", "GTCC"}),
    
    NAN ("NA", new String[]{""});
    
    private final String name;
    private final String[] initialCutSiteRemnant;
    
    /**
     * creates the enzyme
     * @param name String | the name of the enzyme
     * @param initialCutSite String[] | the cutsites
     */
    private EnzymeEnum(String name, String[] initialCutSite){
        this.name = name;
        this.initialCutSiteRemnant = initialCutSite;
    }
    
    /**
     * 
     * @return String the name of the enzyme
     */
    public String getName(){
        return this.name;
    }
        
    /**
     * returns all cutsites
     * @return Collection of String | all the cutsites of the enzyme in a collection
     */
    public Collection<String> getInitialCutSiteRemnant(){
        HashSet<String> cutsitesSet = new HashSet();
        cutsitesSet.addAll(Arrays.asList(this.initialCutSiteRemnant));
        return cutsitesSet;
    }
    
    /**
     * returns all complement cutsites
     * @return Collection of String | all the complement cutsites of the enzyme in a collection
     */
    public Collection<String> getComplementCutSiteRemnant() {
        HashSet<String> cutsitesSet = new HashSet();
        for (String cutsite : this.initialCutSiteRemnant){
            cutsitesSet.add(BasePair.getComplementSequence(cutsite));
        }
        return cutsitesSet;
    }
    
    
    /**
     * 
     * @param enzymeName | String the name of the enzyme
     * @return EnzymeEnum the corresponding EnzymeEnum or null if no enzyme is known
     */
    public static EnzymeEnum getEnzyme(String enzymeName){
        for (EnzymeEnum e : EnzymeEnum.values()){
            if (enzymeName.toLowerCase().equals(e.getName().toLowerCase())){
                return e;
            }
        }
        return null;
    }

    
} public class FastqPairBufferedReader {
    
    private FastqBufferedReader fastqBufferedReader1;
    private FastqBufferedReader fastqBufferedReader2;
    private ReentrantLock lock = new ReentrantLock();
    
    /**
     * creates 2 new FastqBufferedReader of the given files.
     * @param fileRead1 | the file for read 1
     * @param fileRead2 | the file for read 2
     * @param ziped | boolean if the fastq file is ziped (gz)
     * @throws FileNotFoundException | if the given file isn't found
     * @throws IOException | if an error occures while opening the file
     */
    public FastqPairBufferedReader(File fileRead1, File fileRead2, boolean ziped) throws FileNotFoundException, IOException{
        this.fastqBufferedReader1 = new FastqBufferedReader(fileRead1, ziped);
        this.fastqBufferedReader2 = new FastqBufferedReader(fileRead2, ziped);
    }
    
    /**
     * reads the next lines in the fastq file. and returns it as an fastqread
     * @return null if there are no fastq files anymore, else a FastqRead
     * @throws IOException | if any error occures while reading the file
     */
    public HashMap<String, FastqRead> next() throws IOException{
        HashMap<String, FastqRead> tmpMap = new HashMap();
        try{
            lock.lock();
            tmpMap.put("r1", this.fastqBufferedReader1.next());
            tmpMap.put("r2", this.fastqBufferedReader2.next());
        }finally{
            lock.unlock();
        }
        return tmpMap;
    }  
    
    /**
     * closes this buffered reader
     * @throws IOException 
     */
    public void close() throws IOException{
        try{
            lock.lock();
            this.fastqBufferedReader1.close();
            this.fastqBufferedReader2.close();
        }finally{
            lock.unlock();
        }
    }
    
    
    /**
     * closes first the file
     * then execute finilize()
     * @throws Throwable
     */
    @Override
    public void finalize() throws Throwable{
        try {
            this.close();
        } catch (IOException ex) {
            Logger.getLogger(FastqBufferedReader.class.getName()).log(Level.SEVERE, null, ex);
        }finally{
            super.finalize();
        }
    }
} public class FastqPairBufferedWriter {
    private FastqBufferedWriter fastqBufferedWriter1;
    private FastqBufferedWriter fastqBufferedWriter2;
    private ReentrantLock lock = new ReentrantLock();
    
    /**
     * creates 2 new FastqBufferedWriter of the given files.
     * @param fileRead1 | the file for read 1
     * @param fileRead2 | the file for read 2
     * @param ziped | boolean if the fastq file is ziped (gz)
     * @throws FileNotFoundException | if the given file isn't found
     * @throws IOException | if an error occures while opening the file
     */
    public FastqPairBufferedWriter(File fileRead1, File fileRead2, boolean ziped) throws FileNotFoundException, IOException{
        this.fastqBufferedWriter1 = new FastqBufferedWriter(fileRead1, ziped);
        this.fastqBufferedWriter2 = new FastqBufferedWriter(fileRead2, ziped);
    }
    
    /**
     * reads the next lines in the fastq file. and returns it as an fastqread
     * @return null if there are no fastq files anymore, else a FastqRead
     * @throws IOException | if any error occures while reading the file
     */
    public void write(FastqRead fastqRead1, FastqRead fastqRead2) throws IOException{
        try{
            lock.lock();
            this.fastqBufferedWriter1.write(fastqRead1);
            this.fastqBufferedWriter2.write(fastqRead2);
        }finally{
            lock.unlock();
        }
    }  
    
    /**
     * closes this buffered reader
     * @throws IOException 
     */
    public void close() throws IOException{
        try{
            lock.lock();
            this.fastqBufferedWriter1.close();
            this.fastqBufferedWriter2.close();
        }finally{
            lock.unlock();
        }
    }
    
    
    /**
     * closes first the file
     * then execute finilize()
     * @throws Throwable
     */
    @Override
    public void finalize() throws Throwable{
        try {
            this.close();
        } catch (IOException ex) {
            Logger.getLogger(FastqBufferedReader.class.getName()).log(Level.SEVERE, null, ex);
        }finally{
            super.finalize();
        }
    }
} public class FastqRead {
    
    HashMap<FastqParts, String> parts;
    FastqScores fastqScore = FastqScores.getStandard();
    
    /**
     * creates a new empty read
     */
    private FastqRead(){
        this.parts = new HashMap();
    }
    
    /**
     * creates a new read with only a description
     * @param description String | the description of the fastq read
     */
    private FastqRead(String description){
        this.parts = new HashMap();
        if (! description.startsWith("@")){
            description = "@" + description;
        }
        this.parts.put(FastqParts.DESCRIPTION, description);
    }
    
    /**
     * creates a new complete fastq read
     * @param description String | the description of the fastq read
     * @param sequence String | the sequence of the read
     * @param quality String | the quality of the sequence
     */
    public FastqRead(String description, String sequence, String quality){
        this.parts = new HashMap();
        if (! description.startsWith("@")){
            description = "@" + description;
        }
        this.parts.put(FastqParts.DESCRIPTION, description);
        this.parts.put(FastqParts.SEQUENCE, sequence);
        this.parts.put(FastqParts.QUALITY, quality);
    }
    
    /**
     * creates a new fastq read based on the old fastq map with fastq parts system
     * @param fastqMap 
     */
    public FastqRead(HashMap<FastqParts, String> fastqMap){
        this.parts = new HashMap(fastqMap);
    }
    
    /**
     * @param score FastqScores
     * creates a new empty read
     */
    private FastqRead(FastqScores score){
        this.parts = new HashMap();
        this.fastqScore = score;
    }
    
    /**
     * creates a new read with only a description
     * @param description String | the description of the fastq read
     * @param score FastqScores
     */
    private FastqRead(String description, FastqScores score){
        this.parts = new HashMap();
        if (! description.startsWith("@")){
            description = "@" + description;
        }
        this.parts.put(FastqParts.DESCRIPTION, description);
        this.fastqScore = score;
    }
    
    /**
     * creates a new complete fastq read
     * @param description String | the description of the fastq read
     * @param sequence String | the sequence of the read
     * @param quality String | the quality of the sequence
     * @param score FastqScore
     */
    public FastqRead(String description, String sequence, String quality, FastqScores score){
        this.parts = new HashMap();
        if (! description.startsWith("@")){
            description = "@" + description;
        }
        this.parts.put(FastqParts.DESCRIPTION, description);
        this.parts.put(FastqParts.SEQUENCE, sequence);
        this.parts.put(FastqParts.QUALITY, quality);
        this.fastqScore = score;
    }
    
    /**
     * creates a new fastq read based on the old fastq map with fastq parts system
     * @param fastqMap 
     * @param score
     */
    public FastqRead(HashMap<FastqParts, String> fastqMap, FastqScores score){
        this.parts = new HashMap(fastqMap);
        this.fastqScore = score;
    }
    
    /**
     * 
     * @return String | the sequence
     */
    public String getSequence(){
        return this.parts.get(FastqParts.SEQUENCE);
    }
    
    /**
     * 
     * @return String | the description 
     */
    public String getDescription(){
        return this.parts.get(FastqParts.DESCRIPTION);
    }
    
    /**
     * 
     * @return String | the quality
     */
    public String getQuality(){
        return this.parts.get(FastqParts.QUALITY);
    }
    
    /**
     * set the given sequence as new
     * @param newSequence String | may not be null or empty
     */
    private void setSequence(String newSequence){
        if (newSequence != null || ! newSequence.isEmpty()){
            this.parts.put(FastqParts.SEQUENCE, newSequence);
        }
    }
    
    /**
     * set the given quality as new
     * @param newQuality String | may not be null or empty
     */
    private void setQuality(String newQuality){
        if (newQuality != null || ! newQuality.isEmpty()){
            this.parts.put(FastqParts.QUALITY, newQuality);
        }
    }
    
    /**
     * set the givend description as new
     * @param newDescription String | may not be null or empty
     */
    private void setDescription(String newDescription){
        if (newDescription != null || newDescription.isEmpty()){
            if (! newDescription.startsWith("@")){
                newDescription = "@" + newDescription;
            }
            this.parts.put(FastqParts.DESCRIPTION, newDescription);
        }
    }
    
    /**
     * this method trims at the left of both sequence and quality
     * the given distance is the index of the first base to keep (first is 0)
     * @param distance int | the first base to keep
     */
    public void ltrim(int distance){
        this.parts.put(FastqParts.SEQUENCE, this.parts.get(FastqParts.SEQUENCE).substring(distance));
        this.parts.put(FastqParts.QUALITY, this.parts.get(FastqParts.QUALITY).substring(distance));
    }
    
    /**
     * this method trims at the right of both sequence and quality
     * the given distance is the index of the last base exclusive (first is 0)
     * @param distance int | the first base to delete
     */
    public void rtrim(int distance){
        this.parts.put(FastqParts.SEQUENCE, this.parts.get(FastqParts.SEQUENCE).substring(0, distance));
        this.parts.put(FastqParts.QUALITY, this.parts.get(FastqParts.QUALITY).substring(0, distance));
    }
    
    
} public class Sample implements Comparable<Sample> {
    
    private final String sampleID;
    private final Enzyme enzyme;
    private final Enzyme enzyme2;
    private final String barcode;
    private final String barcode2;
    private final String complementBarcode;
    private final HashSet<String> possibleEnzymeSites;
    private final HashSet<String> possibleEnzyme2Sites;
    private int barcodeMismatches;
    
    /**
     * 
     * @param sampleID String | the name of the sample
     * @param enzyme Enzyme | the enzyme used
     * @param barcode String | the barcode used
     */
    public Sample(String sampleID, Enzyme enzyme, String barcode){
        //sample ID
        this.sampleID = sampleID;
        //the used enzyme for this sample
        this.enzyme = enzyme;
        //the used second enzyme for this sample (for double digest)
        this.enzyme2 = enzyme;
        //the used barcode for this sample
        this.barcode = barcode;
        //ths used second barcode (here not used)
        this.barcode2 = null;
        //make the complement of the barcode
        this.complementBarcode = this.complementDNA(barcode);
        //saving the enzyme cut sites and the complement
        this.possibleEnzymeSites = new HashSet();
        for (String enzymeSite : enzyme.getInitialCutSiteRemnant()){
            this.possibleEnzymeSites.add(enzymeSite);
            this.possibleEnzymeSites.add(this.complementDNA(enzymeSite));
        }
        this.possibleEnzyme2Sites = new HashSet();
        for (String enzymeSite : enzyme.getInitialCutSiteRemnant()){
            this.possibleEnzyme2Sites.add(enzymeSite);
            this.possibleEnzyme2Sites.add(this.complementDNA(enzymeSite));
        }
        //save the mismatches of the barcode
        this.barcodeMismatches = -1;
    }
    
    /**
     * 
     * @param sampleID String | the name of the sample
     * @param enzyme Enzyme | the enzyme used
     * @param barcode String | the barcode used
     * @param barcode2 String | the second barcode used
     * @param barcodeMismatches int | the possible mismatches for the barcode for this sample
     */
    public Sample(String sampleID, Enzyme enzyme, Enzyme enzyme2, String barcode, String barcode2, int barcodeMismatches){
        //sample ID
        this.sampleID = sampleID;
        //the used enzyme for this sample
        this.enzyme = enzyme;
        //the used second enzyme for this sample (for double digest)
        this.enzyme2 = enzyme2;
        //the used barcode for this sample
        this.barcode = barcode;
        //ths used second barcode
        this.barcode2 = barcode2;
        //make the complement of the barcode
        this.complementBarcode = this.complementDNA(barcode);
        //saving the enzyme cut sites and the complement
        this.possibleEnzymeSites = new HashSet();
        for (String enzymeSite : enzyme.getInitialCutSiteRemnant()){
            this.possibleEnzymeSites.add(enzymeSite);
            this.possibleEnzymeSites.add(this.complementDNA(enzymeSite));
        }
        this.possibleEnzyme2Sites = new HashSet();
        for (String enzymeSite : enzyme2.getInitialCutSiteRemnant()){
            this.possibleEnzyme2Sites.add(enzymeSite);
            this.possibleEnzyme2Sites.add(this.complementDNA(enzymeSite));
        }
        //save the mismatches of the barcode
        this.barcodeMismatches = barcodeMismatches;
    }
    
    /**
     * 
     * @param sampleID String | the name of the sample
     * @param enzyme Enzyme | the enzyme used
     * @param barcode String | the barcode used
     * @param barcode2 String | the second barcode used
     */
    public Sample(String sampleID, Enzyme enzyme, Enzyme enzyme2, String barcode, String barcode2){
        //sample ID
        this.sampleID = sampleID;
        //the used enzyme for this sample
        this.enzyme = enzyme;
        //the used second enzyme for this sample (for double digest)
        this.enzyme2 = enzyme2;
        //the used barcode for this sample
        this.barcode = barcode;
        //ths used second barcode
        this.barcode2 = barcode2;
        //make the complement of the barcode
        this.complementBarcode = this.complementDNA(barcode);
        //saving the enzyme cut sites and the complement
        this.possibleEnzymeSites = new HashSet();
        for (String enzymeSite : enzyme.getInitialCutSiteRemnant()){
            this.possibleEnzymeSites.add(enzymeSite);
            this.possibleEnzymeSites.add(this.complementDNA(enzymeSite));
        }
        this.possibleEnzyme2Sites = new HashSet();
        for (String enzymeSite : enzyme2.getInitialCutSiteRemnant()){
            this.possibleEnzyme2Sites.add(enzymeSite);
            this.possibleEnzyme2Sites.add(this.complementDNA(enzymeSite));
        }
        //save the mismatches of the barcode
        this.barcodeMismatches = -1;
    }
    
    /**
     * 
     * @return String the id of the sample
     */
    public String getSampleID(){
        return this.sampleID;
    }
    
    /**
     * 
     * @return Enzyme | the enzyme of this sample 
     */
    public Enzyme getEnzyme(){
        return this.enzyme;
    }
    
    
    /**
     * 
     * @return Enzyme | the enzyme second of this sample 
     */
    public Enzyme getEnzyme2(){
        return this.enzyme2;
    }
    
    /**
     * 
     * @return String | the name of the enzyme
     */
    public String getEnzymeName(){
        return this.enzyme.getName();
    }
    
    
    /**
     * 
     * @return String | the name of the second enzyme
     */
    public String getEnzyme2Name(){
        return this.enzyme2.getName();
    }
    
    /**
     * 
     * @return Collection of String | the remains of the cutsite after the cut (place found in the DNA afther cut)
     * @see Enzyme#getInitialCutSiteRemnant() 
     */
    private Collection<String> getEnzymeCutSites(){
        return this.enzyme.getInitialCutSiteRemnant();
    }
    
    /**
     * 
     * @return Collection of String | the remains of the cutsite after the cut (place found in the DNA afther cut) of the second enzyme
     * @see Enzyme#getInitialCutSiteRemnant() 
     */
    private Collection<String> getEnzyme2CutSites(){
        return this.enzyme2.getInitialCutSiteRemnant();
    }
    
    /**
     * 
     * @return HashSet of String | all remains of the cutsite after the cut AND the complements of this site
     */
    private HashSet<String> getAllPossibleEnzymeCutSites(){
        return this.possibleEnzymeSites;
    }
    
    
    /**
     * 
     * @return HashSet of String | all remains of the cutsite after the cut AND the complements of this site
     */
    private HashSet<String> getAllPossibleEnzyme2CutSites(){
        return this.possibleEnzyme2Sites;
    }
    
    /**
     * 
     * @return the length of the first found enzyme site
     */
    public int getPossibleEnzymeCutSiteLength(){
        for (String site : this.getAllPossibleEnzymeCutSites()){
            return site.length();
        }
        return 0;
    }
    
    /**
     * 
     * @return the length of the first found enzyme site
     */
    public int getPossibleEnzyme2CutSiteLength(){
        for (String site : this.getAllPossibleEnzyme2CutSites()){
            return site.length();
        }
        return 0;
    }
    
    /**
     * 
     * @return String | the barcode used to identify the sample 
     */
    public String getBarcode(){
        return this.barcode;
    }
    
    /**
     * 
     * @return String | the second barcode used to identify the sample 
     */
    public String getBarcodeSecond(){
        return this.barcode2;
    }
    
    /**
     * 
     * @return boolean | true if there are 2 barcodes (on both ends), else false
     */
    public boolean has2barcodes(){
        return this.barcode2 != null;
    }
    
    /**
     * 
     * @return String | the complement of the barcode used to identify the sample
     */
    public String getComplementBarcode(){
        return this.complementBarcode;
    }
    
    /**
     * returns the mismatches the barcode can have, or -1 when the sample doesn't know how many mismatches (can be standard, or user defined in parameter)
     * @return int | the mismatches of the barcode
     */
    public int getBarcodeMismatches(){
        return this.barcodeMismatches;
    }
    
    
    /**
     * creates the complement of the given DNA
     * @param DNApiece String | the piece of DNA where the complement must be searched
     * @return the complement of the given DNA
     * @see BasePair#getComplementSequence(java.lang.String) 
     */
    private String complementDNA(String DNApiece){
        return BasePair.getComplementSequence(DNApiece);
    }
    
    /**
     * return true if the given barcode is valid.
     * <br> valid barcodes only exists of A, T, G and/or C
     * @param candidateBarcode String | the barcode to validate
     * @return true if the given candidate barcode is a valid barcode, false otherwise
     */
    public static boolean isValidBarcode(String candidateBarcode){
        Pattern pattern = Pattern.compile("[ATGCatgc]+");
        Matcher matcher = pattern.matcher(candidateBarcode);
        return matcher.matches();
    }

    /**
     * returns 0 if getSampleID == sample.getSampleID
     * returns positive int if this sampleID is alphabeticaly before the sample's sampleID
     * returns negative int if this sampleID is alphabeticly after the sample's sampleID
     * @param sample Sample | the sample to compare to
     * @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.
     * @see String#compareTo(java.lang.String) 
     */
    public int compareTo(Sample sample) {
        return this.getSampleID().compareTo(sample.getSampleID());
    }
    
}
import be.uzleuven.gc.logistics.GBSX.GBSX;
import be.uzleuven.gc.logistics.GBSX.barcodeCorrector.application.BarcodeCorrectingAlgorithm;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.GBSdemultiplex;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.application.distanceAlgorithms.FindingDistanceAlgorithm;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.application.distanceAlgorithms.MismatchIndelDistance;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.exceptions.ErrorInLogException;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.exceptions.InvalidReadEnum;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.exceptions.InvalidReadException;
import be.uzleuven.gc.logistics.GBSX.utils.exceptions.StopExcecutionException;
import be.uzleuven.gc.logistics.GBSX.utils.enzyme.infrastructure.EnzymeFileParser;
import be.uzleuven.gc.logistics.GBSX.utils.fastq.infrastructure.FastqBufferedReader;
import be.uzleuven.gc.logistics.GBSX.utils.fastq.infrastructure.FastqBufferedWriter;
import be.uzleuven.gc.logistics.GBSX.utils.sampleBarcodeEnzyme.infrastructure.InfoFileParser;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.infrastructure.fileInteractors.LoggerFile;
import be.uzleuven.gc.logistics.GBSX.utils.sampleBarcodeEnzyme.model.BasePair;
import be.uzleuven.gc.logistics.GBSX.utils.enzyme.model.Enzyme;
import be.uzleuven.gc.logistics.GBSX.utils.fastq.model.FastqParts;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.model.DemultiplexArguments;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.model.DemultiplexParameters;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.model.ProcessedFragment;
import be.uzleuven.gc.logistics.GBSX.demultiplexer.model.SampleBarcodeCombination;
import be.uzleuven.gc.logistics.GBSX.utils.enzyme.model.EnzymeComparator;
import be.uzleuven.gc.logistics.GBSX.utils.enzyme.model.EnzymeEnum;
import be.uzleuven.gc.logistics.GBSX.utils.fastq.infrastructure.FastqPairBufferedReader;
import be.uzleuven.gc.logistics.GBSX.utils.fastq.infrastructure.FastqPairBufferedWriter;
import be.uzleuven.gc.logistics.GBSX.utils.fastq.model.FastqRead;
import be.uzleuven.gc.logistics.GBSX.utils.sampleBarcodeEnzyme.model.Sample;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
/*
 * This is GBSX v1.0. A toolkit for experimental design and demultiplexing genotyping by sequencing experiments. \n *  \n * Copyright 2014 KU Leuven
 * 
 * 
 * This file is part of GBSX.
 *
 * GBSX is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * GBSX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with GBSX.  If not, see <http://www.gnu.org/licenses/>.
 */
package be.uzleuven.gc.logistics.GBSX.demultiplexer.application;



/**
 *
 * @author Koen Herten for the KU Leuven
 */
public final class FastqDemultiplex {
    
    private final int MAXIMUM_DISTANCE_BETWEEN_START_AND_BARCODE = 20;
    private final int PROCESS_REPORTING_INTERVAL = 100000;
    
    private ArrayList<Sample> sampleList;
    private int longestBarcodeLength;
    private final LoggerFile loggerFile;
    /**
     * correction log is only used in the correction of the pair end demultiplex
     */
    private final CorrectionLog correctionLog;
    
    private final DemultiplexParameters parameters;
    private final FindingDistanceAlgorithm findingDistanceAlgorithm;
    
    /**
     * this makes the bases for the demultiplexing. The given array contains all the needed arguments.
     * <br> If not, there will be thrown exceptions like IllegalArgumentException or RunTimeException. This will end the program and the JVM on a correct way
     * @param args String[] | all the arguments needed for the excecution. 
     * @throws be.uzleuven.gc.logistics.GBSX.utils.exceptions.StopExcecutionException 
     */
    public FastqDemultiplex(String[] args) throws StopExcecutionException{