package de.mpc.pia.modeller;

import java.io.FileWriter;
import java.io.IOException;
import java.io.Serializable;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import de.mpc.pia.modeller.score.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.biojava.nbio.ontology.Term;
import org.biojava.nbio.ontology.Triple;

import de.mpc.pia.JsonAnalysis;
import de.mpc.pia.intermediate.Group;
import de.mpc.pia.intermediate.PIAInputFile;
import de.mpc.pia.intermediate.Peptide;
import de.mpc.pia.intermediate.PeptideSpectrumMatch;
import de.mpc.pia.intermediate.xmlhandler.PIAIntermediateJAXBHandler;
import de.mpc.pia.modeller.psm.PSMReportItem;
import de.mpc.pia.modeller.psm.PSMReportItemComparator;
import de.mpc.pia.modeller.psm.ReportPSM;
import de.mpc.pia.modeller.psm.ReportPSMSet;
import de.mpc.pia.modeller.report.SortOrder;
import de.mpc.pia.modeller.report.filter.AbstractFilter;
import de.mpc.pia.modeller.report.filter.FilterComparator;
import de.mpc.pia.modeller.report.filter.FilterFactory;
import de.mpc.pia.modeller.report.filter.impl.PSMTopIdentificationFilter;
import de.mpc.pia.modeller.score.FDRData.DecoyStrategy;
import de.mpc.pia.modeller.score.comparator.RankCalculator;
import de.mpc.pia.modeller.score.comparator.ScoreComparator;
import de.mpc.pia.tools.OntologyConstants;
import de.mpc.pia.tools.PIAConstants;
import de.mpc.pia.tools.PIATools;
import de.mpc.pia.tools.obo.AbstractOBOMapper;
import de.mpc.pia.tools.obo.OBOMapper;


/**
 * Modeller for the PSM related stuff.
 *
 * @author julianu
 *
 */
public class PSMModeller implements Serializable {

    private static final long serialVersionUID = 5958133586889325193L;


    /** logger for this class */
    private static final Logger LOGGER = LogManager.getLogger();


    /** maps from the fileID to the {@link PIAInputFile}s, they are straight from the intermediateHandler */
    private Map<Long, PIAInputFile> inputFiles;

    /** the name of the PIA XML file */
    private String fileName;

    /** maps from the spectrum ID in the PIA intermediate file to the report PSM */
    private Map<Long, ReportPSM> spectraPSMs;

    /** maps from the file ID to the List of {@link ReportPSM}s */
    private Map<Long, List<ReportPSM> > fileReportPSMs;

    /** List of the {@link ReportPSMSet}s for the whole file */
    // TODO: make the reportPSMSets and reportPSMSetMap accessible by getter only, create on demand and null them, when the PSMSetSettings or createPSMSets are changed
    private List<ReportPSMSet> reportPSMSets;

    /** map of the ReportPSMSets, for faster access in the other modellers, this is static per PIA XML file and global settings */
    private Map<String, ReportPSMSet> reportPSMSetMap;


    /** maps from the fileID to List of short names  of the scores. Actually only a reference to the same field in the {@link PIAModeller}. */
    private Map<Long, List<String>> fileScoreShortNames;

    /** mapping from fileId to the available sortings */
    private Map<Long, Set<String>> fileSortables;

    /** maps from the fileID to the corresponding FDR data */
    private Map<Long, FDRData> fileFDRData;

    /** maps from the fileID to whether an FDR is calculated or not */
    private Map<Long, Boolean> fileFDRCalculated;

    /** map from the file's id to the used top identifications for FDR calculation */
    private Map<Long, Integer> fileTopIdentifications;

    /** Maps, whether the file has PSMs with decoy information from the searchengine or otherwise in the PIA XML defined decoys or not */
    private Map<Long, Boolean> fileHasInternalDecoy;

    /** The warnings generated by the {@link PIAIntermediateJAXBHandler} for the PSMSetSettings */
    private Map<String, Set<Long>> psmSetSettingsWarnings;

    /** The represented {@link IdentificationKeySettings} in this Set are used for the calculation of PSM sets or not */
    private Map<String, Boolean> psmSetSettings;

    /** whether to create PSM sets at all or use every PSM independently (useful for data sets with several runs and one search engine) */
    private boolean createPSMSets;

    /** a list of score shortnames, representing the preferred score for the FDR calulation (if it is not manually set) */
    private List<String> preferredFDRScores;


    /** the OBO mapper, to get additional data */
    private transient OBOMapper oboMapper = null;

    /** maps from the scoreShort to the scoreName */
    private Map<String, String> scoreShortToScoreName;

    /** maps from the scoreShort to the comparator */
    private HashMap<String, Comparator<PSMReportItem>> scoreShortToComparator;

    /** maps from the score short to whether the higher score is better (probably current setting) */
    private Map<String, Boolean> scoreShortToHigherScoreBetter;

    /** maps from the score short to whether the higher score better is changeable, because it was not hard coded or found in a CV */
    private Map<String, Boolean> scoreShortToHigherScoreBetterChangeable;


    /** default decoy pattern */
    private String defaultDecoyPattern;

    /** default FDR threshold */
    private Double defaultFDRThreshold;

    /** default for the number of highest ranking PSMs (per spectrum) used for FDR calculation */
    private Integer defaultFDRTopIdentifications;


    /** the list of filters applied to the data, mapped by the file ID */
    private Map<Long, List<AbstractFilter>> fileFiltersMap;


    /**
     * Basic constructor, creates the {@link ReportPSM}s and
     * {@link ReportPSMSet}s from the given {@link Group}s. The {@link Group}s
     * should derive from a {@link PIAInputFile}.
     *
     * @param groups groups of the PIA intermediate file
     * @param inputFiles the used {@link PIAInputFile}s
     */
    public PSMModeller(Map<Long, Group> groups,
            Map<Long, PIAInputFile> inputFiles,
            String fileName,
            Map<String, Set<Long>> psmSetSettingsWarnings,
            int nrPSMs) {

        // create the file mapping and also add the overview file with ID 0
        this.inputFiles = new HashMap<>(inputFiles.size() + 1);
        this.inputFiles.put(0L, new PIAInputFile(0L, "All files",
                "Overview_of_all_files", "none"));
        this.inputFiles.putAll(inputFiles);

        fileFiltersMap = new HashMap<>(inputFiles.size());

        this.fileName = fileName;

        this.scoreShortToScoreName = new HashMap<>();

        // TODO: get defaults from ini-file (or something like that)
        defaultDecoyPattern = "s.*";
        defaultFDRThreshold = 0.01;
        defaultFDRTopIdentifications = 0;
        preferredFDRScores = new ArrayList<>();

        // initialize the used PSM set settings
        this.psmSetSettingsWarnings = psmSetSettingsWarnings;
        this.psmSetSettings = getMaximalPSMSetSettings();

        // remove redundant psmSetSettings (and use only the more failure tolerant ones)
        psmSetSettings = IdentificationKeySettings.noRedundantSettings(psmSetSettings);

        // by default create PSM sets (and therefore remove fileID for set identifications)
        this.createPSMSets = true;
        this.psmSetSettings.remove(IdentificationKeySettings.FILE_ID.toString());

        // no settings are needed for the calculation of the ReportPSMs, but the PSM Set settings are used
        createReportPSMsFromGroups(groups, nrPSMs);
    }


    /**
     * Applies the general settings and recalculates the PSMSets
     */
    public void applyGeneralSettings(boolean createSets) {
        createPSMSets = createSets;

        if (!createPSMSets) {
            // no sets across files needed -> put fileID into settings
            this.psmSetSettings.put(IdentificationKeySettings.FILE_ID.name(), true);
        }

        // rebuild the PSM sets
        List<AbstractFilter> filters = getFilters(0L);

        // map to create the PSMSets
        Map<String, List<ReportPSM>> psmSetsMap =
                new HashMap<>();

        // sort the PSMs in sets with their identificationKeys
        // put the PSM in the psmKey -> ReportPSM map
        spectraPSMs.values().stream().filter(psm -> FilterFactory.satisfiesFilterList(psm, 0L, filters)).forEach(psm -> {
            String psmKey = psm.getIdentificationKey(this.psmSetSettings);

            // put the PSM in the psmKey -> ReportPSM map
            if (!psmSetsMap.containsKey(psmKey)) {
                psmSetsMap.put(psmKey, new ArrayList<>());
            }
            psmSetsMap.get(psmKey).add(psm);
        });

        createReportPSMSets(psmSetsMap);
    }


    /**
     * Gets whether PSM sets should be used across files
     * @return
     */
    public Boolean getCreatePSMSets() {
        return createPSMSets;
    }


    /**
     * Getter for the {@link IdentificationKeySettings}.
     * @return
     */
    public Map<String, Boolean> getPSMSetSettings() {
        return psmSetSettings;
    }


    /**
     * Setter for the {@link IdentificationKeySettings}.
     *
     * @param newSettings A mapping from the
     * @return Returns the settings which were active before changing to the new
     * settings
     */
    public Map<String, Boolean> setPSMSetSettings(Map<String, Boolean> newSettings) {
        Map<String, Boolean> oldSettings = psmSetSettings;
        psmSetSettings = newSettings;

        // if no sets should be created, add the fileID to the settings
        if (!this.createPSMSets) {
            this.psmSetSettings.put(IdentificationKeySettings.FILE_ID.name(), true);
        }

        if (!psmSetSettings.equals(oldSettings)) {
            LOGGER.info("need to re-apply general settings");
            applyGeneralSettings(getCreatePSMSets());
        }

        return oldSettings;
    }


    /**
     * Returns the maximal set of (redundant) PSMSetSettings to combine PSMs of
     * all input files.
     *
     * @return
     */
    public Map<String, Boolean> getMaximalPSMSetSettings() {
        Map<String, Boolean> settings = new HashMap<>(IdentificationKeySettings.values().length);

        for (IdentificationKeySettings setting : IdentificationKeySettings.values()) {
            if ((getPSMSetSettingsWarnings().get(setting.toString()) != null) &&
                    !getPSMSetSettingsWarnings().get(setting.toString()).isEmpty()) {
                settings.put(setting.toString(), false);
            } else {
                settings.put(setting.toString(), true);
            }
        }

        return settings;
    }


    /**
     * Getter for the PSMSetSettingsWarnings
     * @return
     */
    public Map<String, Set<Long>> getPSMSetSettingsWarnings() {
        return psmSetSettingsWarnings;
    }


    /**
     * This method creates for each {@link PeptideSpectrumMatch} in the
     * given Map of {@link Group}s the corresponding {@link ReportPSM} and the
     * List of {@link ReportPSMSet}s for the overview.
     *
     * TODO: this could be run threaded (by PIA clusters)!
     *
     * @return a mapping from the spectrum ID to the ReportPSM
     */
    private void createReportPSMsFromGroups(Map<Long, Group> groups, int nrAllPSMs) {
        LOGGER.info("createReportPSMsFromGroups started...");

        Integer psmsPerFile = nrAllPSMs / (inputFiles.size()-1);

        // reset the PSMs
        spectraPSMs = new HashMap<>();
        fileReportPSMs = new HashMap<>();

        // reset the scores
        fileScoreShortNames = new HashMap<>();

        // reset the available sortings
        fileSortables = new HashMap<>();
        fileSortables.put(0L, new HashSet<>());

        // reset the FDR data
        fileFDRData = new HashMap<>();
        fileFDRCalculated = new HashMap<>();

        // reset the FDR data for the overview
        fileFDRData.put(0L, new FDRData(FDRData.DecoyStrategy.ACCESSIONPATTERN,
                defaultDecoyPattern, defaultFDRThreshold));
        fileFDRCalculated.put(0L, false);

        // reset the value for used top identifications in FDR calculation
        fileTopIdentifications = new HashMap<>();

        // reset internal decoy knowledge
        fileHasInternalDecoy = new HashMap<>(inputFiles.size());
        inputFiles.keySet().stream().filter(fileID -> fileID > 0).forEach(fileID -> fileHasInternalDecoy.put(fileID, false));

        scoreShortToComparator = new HashMap<>();

        scoreShortToHigherScoreBetter = new HashMap<>();

        scoreShortToHigherScoreBetterChangeable = new HashMap<>();

        // map to create the PSMSets
        Map<String, List<ReportPSM>> psmSetsMap =
                new HashMap<>();

        // this map is used, to get the identification ranking for each score of a PSMs
        //  fileID    spectrumID  scoreShort        psm
        Map<Long, Map<String, Map<String, ArrayList<ReportPSM>>>> fileToRankings =
                new HashMap<>();


        // iterate through the groups
        long nrPSMs = 0;
        for (Map.Entry<Long, Group> groupIt : groups.entrySet()) {
            Map<String, Peptide> peptides = groupIt.getValue().getPeptides();

            // only groups with peptides can have PSMs
            if ((peptides != null) && (peptides.size() > 0)) {
                for (Map.Entry<String, Peptide> pepIt : peptides.entrySet()) {
                    List<PeptideSpectrumMatch> spectra = pepIt.getValue().getSpectra();
                    if (spectra != null) {
                        for (PeptideSpectrumMatch spec : spectra) {
                            ReportPSM psm = new ReportPSM(spec.getID(), spec);
                            Long fileID = spec.getFile().getID();
                            String psmKey = spec.getIdentificationKey(psmSetSettings);

                            // add the accessions
                            // only add accession, if it was found in the spectrum's file
                            groupIt.getValue().getAllAccessions().values().stream().filter(acc -> acc.foundInFile(fileID)).forEach(psm::addAccession);

                            if (spectraPSMs.put(spec.getID(), psm) != null) {
                                // TODO: better warning
                                LOGGER.warn("psm with ID '{}' already in map", spec.getID());
                            }


                            // put the PSM in the fileID -> ReportPSMs mapping
                            List<ReportPSM> filesPSMList = fileReportPSMs.get(fileID);
                            if (filesPSMList == null) {
                                filesPSMList = new ArrayList<>(psmsPerFile);
                                fileReportPSMs.put(fileID, filesPSMList);

                                // this file is new, so also add the scoreName-Maps and sorting maps
                                fileScoreShortNames.put(fileID, new ArrayList<>());

                                fileSortables.put(fileID, new HashSet<>());

                                // also re-initialise the FDR data
                                fileFDRData.put(fileID,
                                        new FDRData(FDRData.DecoyStrategy.ACCESSIONPATTERN,
                                                defaultDecoyPattern,
                                                defaultFDRThreshold));
                                fileFDRCalculated.put(fileID, false);

                                //and the topIdentifications
                                fileTopIdentifications.put(fileID,
                                        defaultFDRTopIdentifications);
                            }
                            filesPSMList.add(psm);

                            // also put the PSM in the psmKey -> ReportPSM map, which is needed for the creation of the ReportPSMSets
                            List<ReportPSM> psmSets = psmSetsMap.computeIfAbsent(psmKey, k -> new ArrayList<>());
                            psmSets.add(psm);


                            // record everything needed for the identification ranking
                            Map<String, Map<String, ArrayList<ReportPSM>>> spectraToPSMs =
                                    fileToRankings.computeIfAbsent(fileID, k -> new HashMap<>(psmsPerFile));


                            String psmScoreRankKey = createPSMKeyForScoreRanking(psm);
                            Map<String, ArrayList<ReportPSM>> scoreshortsToPSMs =
                                    spectraToPSMs.computeIfAbsent(psmScoreRankKey, k -> new HashMap<>());

                            for (ScoreModel score : psm.getScores()) {
                                // add the scorenames, if not yet done, and take the values for topIdentificationRanking
                                List<String> scoreShortNames = fileScoreShortNames.get(fileID);
                                if (!scoreShortNames.contains(score.getShortName())) {
                                    scoreShortNames.add(score.getShortName());

                                    if (!scoreShortToScoreName.containsKey(score.getShortName())) {
                                        scoreShortToScoreName.put(score.getShortName(),
                                                score.getName());

                                        LOGGER.debug("Added to scoremap: {} -> {}", score.getShortName(), score.getName());
                                    }

                                    // add score to the available sortings
                                    String scoreSortName =
                                            PSMReportItemComparator.getScoreSortName(score.getShortName());
                                    if (scoreSortName != null) {
                                        fileSortables.get(fileID).add(scoreSortName);
                                    }
                                }

                                // get comparators for all the PSM scores
                                if (!scoreShortToComparator.containsKey(score.getShortName())) {

                                    String scoreSortName =
                                            PSMReportItemComparator.getScoreSortName(score.getShortName());
                                    Comparator<PSMReportItem> comp;
                                    if (scoreSortName != null) {
                                        // this score is hard coded
                                        comp = PSMReportItemComparator.getComparatorByName(scoreSortName, SortOrder.ascending);

                                        scoreShortToHigherScoreBetterChangeable.put(
                                                score.getShortName(), false);

                                        scoreShortToHigherScoreBetter.put(
                                                score.getShortName(),
                                                ScoreModelEnum.getModelByDescription(score.getShortName()).higherScoreBetter());
                                    } else {
                                        Boolean higherscorebetter = null;

                                        Term oboTerm = getOBOMapper().getTerm(score.getAccession());
                                        if (oboTerm != null) {
                                            // the score is in the OBO file, get the relations etc.
                                            Set<Triple> tripleSet = getOBOMapper().getTriples(oboTerm, null, null);

                                            for (Triple triple : tripleSet) {
                                                if (triple.getPredicate().getName().equals(AbstractOBOMapper.OBO_IS_A)) {
                                                    if (triple.getObject().getName().equals(OntologyConstants.PSM_LEVEL_PVALUE.getPsiAccession())
                                                            || triple.getObject().getName().equals(OntologyConstants.PSM_LEVEL_EVALUE.getPsiAccession())
                                                            || triple.getObject().getName().equals(OntologyConstants.PSM_LEVEL_QVALUE.getPsiAccession())
                                                            || triple.getObject().getName().equals(OntologyConstants.PEPTIDE_LEVEL_QVALUE.getPsiAccession())
                                                            || triple.getObject().getName().equals(OntologyConstants.PEPTIDE_LEVEL_PVALUE.getPsiAccession())
                                                            || triple.getObject().getName().equals(OntologyConstants.PEPTIDE_LEVEL_EVALUE.getPsiAccession())) {
                                                        higherscorebetter = false;
                                                    }
                                                } else if (triple.getPredicate().getName().equals(AbstractOBOMapper.OBO_RELATIONSHIP)) {
                                                    if (triple.getObject().getName().equals(OBOMapper.OBO_HAS_ORDER_HIGHERSCOREBETTER)) {
                                                        higherscorebetter = true;
                                                    } else if (triple.getObject().getName().equals(OBOMapper.OBO_HAS_ORDER_LOWERSCOREBETTER)) {
                                                        higherscorebetter = false;
                                                    }
                                                }
                                            }
                                        }
                                        
                                        if (higherscorebetter != null) {
                                            // the status of higherScoreBetter is not to be changed by the user
                                            scoreShortToHigherScoreBetterChangeable.put(
                                                    score.getShortName(), false);
                                        } else {
                                            // the status of higherScoreBetter may be changed by the user
                                            scoreShortToHigherScoreBetterChangeable.put(
                                                    score.getShortName(), true);
                                            higherscorebetter = true;
                                        }
                                        scoreShortToHigherScoreBetter.put(
                                                score.getShortName(),
                                                higherscorebetter);
                                        comp = new ScoreComparator<>(score.getShortName(), higherscorebetter);
                                    }

                                    LOGGER.debug("adding score comparator for {}: {}", score.getShortName(), comp);

                                    scoreShortToComparator.put(score.getShortName(), comp);
                                }

                                ArrayList<ReportPSM> psmsOfSpectrum =
                                        scoreshortsToPSMs.computeIfAbsent(score.getShortName(), k -> new ArrayList<>(10));
                                psmsOfSpectrum.add(psm);
                            }


                            if (!fileHasInternalDecoy.get(fileID).booleanValue() &&
                                    (spec.getIsDecoy() != null) && spec.getIsDecoy().booleanValue()) {
                                fileHasInternalDecoy.put(fileID, true);
                            }

                            nrPSMs++;
                            if (nrPSMs % 100000 == 0) {
                                LOGGER.info("{} PSMs done", nrPSMs);
                            }
                        }
                    }
                }
            }
        }

        // now set ranks to PSMs which have a known ranking
        for (Map<String, Map<String, ArrayList<ReportPSM>>> spectraToPSM
                : fileToRankings.values()) {

            for (Map<String, ArrayList<ReportPSM>> scoreshortsToPSMs
                    : spectraToPSM.values()) {

                for (Map.Entry<String, ArrayList<ReportPSM>> scoreToPSMsIt
                        : scoreshortsToPSMs.entrySet()) {
                    String scoreShort = scoreToPSMsIt.getKey();
                    Comparator<PSMReportItem> comp = scoreShortToComparator.get(scoreShort);

                    // only sort and rank, if we know how
                    if (comp != null) {
                        (scoreToPSMsIt.getValue()).sort(comp);

                        // give the ranks to the PSMs
                        Double lastScore = null;
                        int rank = 0;
                        for (ReportPSM psm : scoreToPSMsIt.getValue()) {
                            Double thisScore =
                                    psm.getScore(scoreShort);

                            if (!thisScore.equals(lastScore)) {
                                rank++;
                            }

                            psm.setIdentificationRank(scoreShort, rank);

                            lastScore = thisScore;
                        }
                    } else {
                        // unrankable get all ranked as -1
                        for (ReportPSM psm : scoreToPSMsIt.getValue()) {
                            psm.setIdentificationRank(scoreShort, -1);
                        }
                    }
                }
            }
        }

        // create and fill the ReportPSMSets for the overview
        createReportPSMSets(psmSetsMap);

        LOGGER.info("createReportPSMsFromGroups done.");
    }


    /**
     * Getter for the oboMapper. Initializes the OBOMapper on the first call.
     * @return
     */
    private OBOMapper getOBOMapper() {
        if (oboMapper == null) {
            oboMapper = new OBOMapper();
        }
        return oboMapper;
    }


    /**
     * Creates the {@link ReportPSMSet}s, given the {@link ReportPSM}s in a
     * mapping from the PSM-identificationKeys
     * @param psmSetsMap
     */
    private void createReportPSMSets(Map<String, List<ReportPSM>> psmSetsMap) {
        reportPSMSetMap = new HashMap<>(psmSetsMap.size());

        for (Map.Entry<String, List<ReportPSM>> psmSetsIt : psmSetsMap.entrySet()) {
            ReportPSMSet psmSet =
                    new ReportPSMSet(psmSetsIt.getValue(), psmSetSettings);
            reportPSMSetMap.put(psmSetsIt.getKey(), psmSet);
        }

        reportPSMSets = new ArrayList<>(
                reportPSMSetMap.values());

        fileFDRData.put(0L,
                new FDRData(fileFDRData.get(0L).getDecoyStrategy(),
                        fileFDRData.get(0L).getDecoyPattern(),
                        fileFDRData.get(0L).getFDRThreshold()));
        LOGGER.info("createReportPSMSets done");
    }


    /**
     * Returns a key by which the PSMs can be grouped for calculation of top
     * rank identifications.
     *
     * @param psm
     * @return
     */
    private static String createPSMKeyForScoreRanking(ReportPSM psm) {
        StringBuilder sb = new StringBuilder();

        sb.append(psm.getSourceID());
        sb.append(':');
        sb.append(psm.getSpectrum().getSpectrumTitle());
        sb.append(':');
        sb.append(Double.toString(PIATools.round(psm.getMassToCharge(), PIAConstants.MASS_TO_CHARGE_PRECISION)));
        sb.append(':');
        if (psm.getRetentionTime() != null) {
            sb.append(Double.toString(PIATools.round(psm.getRetentionTime(), PIAConstants.RETENTION_TIME_PRECISION)));
        } else {
            sb.append("null");
        }

        return sb.toString();
    }


    /**
     * Getter for the files used in the PIA intermediate file, including the
     * pseudo-overview-file.
     *
     * @return
     */
    public Map<Long, PIAInputFile> getFiles() {
        return inputFiles;
    }


    /**
     * Getter for the filename of the PIA XML file.
     *
     * @return
     */
    public String getFileName() {
        return fileName;
    }


    /**
     * Getter for the default decoy pattern.
     * @return
     */
    public String getDefaultDecoyPattern() {
        return defaultDecoyPattern;
    }


    /**
     * Getter for the default FDR threshold for acceptance.
     * @return
     */
    public Double getDefaultFDRThreshold() {
        return defaultFDRThreshold;
    }


    /**
     * Getter for the default number of highest ranking PSMs (per spectrum) used
     * for FDR calculation.
     * @return
     */
    public Integer getDefaultFDRTopIdentifications() {
        return defaultFDRTopIdentifications;
    }


    /**
     * Reports the mapping from the ReportPSMSet identificationKeys to the
     * {@link ReportPSMSet}s. This map includes all possible, unfiltered PSM
     * sets.
     *
     * @return
     */
    public Map<String, ReportPSMSet> getReportPSMSets() {
        return reportPSMSetMap;
    }


    /**
     * Returns the number of PSMs or PSM sets for the given file ID.
     * @param fileID
     * @return
     */
    public int getNrReportPSMs(Long fileID) {
        if (fileID > 0) {
            if (fileReportPSMs.containsKey(fileID)) {
                return fileReportPSMs.get(fileID).size();
            } else {
                return -1;
            }
        } else {
            return reportPSMSets.size();
        }
    }


    /**
     * Returns the filtered List of {@link ReportPSM}s for the given fileID.
     *
     * @param fileID
     * @param filters
     * @return
     */
    public List<ReportPSM> getFilteredReportPSMs(Long fileID,
            List<AbstractFilter> filters) {
        if (fileReportPSMs.containsKey(fileID)) {
            return FilterFactory.applyFilters(fileReportPSMs.get(fileID),
                    filters, fileID);
        } else {
            LOGGER.error("There are no ReportPSMs for the fileID {}", fileID);
            return new ArrayList<>(1);
        }
    }


    /**
     * Returns the filtered List of {@link ReportPSMSet}s for the PSM sets,
     * applying the given filters.
     *
     * @param filters
     * @return
     */
    public List<ReportPSMSet> getFilteredReportPSMSets(
            List<AbstractFilter> filters) {
        List<ReportPSMSet> filteredPSMSets = new ArrayList<>();

        // the PSM sets need a special filtering, some of the sets can become empty, due to filters on PSM level
        reportPSMSets.stream().filter(psmSet -> FilterFactory.satisfiesFilterList(psmSet, 0L, filters)).forEach(psmSet -> {
            List<ReportPSM> psms = FilterFactory.applyFilters(psmSet.getPSMs(), filters);

            if (!psms.isEmpty()) {
                ReportPSMSet set = new ReportPSMSet(psms, psmSetSettings);
                set.copyInfo(psmSet);
                filteredPSMSets.add(set);
            }
        });

        return filteredPSMSets;
    }


    /**
     * Returns the Score name, given the scoreShortName.
     * @param shortName
     * @return
     */
    public String getScoreName(String shortName) {
        return scoreShortToScoreName.get(shortName);
    }


    /**
     * Returns, whether the "higherscorebetter" can be changed by the user for
     * this score.
     *
     * @return
     */
    public Boolean getHigherScoreBetterChangeable(String scoreShort) {
        return scoreShortToHigherScoreBetterChangeable.get(scoreShort);
    }


    /**
     * Gets whether the higherScoreBetter is true or false for the score.
     *
     * @param scoreShort
     * @return
     */
    public Boolean getHigherScoreBetter(String scoreShort) {
        return scoreShortToHigherScoreBetter.get(scoreShort);
    }


    /**
     * Sets whether the higherScoreBetter is true or false for the score.
     *
     * @param scoreShort
     */
    public void setHigherScoreBetter(String scoreShort,
            Boolean higherScoreBetter) {
        if (scoreShortToHigherScoreBetterChangeable.get(scoreShort).booleanValue()) {
            scoreShortToHigherScoreBetter.put(scoreShort, higherScoreBetter);

            scoreShortToComparator.put(scoreShort,
                    new ScoreComparator<>(
                            scoreShort, higherScoreBetter));

            LOGGER.debug("setHigherScoreBetter: {}", scoreShortToComparator.get(scoreShort));
        } else {
            LOGGER.warn("The comparator for {} ({}) may not be changed!",
            		scoreShort, scoreShortToScoreName.get(scoreShort));
        }
    }


    /**
     * Returns the comparator for the given short.
     *
     * @param scoreShort
     * @return
     */
    public Comparator<PSMReportItem> getScoreComparator(String scoreShort) {
        if (scoreShortToComparator.containsKey(scoreShort)) {
            return scoreShortToComparator.get(scoreShort);
        }

        LOGGER.warn("no comparator found for {}", scoreShort);
        return null;
    }


    /**
     * Returns for the given score, whether a higher score is better.
     *
     * @param scoreShort
     * @return
     */
    public Boolean getHigherScoreBetterForScore(String scoreShort) {
        return scoreShortToHigherScoreBetter.get(scoreShort);
    }


    /**
     * Getter for the shortNames of all scores of the given file
     *
     * @param fileID
     * @return
     */
    public List<String> getScoreShortNames(Long fileID) {
        if (fileScoreShortNames.containsKey(fileID)) {
            return fileScoreShortNames.get(fileID);
        } else {
            return new ArrayList<>(1);
        }
    }


    /**
     * Returns the mapping from the shortNames to the nicely readable names.
     *
     * @return
     */
    public Map<String, String> getScoreShortsToScoreNames() {
        return scoreShortToScoreName;
    }


    /**
     * Returns the {@link FDRData} for the file with the given ID.
     *
     * @param fileID
     * @return
     */
    public FDRData getFilesFDRData(Long fileID) {
        return fileFDRData.get(fileID);
    }


    /**
     * Getter for the map of FDR data
     * @return
     */
    public Map<Long, FDRData> getFileFDRData() {
        return fileFDRData;
    }


    /**
     * Returns the set number of top identifications used for the FDR
     * calculation for the given file.
     *
     * @param fileID
     * @return
     */
    public Integer getFilesTopIdentifications(Long fileID) {
        return fileTopIdentifications.get(fileID);
    }


    /**
     * Sets the number of top identifications used for the FDR calculation for
     * the given file.
     *
     * @param fileID
     * @return
     */
    public void setFilesTopIdentifications(Long fileID, Integer topIDs) {
        fileTopIdentifications.put(fileID, topIDs);
    }


    /**
     * Sets the number of top identifications used for the FDR calculation for
     * all files.
     *
     * @return
     */
    public void setAllTopIdentifications(Integer topIDs) {
        for (Long fileID : fileTopIdentifications.keySet()) {
            setFilesTopIdentifications(fileID, topIDs);
        }
    }


    /**
     * Sets the given pattern as the decoy pattern for all files' FDR data and sets the FDR decoy strategy to
     * {@link FDRData.DecoyStrategy}.ACCESSIONPATTERN, unless "searchengine" is given as pattern, which will set
     * {@link FDRData.DecoyStrategy}.SEARCHENGINE as decoy strategy. The {@link DecoyStrategy}.INHERIT can only be used
     * for protein or peptide level.
     *
     * @param pattern
     */
    public void setAllDecoyPattern(String pattern) {
        FDRData.DecoyStrategy decoyStrategy;
        boolean setPattern;

        if (FDRData.DecoyStrategy.SEARCHENGINE.toString().equals(pattern)) {
            decoyStrategy = FDRData.DecoyStrategy.SEARCHENGINE;
            setPattern = false;
        } else {
            decoyStrategy = FDRData.DecoyStrategy.ACCESSIONPATTERN;
            setPattern = true;
        }

        for (FDRData fdrData : fileFDRData.values()) {
            fdrData.setDecoyStrategy(decoyStrategy);
            if (setPattern) {
                fdrData.setDecoyPattern(pattern);
            }
        }
    }

    
    /**
     * Sets the FDR threshold for all files to the given value
     * @param thr
     */
    public void setAllFDRThresholds(Double thr) {
        for (FDRData fdrData : fileFDRData.values()) {
        	fdrData.setFDRThreshold(thr);
        }
    }
    

    /**
     * Updates the {@link FDRData} for the given file.
     *
     * @param fileID
     * @return
     */
    public void updateFilesFDRData(Long fileID, DecoyStrategy decoyStrategy,
            String decoyPattern, Double fdrThreshold, String scoreModelShort,
            Integer topIdentifications) {
        FDRData fdrData = fileFDRData.get(fileID);

        if (fdrData != null) {
            fdrData.setDecoyStrategy(decoyStrategy);
            fdrData.setDecoyPattern(decoyPattern);
            fdrData.setFDRThreshold(fdrThreshold);
            fdrData.setScoreShortName(scoreModelShort);

            setFilesTopIdentifications(fileID, topIdentifications);

            LOGGER.info("{}'s FDRData set to: {}, {}, {}, {}, {}", fileID,
                    fdrData.getDecoyStrategy(), fdrData.getDecoyPattern(),
                    fdrData.getFDRThreshold(), fdrData.getScoreShortName(),
                    getFilesTopIdentifications(fileID));
        } else {
            LOGGER.error("No FDRData for file with ID {}", fileID);
        }
    }


    /**
     * Returns a List of scoreShortNames of available Scores for FDR calculation
     * for the given file.
     *
     * @param fileID
     * @return
     */
    public List<String> getFilesAvailableScoreShortsForFDR(Long fileID) {
        List<String> fdrScoreNames = new ArrayList<>();

        // the overview is treated separately (it has no available scores, but only the FDRScore
        if (fileID > 0) {
            if (fileScoreShortNames.containsKey(fileID)) {
                List<String> scoreShorts = fileScoreShortNames.get(fileID);
                // FDR score is not available for FDR calculation
                fdrScoreNames.addAll(scoreShorts.stream().filter(scoreShort -> !ScoreModelEnum.PSM_LEVEL_FDR_SCORE.isValidDescriptor(scoreShort)).collect(Collectors.toList()));
            } else {
                LOGGER.error("No scores available for FDR calculation for the file with ID {}", fileID);
            }
        }

        return fdrScoreNames;
    }


    /**
     * Returns a mapping from the file IDs to the scoreNames used for FDR
     * calculation for each file.
     *
     * @return
     */
    public Map<Long, String> getFileIDsToScoreOfFDRCalculation() {
        Map<Long, String> filenameToScoreOfFDRCalculation = new HashMap<>(inputFiles.size() - 1);

        for (PIAInputFile file : inputFiles.values()) {
            if (file.getID() != 0L) {
                String scoreName;
                boolean fdrCalculated = fileFDRCalculated.get(file.getID());
                if (fdrCalculated) {
                    FDRData fdrData = fileFDRData.get(file.getID());
                    scoreName = ScoreModelEnum.getName(fdrData.getScoreShortName());
                } else {
                    scoreName = "no FDR calculated";
                }

                filenameToScoreOfFDRCalculation.put(file.getID(), scoreName);
            }
        }

        return filenameToScoreOfFDRCalculation;
    }


    /**
     * Returns, whether for the given file an FDR is calculated.
     * @param fileID
     * @return
     */
    public Boolean isFDRCalculated(Long fileID) {
        return fileFDRCalculated.get(fileID);
    }


    /**
     * Returns true, if all files have a calculated FDR.
     * @return
     */
    public Boolean getAllFilesHaveFDRCalculated() {
        for (PIAInputFile file : inputFiles.values()) {
            if ((file.getID() > 0) &&
                    ((fileFDRCalculated.get(file.getID()) == null) ||
                            !fileFDRCalculated.get(file.getID()))) {
                return false;
            }

        }

        return true;
    }


    /**
     * Returns whether the combined FDR Score is calculated.
     * @return
     */
    public boolean isCombinedFDRScoreCalculated() {
        return !reportPSMSets.isEmpty()
                && (fileFDRCalculated.get(0L) != null)
                && fileFDRCalculated.get(0L);
    }


    /**
     * Returns, whether the file with the given ID has internal decoys, i.e.
     * PSMs which are set to be decoys in the PIA XML file.
     *
     * @param fileID
     * @return
     */
    public Boolean getFileHasInternalDecoy(Long fileID) {
        return fileHasInternalDecoy.get(fileID);
    }


    /**
     * Updates the decoy states of the PSMs with the current settings from the
     * file's FDRData.
     *
     */
    public void updateDecoyStates(Long fileID) {
        FDRData fdrData = fileFDRData.get(fileID);

        LOGGER.info("updateDecoyStates {}", fileID);

        // select either the PSMs from the given file or all and calculate the fdr
        if (fdrData == null) {
            LOGGER.error("No FDR settings given for file with ID={}", fileID);
            // TODO: throw an exception or something
        } else {
            Pattern p = Pattern.compile(fdrData.getDecoyPattern());

            if (fileID > 0) {
                // get a List of the ReportPSMs for FDR calculation
                List<ReportPSM> listForFDR = fileReportPSMs.get(fileID);

                if (listForFDR == null) {
                    LOGGER.error("No PSMs found for the file with ID={}", fileID);
                    // TODO: throw an exception
                    return;
                }

                for (ReportPSM psm : listForFDR) {
                    // dump all FDR data
                    psm.dumpFDRCalculation();
                    psm.updateDecoyStatus(fdrData.getDecoyStrategy(), p);
                }
            } else {
                // set decoy information for PSM sets
                for (ReportPSMSet psmSet : reportPSMSets) {
                    psmSet.dumpFDRCalculation();
                    psmSet.updateDecoyStatus(fdrData.getDecoyStrategy(), p);
                }
            }
        }
    }


    /**
     * Calculate the FDR for all files. <br/>
     * If no score for the FDR calculation is given, use a default.
     */
    public void calculateAllFDR() {
        fileReportPSMs.keySet().forEach(this::calculateFDR);
    }


    /**
     * Calculate the FDR for the file given by fileID
     *
     * @param fileID
     */
    public void calculateFDR(Long fileID) {
        FDRData fdrData = fileFDRData.get(fileID);

        // select either the PSMs from the given file or all and calculate the fdr
        if (fdrData == null) {
            LOGGER.error("No FDR settings given for file with ID={}", fileID);
            // TODO: throw an exception
        } else {
            fdrData.setScoreShortName(getFilesPreferredFDRScore(fileID));
            LOGGER.info("set the score for FDR calculation for fileID={}: {}",
            		fileID, fdrData.getScoreShortName());

            // recalculate the decoy status (especially important, if decoy pattern was changed)
            updateDecoyStates(fileID);


            if (fileReportPSMs.get(fileID) == null) {
                LOGGER.error("No PSMs found for the file with ID={}", fileID);
                // TODO: throw an exception
                return;
            }

            // get a List of the ReportPSMs for FDR calculation
            List<PSMReportItem> listForFDR = new ArrayList<>(fileReportPSMs.get(fileID));

            if ((fileTopIdentifications.get(fileID) != null) &&
                    (fileTopIdentifications.get(fileID) > 0)) {

                LOGGER.info("applying topIdentification filter: top {} for {}",
                		fileTopIdentifications.get(fileID), fdrData.getScoreShortName());

                // as the used ReportPSMs may change with the filter, clear all prior FDR information
                listForFDR.forEach(FDRComputable::dumpFDRCalculation);

                // only the topIdentifications should be used, so a filter is needed
                List<AbstractFilter> topRankFilter = new ArrayList<>(1);

                topRankFilter.add(new PSMTopIdentificationFilter(
                        FilterComparator.less_equal,
                        fileTopIdentifications.get(fileID),
                        false,
                        fdrData.getScoreShortName()));

                listForFDR = FilterFactory.applyFilters(
                        listForFDR,
                        topRankFilter,
                        fileID);
            }


            if (scoreShortToComparator.get(fdrData.getScoreShortName()) == null) {
                LOGGER.warn("No comparator for FDR calculation, "
                        + "aborted calculateFDR!");
                return;
            }

            // calculate the FDR values
            fdrData.calculateFDR(listForFDR,
                    scoreShortToComparator.get(fdrData.getScoreShortName()));

            // and also calculate the FDR score
            FDRScore.calculateFDRScore(listForFDR, fdrData,
                    scoreShortToHigherScoreBetter.get(fdrData.getScoreShortName()));

            addPSMLevelFDRSCoreToFilesScores(fileID);

            if (!createPSMSets) {
                // if no PSM sets are created, add FDRScore to the overview
                addPSMLevelFDRSCoreToFilesScores(0L);
            }

            // the FDR for this file is calculated now
            fileFDRCalculated.put(fileID, true);
        }
    }


    /**
     * Adds the PSM FDR score to the given file's score short names
     *
     * @param fileID
     */
    private void addPSMLevelFDRSCoreToFilesScores(Long fileID) {
        fileScoreShortNames.computeIfAbsent(fileID, k -> new ArrayList<>());

        List<String> scoreShorts = fileScoreShortNames.get(fileID);
        if (!scoreShorts.contains(ScoreModelEnum.PSM_LEVEL_FDR_SCORE.getShortName())) {
            // add the FDR score to scores of this file
            scoreShorts.add(ScoreModelEnum.PSM_LEVEL_FDR_SCORE.getShortName());
            scoreShortToScoreName.put(
                    ScoreModelEnum.PSM_LEVEL_FDR_SCORE.getShortName(),
                    ScoreModelEnum.PSM_LEVEL_FDR_SCORE.getName());
            scoreShortToComparator.put(ScoreModelEnum.PSM_LEVEL_FDR_SCORE.getShortName(),
                    new ScoreComparator<>(
                            ScoreModelEnum.PSM_LEVEL_FDR_SCORE.getShortName(),
                            false));
            scoreShortToHigherScoreBetter.put(
                    ScoreModelEnum.PSM_LEVEL_FDR_SCORE.getShortName(),
                    false);
            scoreShortToHigherScoreBetterChangeable.put(
                    ScoreModelEnum.PSM_LEVEL_FDR_SCORE.getShortName(),
                    false);

            // and also to the sortable fields
            fileSortables.get(fileID).add(
                    PSMReportItemComparator.getScoreSortName(ScoreModelEnum.PSM_LEVEL_FDR_SCORE.getShortName()) );
        }
    }


    /**
     * Clears all preferred FDR scores.
     */
    public void resetPreferredFDRScores() {
        preferredFDRScores.clear();

        // set all FDR data scoreshorts for the regular files to null
        fileFDRData.entrySet().stream().filter(fdrIt -> !fdrIt.getKey().equals(0L)).forEach(fdrIt -> fdrIt.getValue().setScoreShortName(null));
    }


    /**
     * Getter for the set of preferred scores for FDR  calculation.
     * @return
     */
    public List<String> getPreferredFDRScores() {
        return preferredFDRScores;
    }


    /**
     * Adds the given scoreShortNames to the preferred FDR scores.
     */
    public void addPreferredFDRScores(List<String> scoreShortNames) {
        scoreShortNames.forEach(this::addPreferredFDRScore);
    }


    /**
     * Adds the score with the given short to the preferred FDR scores. If the
     * score does not exist, do nothing.
     *
     * @param scoreShortName
     */
    public void addPreferredFDRScore(String scoreShortName) {
        if (scoreShortName == null) {
            return;
        }

        // get the unique score SHORT_name, if the score is known
        ScoreModelEnum model =
                ScoreModelEnum.getModelByDescription(scoreShortName);
        String shortName = model.getShortName();
        if (shortName == null) {
            // for an unknown score, take the given SHORT_NAME
            shortName = scoreShortName;
        }

        if (!preferredFDRScores.contains(shortName)) {
            preferredFDRScores.add(shortName);
        }
    }


    /**
     * Returns the scoreShort which will be used for FDR calculation of the
     * given file. This is either the first applicable in the preferred scores
     * of the first one of the file.
     *
     * @param fileID
     * @return
     */
    public String getFilesPreferredFDRScore(Long fileID) {
        // first look in the preferred scores
        for (String scoreShort : preferredFDRScores) {
            if (fileScoreShortNames.get(fileID).contains(scoreShort)) {
                return scoreShort;
            }
        }

        // if no score is set in the preferred, look for searchengine main scores
        if (fileScoreShortNames.containsKey(fileID)) {
            for (String scoreShort : fileScoreShortNames.get(fileID)) {
                if (ScoreModelEnum.getModelByDescription(scoreShort).isSearchengineMainScore()) {
                    return scoreShort;
                }
            }

            // if no score is returned yet, take the first best score
            if(fileScoreShortNames.get(fileID) != null && !fileScoreShortNames.get(fileID).isEmpty())
                return fileScoreShortNames.get(fileID).get(0);
        }

        return null;
    }



    /**
     * Calculates the Combined FDR Score for the PSM sets in the overview
     */
    public void calculateCombinedFDRScore() {
        Map<String, List<ReportPSMSet>> fileLists = new HashMap<>();
        String key;

        updateDecoyStates(0L);

        // first we need the Average FDR Score for each PSM set
        for (ReportPSMSet set : reportPSMSets) {
            set.calculateAverageFDRScore();

            if (!set.getAverageFDRScore().getValue().equals(Double.NaN)) {
                // put the PSM set into the List, which holds the sets identified in the same files
                if (set.getPSMs().size() > 1) {

                    Set<Long> files = set.getPSMs().stream().filter(psm -> (psm.getFDRScore() != null) &&
                            !psm.getFDRScore().getValue().equals(Double.NaN)).map(ReportPSM::getFileID).collect(Collectors.toCollection(TreeSet::new));
                    // the psm has a valid FDR for this file

                    StringBuilder sbKey = new StringBuilder();

                    for (Long file : files) {
                        if (sbKey.length() > 0) {
                            sbKey.append(':');
                        }
                        sbKey.append(file);
                    }
                    key = sbKey.toString();
                } else {
                    key = set.getPSMs().get(0).getFileID().toString();
                }

                if (!fileLists.containsKey(key)) {
                    fileLists.put(key, new ArrayList<>());
                }

                fileLists.get(key).add(set);
            } else {
                // this PSM set gets no Combined FDR Score
                set.setFDRScore(Double.NaN);
            }
        }


        // go through the search-engine-sets, sort by AFS and calculate combined FDR Score
        for (Map.Entry<String, List<ReportPSMSet>> seSetIt : fileLists.entrySet()) {
            LOGGER.info("Calculation of Combined FDR Score for {}", seSetIt.getKey());

            (seSetIt.getValue()).sort(new ScoreComparator<>(ScoreModelEnum.AVERAGE_FDR_SCORE.getShortName()));

            FDRData fdrData = fileFDRData.get(0L);

            fdrData.setScoreShortName(ScoreModelEnum.AVERAGE_FDR_SCORE.getShortName());
            fdrData.calculateFDR(seSetIt.getValue());

            if (seSetIt.getValue().size() > 2) {
                FDRScore.calculateFDRScore(seSetIt.getValue(), fdrData,
                        ScoreModelEnum.AVERAGE_FDR_SCORE.higherScoreBetter());
            } else {
                for (ReportPSMSet set : seSetIt.getValue()) {
                    set.setFDRScore(set.getAverageFDRScore().getValue());
                }
            }
        }


        // add to the sortable fields
        fileSortables.get(0L).add(
                PSMReportItemComparator.getScoreSortName(ScoreModelEnum.PSM_LEVEL_COMBINED_FDR_SCORE.getShortName()) );

        // and add to the score fields
        fileScoreShortNames.put(0L, new ArrayList<>(1));
        fileScoreShortNames.get(0L).add(ScoreModelEnum.PSM_LEVEL_COMBINED_FDR_SCORE.getShortName());
        scoreShortToScoreName.put(ScoreModelEnum.PSM_LEVEL_COMBINED_FDR_SCORE.getShortName(), ScoreModelEnum.PSM_LEVEL_COMBINED_FDR_SCORE.getName());
        scoreShortToComparator.put(ScoreModelEnum.PSM_LEVEL_COMBINED_FDR_SCORE.getShortName(),
                new ScoreComparator<>(
                        ScoreModelEnum.PSM_LEVEL_COMBINED_FDR_SCORE.getShortName(),
                        false));
        scoreShortToHigherScoreBetter.put(
                ScoreModelEnum.PSM_LEVEL_COMBINED_FDR_SCORE.getShortName(),
                false);
        scoreShortToHigherScoreBetterChangeable.put(
                ScoreModelEnum.PSM_LEVEL_COMBINED_FDR_SCORE.getShortName(),
                false);

        // correct the numbers of decoys etc.
        int nrDecoys = 0;
        int nrTargets = 0;
        int nrItems = 0;
        int nrFDRGoodDecoys = 0;
        int nrFDRGoodTargets = 0;
        double thr = fileFDRData.get(0L).getFDRThreshold();

        for (ReportPSMSet set : reportPSMSets) {
            if (!set.getFDRScore().getValue().equals(Double.NaN)) {
                nrItems++;
                if (set.getIsDecoy()) {
                    nrDecoys++;
                    if (set.getFDRScore().getValue() <= thr) {
                        nrFDRGoodDecoys++;
                    }
                } else {
                    nrTargets++;
                    if (set.getFDRScore().getValue() <= thr) {
                        nrFDRGoodTargets++;
                    }
                }
            }
        }

        fileFDRData.get(0L).correctNumbers(nrDecoys, nrFDRGoodDecoys,
                nrFDRGoodTargets, nrItems, nrTargets);

        // the combined FDR is calculated now
        fileFDRCalculated.put(0L, true);
    }


    /**
     * Sorts the List of {@link ReportPSM}s of the file given by fileID with the
     * prior specified sorting parameters.
     */
    public void sortReport(Long fileID, List<String> sortOrders,
            Map<String, SortOrder> sortables) {
        List<Comparator<PSMReportItem>> compares =
                new ArrayList<>();

        for (String sortKey : sortOrders) {
            Comparator<PSMReportItem> comp = getScoreComparator(sortKey);

            if (comp == null) {
                comp = PSMReportItemComparator.getComparatorByName(
                        sortKey,
                        sortables.get(sortKey));
            } else if (sortables.get(sortKey).equals(SortOrder.descending)){
                comp = PSMReportItemComparator.descending(comp);
            }

            if (comp != null) {
                compares.add( comp);
            } else {
                LOGGER.error("no comparator found for {}", sortKey);
            }
        }

        if (fileID > 0) {
            (fileReportPSMs.get(fileID)).sort(PSMReportItemComparator.getComparator(compares));
        } else {
            reportPSMSets.sort(PSMReportItemComparator.getComparator(compares));
        }
    }


    /**
     * Returns a List of scoreShortNames of available Scores for ranking.
     *
     * @param fileID
     * @return
     */
    public List<String> getFilesAvailableScoreShortsForRanking(Long fileID) {
        List<String> rankingScoreNames = new ArrayList<>();

        if (fileScoreShortNames.containsKey(fileID)) {
            rankingScoreNames.addAll(fileScoreShortNames.get(fileID));
        }

        if (rankingScoreNames.isEmpty() && (fileID > 0)) {
            LOGGER.error("No scores available for ranking for the file with ID {}", fileID);
        }

        return rankingScoreNames;
    }


    /**
     * Calculates the ranking for the given file and scoreShortName. If the
     * filter List is not null or empty, the Report is filtered before ranking.
     */
    public void calculateRanking(Long fileID, String rankableShortName,
            List<AbstractFilter> filters) {
        if ((rankableShortName == null) || rankableShortName.trim().isEmpty()) {
            LOGGER.error("No score SHORT_NAME given for ranking calculation.");
            return;
        }

        // first, dump all prior ranking
        List<?> reports;
        if (fileID > 0) {
            reports = fileReportPSMs.get(fileID);
        } else {
            reports = reportPSMSets;
        }
        if (reports != null) {
            reports.stream().filter(PSMReportItem.class::isInstance).forEach(obj -> ((PSMReportItem) obj).setRank(-1L));
        }

        if (fileID > 0) {
            RankCalculator.calculateRanking(rankableShortName,
                    FilterFactory.applyFilters(fileReportPSMs.get(fileID),
                            filters, fileID),
                    new ScoreComparator<>(rankableShortName));
        } else {
            RankCalculator.calculateRanking(rankableShortName,
                    FilterFactory.applyFilters(reportPSMSets,
                            filters, fileID),
                    new ScoreComparator<>(rankableShortName));
        }
    }


    /**
     * Returns the filters set for the given file.
     * @param fileID
     * @return
     */
    public List<AbstractFilter> getFilters(Long fileID) {
        return fileFiltersMap.computeIfAbsent(fileID, k -> new ArrayList<>());
    }


    /**
     * Add a new filter for the given file
     */
    public boolean addFilter(Long fileID, AbstractFilter newFilter) {
        return newFilter != null && getFilters(fileID).add(newFilter);
    }


    /**
     * Removes the filter from the given file at the given index.
     * @param fileID
     * @param removingIndex
     * @return
     */
    public AbstractFilter removeFilter(Long fileID, int removingIndex) {
        List<AbstractFilter> filters = getFilters(fileID);

        if ((removingIndex >= 0) &&
                (filters != null) &&
                (removingIndex < filters.size())) {
            return filters.remove(removingIndex);
        }

        return null;
    }
    
    
    /**
     * Adds the filters given by the array derived from parsing the json
     * 
     * @param filters
     * @param fileID
     * @return
     */
	public boolean addPSMFiltersFromJSONStrings(String[] filters, long fileID) {
		boolean allOk = true;
		
		for (String filter : filters) {
			StringBuilder messageBuffer = new StringBuilder();
			AbstractFilter newFilter = FilterFactory.createInstanceFromString(filter, messageBuffer);
			
			if (newFilter != null) {
				LOGGER.info("Adding filter: {}", newFilter);
				allOk |= addFilter(fileID, newFilter);
			} else {
				LOGGER.error("Could not create filter from string '{}': {}", filter, messageBuffer);
				allOk = false;
			}
		}
		
		return allOk;
	}



    public double[] getPPMDeviationData(Long fileID, boolean fdrGood) {
        if (fdrGood && !isFDRCalculated(fileID).booleanValue()) {
            return new double[0];
        }

        ArrayList<Double> ppmData;

        // put all PPM shifts into the array
        if (fileID > 0) {
            ppmData = new ArrayList<>(fileReportPSMs.get(fileID).size());
            ppmData.addAll(fileReportPSMs.get(fileID).stream().filter(psm -> !fdrGood || (!psm.getIsDecoy() && psm.getIsFDRGood())).map(ReportPSM::getDeltaPPM).collect(Collectors.toList()));
        } else {
            ppmData = new ArrayList<>(reportPSMSets.size());
            ppmData.addAll(reportPSMSets.stream().filter(psm -> !fdrGood || (!psm.getIsDecoy() && psm.getIsFDRGood())).map(ReportPSMSet::getDeltaPPM).collect(Collectors.toList()));
        }

        double[] ppmResult = new double[ppmData.size()];
        ListIterator<Double> iter = ppmData.listIterator();
        int idx = 0;
        while (iter.hasNext()) {
            ppmResult[idx] = iter.next();
            idx++;
        }
        return ppmResult;
    }


    /**
     * Calculates the data for a histogram of the distribution of the PPM
     * divergence. If fdrGood is true, only the FDR good target PSM (sets) are
     * taken into account.
     *
     * @param fileID
     * @param fdrGood whether to use only the FDR good target PSM(set)s
     * @return
     */
    public List<List<Integer>> getPPMs(Long fileID, boolean fdrGood) {
        if (fdrGood && !isFDRCalculated(fileID).booleanValue()) {
            List<List<Integer>> labelled = new ArrayList<>();
            labelled.add(new ArrayList<>());
            labelled.add(new ArrayList<>());
            return labelled;
        }

        Map<Integer, Integer> ppmMap = new HashMap<>();
        int counted = 0;
        int labelMax = 0;
        int labelMin = 0;

        // put the PPMs in 1-PPM bins in the map
        if (fileID > 0) {
            for (ReportPSM psm : fileReportPSMs.get(fileID)) {
                if (!fdrGood
                        || (!psm.getIsDecoy() && psm.getIsFDRGood())) {
                    Integer label = (int)Math.floor(psm.getDeltaPPM() + 0.5d);

                    if (!ppmMap.containsKey(label)) {
                        ppmMap.put(label, 0);
                    }
                    ppmMap.put(label, ppmMap.get(label) + 1);
                    counted++;

                    if (label < labelMin) {
                        labelMin = label;
                    }
                    if (label > labelMax) {
                        labelMax = label;
                    }
                }
            }
        } else {
            for (ReportPSMSet psm : reportPSMSets) {
                if (!fdrGood
                        || (!psm.getIsDecoy() && psm.getIsFDRGood())) {
                    Integer label = (int)Math.floor(psm.getDeltaPPM() + 0.5d);

                    if (!ppmMap.containsKey(label)) {
                        ppmMap.put(label, 0);
                    }
                    ppmMap.put(label, ppmMap.get(label) + 1);
                    counted++;

                    if (label < labelMin) {
                        labelMin = label;
                    }
                    if (label > labelMax) {
                        labelMax = label;
                    }
                }
            }
        }

        // create the PPM counts and labels list from the map
        List<Integer> ppms = new ArrayList<>();
        List<Integer> labels = new ArrayList<>();
        int drawn = 0;

        labels.add(0);
        if (ppmMap.containsKey(0)) {
            ppms.add(ppmMap.get(0));
            drawn += ppmMap.get(0);
        } else {
            ppms.add(0);
        }

        int i;
        for (i=1; (i < 10) && (drawn < 0.995 * counted); i++) {
            labels.add(i);
            if (ppmMap.containsKey(i)) {
                ppms.add(ppmMap.get(i));
                drawn += ppmMap.get(i);
            } else {
                ppms.add(0);
            }

            labels.add(0, -i);
            if (ppmMap.containsKey(-i)) {
                ppms.add(0, ppmMap.get(-i));
                drawn += ppmMap.get(-i);
            } else {
                ppms.add(0, 0);
            }
        }

        // all above
        for (drawn=0; i < labelMax; i++) {
            if (ppmMap.containsKey(i)) {
                drawn += ppmMap.get(i);
            }
        }
        labels.add(null);
        ppms.add(drawn);

        // all below
        for (i=labelMin, drawn=0; i < labels.get(0); i++) {
            if (ppmMap.containsKey(i)) {
                drawn += ppmMap.get(i);
            }
        }
        labels.add(0, null);
        ppms.add(0, drawn);

        List<List<Integer>> labelled = new ArrayList<>();
        labelled.add(ppms);
        labelled.add(labels);
        return labelled;
    }



    /**
     * Returns, how many times a PSM set had how many identifications. The first
     * entry in the list represents one identification, the second two...
     *
     * @param fdrGood
     * @return
     */
    public List<Integer> getNrIdentifications(boolean fdrGood) {
        if (fdrGood && !isCombinedFDRScoreCalculated()) {
            List<Integer> idList = new ArrayList<>(1);
            idList.add(0);
            return idList;
        }

        Map<Integer, Integer> idMap = new HashMap<>();
        int maxIDs = 0;

        // count the number of identifications
        for (ReportPSMSet psm : reportPSMSets) {
            if (!fdrGood ||
                    (!psm.getIsDecoy() && psm.getIsFDRGood())) {
                Integer ids = psm.getPSMs().size();

                if (!idMap.containsKey(ids)) {
                    idMap.put(ids, 1);
                } else {
                    idMap.put(ids, idMap.get(ids) + 1);
                }

                if (ids > maxIDs) {
                    maxIDs = ids;
                }
            }
        }

        List<Integer> idList = new ArrayList<>(maxIDs);
        for (int i=1; i <= maxIDs; i++) {
            idList.add(idMap.getOrDefault(i, 0));
        }

        return idList;
    }


    /**
     * Writes some PSM level information.
     *
     * @throws IOException
     */
    public void writePSMInformation(String fileName, boolean calculate) {
        try (Writer writer = new FileWriter(fileName, false)) {
            writePSMInformation(writer, calculate);
        } catch (IOException e) {
            LOGGER.error("Could not write PSM information to {}", fileName, e);
        }
    }


    /**
     * Writes some PSM level information.
     *
     * @throws IOException
     */
    public void writePSMInformation(Writer writer, boolean calculate) throws IOException {
        FDRData fdrData;
        List<Double> fdrThresholds = Arrays.asList(0.01, 0.03, 0.05);
        Double originalFDRThreshold;
        String nl = System.getProperty("line.separator");

        writer.append("PSM information about ").append(fileName).append(nl);

        // numbers for each file
        for (Map.Entry<Long, PIAInputFile> longPIAInputFileEntry : inputFiles.entrySet()) {
            if (longPIAInputFileEntry.getKey() < 1) {
                // skip the overview, combined FDR is added later
                continue;
            }

            writer.append(nl).append("PSMs in file #").append(String.valueOf(longPIAInputFileEntry.getKey())).append(" (").append(longPIAInputFileEntry.getValue().getName()).append(")").append(nl);
            writer.append("==============================").append(nl);
            writer.append("#PSMs: ");
            writer.append(Integer.toString(getNrReportPSMs(longPIAInputFileEntry.getKey())));
            writer.append(nl);

            Boolean fdrCalculated = isFDRCalculated(longPIAInputFileEntry.getKey());
            if (fdrCalculated == null) {
                writer.append("FDR was not calculated or was not able to be calculated.");
                writer.append(nl);
            } else if (fdrCalculated) {
                fdrData = getFileFDRData().get(longPIAInputFileEntry.getKey());
                originalFDRThreshold = fdrData.getFDRThreshold();

                writer.append(nl);
                writer.append("FDR is calculated with ").append(getScoreName(fdrData.getScoreShortName()));
                writer.append(" using ").append(String.valueOf(getFilesTopIdentifications(longPIAInputFileEntry.getKey()))).append(" top identifications");
                writer.append(nl);
                if (fdrData.getDecoyStrategy().equals(DecoyStrategy.ACCESSIONPATTERN)) {
                    writer.append("Regular expression used to identify decoys: ").append(fdrData.getDecoyPattern());
                } else {
                    writer.append("Searchengine internal decoys are used.");
                }
                writer.append(nl);
                writer.append(nl);

                writer.append("#PSMs with FDR: ");
                writer.append(Integer.toString(fdrData.getNrItems()));
                writer.append(nl);
                writer.append("  #targets:     ");
                writer.append(Integer.toString(fdrData.getNrTargets()));
                writer.append(nl);
                writer.append("  #decoys:      ");
                writer.append(Integer.toString(fdrData.getNrDecoys()));
                writer.append(nl);

                if (!calculate ||
                        !fdrThresholds.contains(fdrData.getFDRThreshold())) {
                    writer.append("FDR ").append(String.valueOf(fdrData.getFDRThreshold())).append(":").append(nl);
                    writer.append("  #targets below ").append(String.valueOf(fdrData.getFDRThreshold())).append(" threshold: ");
                    writer.append(fdrData.getNrFDRGoodTargets().toString());
                    writer.append(nl);
                    writer.append("  #decoys below ").append(String.valueOf(fdrData.getFDRThreshold())).append(" threshold:  ");
                    writer.append(fdrData.getNrFDRGoodDecoys().toString());
                    writer.append(nl);
                    writer.append("  score at threshold: ");
                    writer.append("").append(String.valueOf(fdrData.getScoreAtThreshold()));
                    writer.append(nl);
                }

                if (calculate) {
                    //TODO: make this faster, calculating always the FDR is time consuming
                    for (Double thr : fdrThresholds) {
                        if (!thr.equals(fdrData.getFDRThreshold())) {
                            fdrData.setFDRThreshold(thr);
                            calculateFDR(longPIAInputFileEntry.getKey());
                        }

                        writer.append("FDR ").append(String.valueOf(thr)).append(":").append(nl);
                        writer.append("  #targets below ").append(String.valueOf(thr)).append(" threshold: ");
                        writer.append(fdrData.getNrFDRGoodTargets().toString());
                        writer.append(nl);
                        writer.append("  #decoys below ").append(String.valueOf(thr)).append(" threshold:  ");
                        writer.append(fdrData.getNrFDRGoodDecoys().toString());
                        writer.append(nl);
                        writer.append("  score at threshold: ");
                        writer.append("").append(String.valueOf(fdrData.getScoreAtThreshold()));
                        writer.append(nl);
                    }

                    // reset the FDR data
                    fdrData.setFDRThreshold(originalFDRThreshold);
                    calculateFDR(longPIAInputFileEntry.getKey());
                }
            }
        }

        // numbers for PSMSets
        writer.append(nl).append("PSM sets (create sets = ").append(String.valueOf(createPSMSets)).append(")").append(nl);
        writer.append("==============================").append(nl);
        writer.append("#PSM sets: ");
        writer.append(Integer.toString(getNrReportPSMs(0L)));
        writer.append(nl);



        if (isCombinedFDRScoreCalculated()) {
            writer.append("combined FDR score is calculated").append(nl);

            fdrData = getFileFDRData().get(0L);
            originalFDRThreshold = fdrData.getFDRThreshold();

            writer.append("#PSM sets with FDR: ");
            writer.append(Integer.toString(fdrData.getNrItems()));
            writer.append(nl);
            writer.append("  #targets:         ");
            writer.append(Integer.toString(fdrData.getNrTargets()));
            writer.append(nl);
            writer.append("  #decoys:          ");
            writer.append(Integer.toString(fdrData.getNrDecoys()));
            writer.append(nl);

            if (!calculate ||
                    !fdrThresholds.contains(fdrData.getFDRThreshold())) {
                writer.append("FDR ").append(String.valueOf(fdrData.getFDRThreshold())).append(":").append(nl);
                writer.append("  #targets below ").append(String.valueOf(fdrData.getFDRThreshold())).append(" threshold: ");
                writer.append(fdrData.getNrFDRGoodTargets().toString());
                writer.append(nl);
                writer.append("  #decoys below ").append(String.valueOf(fdrData.getFDRThreshold())).append(" threshold:  ");
                writer.append(fdrData.getNrFDRGoodDecoys().toString());
                writer.append(nl);
                writer.append("  score at threshold: ");
                writer.append("").append(String.valueOf(fdrData.getScoreAtThreshold()));
                writer.append(nl);
            }

            if (calculate) {
                //TODO: make this better, calculating always the FDR is time consuming
                for (Double thr : fdrThresholds) {
                    fdrData.setFDRThreshold(thr);
                    calculateCombinedFDRScore();
                    writer.append("FDR ").append(String.valueOf(thr)).append(":").append(nl);
                    writer.append("  #targets below ").append(String.valueOf(thr)).append(" threshold: ");
                    writer.append(fdrData.getNrFDRGoodTargets().toString());
                    writer.append(nl);
                    writer.append("  #decoys below ").append(String.valueOf(thr)).append(" threshold:  ");
                    writer.append(fdrData.getNrFDRGoodDecoys().toString());
                    writer.append(nl);
                    int nrPSMs = 0;
                    writer.append("  #good identifications in sets:").append(nl);
                    for (Integer nrIDs : getNrIdentifications(true)) {
                        nrPSMs++;
                        writer.append("    ").append(String.valueOf(nrPSMs)).append(" - ").append(String.valueOf(nrIDs)).append(nl);
                    }
                }

                // reset the FDR data
                fdrData.setFDRThreshold(originalFDRThreshold);
                calculateCombinedFDRScore();
            }
        }

        // TODO: create histogram-data for mass-shifts at the 1%, 3% and 5% FDR value

        writer.flush();
    }
    

    /**
     * Execute analysis on PSM level, getting the settings from JSON.
     * <p>
     * If a required setting is not given, the default value is used.
     */
    public boolean executePSMOperations(JsonAnalysis json) {
    	boolean allOk = true;
    	
    	applyGeneralSettings(json.isCreatePSMsets());
    	
        // FDR strategy and pattern
        setAllDecoyPattern(json.getDecoyPattern());

    	// as it is used only internally, hard code the FDR threshold to 0.01 for now
        setAllFDRThresholds(0.01);
        
        // set the top identifications
        setAllTopIdentifications(json.getTopIdentifications());
        
        // set the preferred scores for FDR calculation
        resetPreferredFDRScores();
        for (String fdrScore : json.getPreferredFDRScores()) {
            addPreferredFDRScore(fdrScore);
        }

        // at least set the decoy state of the selected file
        updateDecoyStates(json.getPsmLevelFileID());

        // calculate the FDR
        boolean fdrCalculated =false;
        if (json.isCalculateAllFDR()) {
            // all FDR should be calculated
            calculateAllFDR();
            fdrCalculated = true;
        } else if (json.getCalculateFDRFileIDs().length > 0) {
        	// calculate FDRs for single files
        	for (long id : json.getCalculateFDRFileIDs()) {
        		calculateFDR(id);
                fdrCalculated = true;
        	}
        }
            
        if (json.isErrorOnNoDecoys()) {
        	allOk = checkPSMsForDecoys();
        }
        
        if (allOk) {
	        if (json.isCreatePSMsets()
	                && fdrCalculated
	                && json.isCalculateCombinedFDRScore()) {
	            // calculate the Combined FDR Score only if there are PSM sets and calculated FDRs
	            calculateCombinedFDRScore();
	        } else if (json.isCalculateAllFDR()) {
	            // PSM sets are not created, but all FDRs are calculated -> set decoy level on PSM overview
	            updateDecoyStates(0L);
	        }
        }

        return allOk;
    }
    
    
    /**
     * Checks in all the FDRData of each file, whether at least one decoy was
     * found and whether the FDR could be calculated. Must be called after
     * calculating the FDR on all files.
     * 
     * @return
     */
	private boolean checkPSMsForDecoys() {
		boolean isOk = true;

		long allDecoys = 0;
		boolean oneFdrNotNull = false;
		for (Map.Entry<Long, FDRData> fdrDataIt : getFileFDRData().entrySet()) {
			if (fdrDataIt.getKey() != 0L) {
				FDRData fdrData = fdrDataIt.getValue();
				if (fdrData != null) {
					oneFdrNotNull = true;
					allDecoys += fdrData.getNrDecoys();
				}
			}
		}

		if (!oneFdrNotNull) {
			LOGGER.error("FDR could not be calculated for any of the given files.");
			isOk = false;
		}
		if (allDecoys < 1) {
			LOGGER.error("No decoy was found, check the pattern used for decoy detection.");
			isOk = false;
		}
		
		return isOk;
	}
}
