/*
 *                  Aozan development code
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU General Public License version 3 or later 
 * and CeCILL. This should be distributed with the code. If you 
 * do not have a copy, see:
 *
 *      http://www.gnu.org/licenses/gpl-3.0-standalone.html
 *      http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
 *
 * Copyright for this code is held jointly by the Genomic platform
 * of the Institut de Biologie de l'École Normale Supérieure and
 * the individual authors. These should be listed in @author doc
 * comments.
 *
 * For more information on the Aozan project and its aims,
 * or to join the Aozan Google group, visit the home page at:
 *
 *      http://outils.genomique.biologie.ens.fr/aozan
 *
 */

package fr.ens.biologie.genomique.aozan;

import static fr.ens.biologie.genomique.aozan.collectors.ReadCollector.READ_DATA_PREFIX;
import static fr.ens.biologie.genomique.aozan.collectors.SamplesheetCollector.SAMPLESHEET_DATA_PREFIX;
import static fr.ens.biologie.genomique.kenetre.io.FileUtils.checkExistingFile;
import static java.util.Objects.requireNonNull;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.io.Files;

import fr.ens.biologie.genomique.aozan.collectors.AozanCollector;
import fr.ens.biologie.genomique.aozan.collectors.ReadCollector;

/**
 * This class define a RunData object that contains all information generated by
 * collectors.
 * @author Laurent Jourdren
 * @since 0.8
 */
public class RunData {

  /** Splitter. */
  private static final Splitter COMMA_SPLITTER =
      Splitter.on(",").trimResults().omitEmptyStrings();

  /** The map. */
  private final Map<String, String> map = new LinkedHashMap<>();

  //
  // Comparators
  //

  /**
   * This class define a comparator to order projects.
   */
  public static class ProjectComparator implements Comparator<Integer> {

    private final RunData data;

    @Override
    public int compare(final Integer i1, final Integer i2) {

      final String projectName1 = data.getProjectName(i1);
      final String projectName2 = data.getProjectName(i2);

      return projectName1.compareTo(projectName2);
    }

    /**
     * Constructor.
     * @param data run data object
     */
    public ProjectComparator(final RunData data) {

      requireNonNull(data, "data object cannot be null");
      this.data = data;
    }
  }

  /**
   * This class define a comparator to order pooled samples.
   */
  public static class PooledSampleComparator implements Comparator<Integer> {

    private final RunData data;

    @Override
    public int compare(final Integer i1, final Integer i2) {

      final boolean undetermined1 = data.isUndeterminedPooledSample(i1);
      final boolean undetermined2 = data.isUndeterminedPooledSample(i2);

      final int compare1 = Boolean.compare(undetermined1, undetermined2);

      if (compare1 != 0) {
        return compare1;
      }

      final String projectName1 = data.getPooledSampleProjectName(i1);
      final String projectName2 = data.getPooledSampleProjectName(i2);

      final int compare2 = projectName1.compareTo(projectName2);

      if (compare2 != 0) {
        return compare2;
      }

      final String pooledSampleName1 = data.getPooledSampleDemuxName(i1);
      final String pooledSampleName2 = data.getPooledSampleDemuxName(i2);

      return pooledSampleName1.compareTo(pooledSampleName2);
    }

    /**
     * Constructor.
     * @param data run data object
     */
    public PooledSampleComparator(final RunData data) {

      requireNonNull(data, "data object cannot be null");
      this.data = data;
    }
  }

  //
  // Collector getters
  //

  /**
   * Get the list of collector names.
   * @return a list with the names of the collectors
   */
  public List<String> getCollectorNames() {

    final String key = AozanCollector.PREFIX + ".conf." + QC.QC_COLLECTOR_NAMES;

    if (!contains(key)) {
      return Collections.emptyList();
    }

    return Splitter.on(',').trimResults().omitEmptyStrings()
        .splitToList(get(key));
  }

  /**
   * Test if a collector is enabled.
   * @param collectorName name of the collector to test
   * @return true if the collector is enabled
   */
  public boolean isCollectorEnabled(final String collectorName) {

    return getCollectorNames().contains(collectorName);
  }

  //
  // Sample getters
  //

  /**
   * Get all samples names related to the lane as a list.
   * @param lane the lane number
   * @return all samples related to the lane
   */
  public List<Integer> getAllSamplesInLane(final int lane) {

    final String value =
        get(SAMPLESHEET_DATA_PREFIX + ".lane" + lane + ".samples");

    if (value == null) {
      return Collections.emptyList();
    }

    final List<String> resultString = COMMA_SPLITTER.splitToList(value);

    final List<Integer> result = new ArrayList<>(resultString.size());

    for (String s : resultString) {
      result.add(Integer.parseInt(s));
    }

    return result;
  }

  /**
   * Get all samples identifiers (without undetermined samples) related to the
   * lane as a list.
   * @param lane the lane number
   * @return all samples related to the lane
   */
  public List<Integer> getSamplesInLane(final int lane) {

    final List<Integer> result = new ArrayList<Integer>();

    for (int sampleId : getAllSamplesInLane(lane)) {

      if (!isUndeterminedSample(sampleId)) {
        result.add(sampleId);
      }
    }

    return result;
  }

  /**
   * Get all samples identifiers (with undetermined samples) of the run.
   * @return all samples related to the lane
   */
  public List<Integer> getAllSamples() {

    final List<Integer> result = new ArrayList<Integer>();
    final int sampleCount = getSampleCount();

    for (int i = 1; i <= sampleCount; i++) {
      result.add(i);
    }

    return result;
  }

  /**
   * Get all samples identifiers (without undetermined samples) of the run.
   * @return all samples related to the lane
   */
  public List<Integer> getSamples() {

    final List<Integer> result = new ArrayList<Integer>();
    final int sampleCount = getSampleCount();

    for (int i = 1; i <= sampleCount; i++) {
      if (!isUndeterminedSample(i)) {
        result.add(i);
      }
    }

    return result;
  }

  /**
   * Get the sample count.
   * @return the sample count
   */
  public int getSampleCount() {
    return getInt(SAMPLESHEET_DATA_PREFIX + ".sample.count");
  }

  /**
   * Check if undetermined FASTQ file exist for a lane.
   * @param lane the lane
   * @return true if if undetermined FASTQ file exist for a lane
   */
  public boolean isUndeterminedInLane(final int lane) {

    final List<Integer> samples = getAllSamplesInLane(lane);

    if (samples == null) {
      return false;
    }

    for (int sampleId : samples) {

      if (isUndeterminedSample(sampleId)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Get the sequence index related to a sample.
   * @param sampleId the sample id
   * @return sequence index related to sample
   */
  public String getIndexSample(final int sampleId) {
    return get(SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".index");
  }

  /**
   * Get the sample identifier string.
   * @param sampleId the sample id
   * @return sequence index related to the lane and sample name
   */
  public String getSampleIdentifier(final int sampleId) {
    return get(SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".id");
  }

  /**
   * Get the name of a sample.
   * @param sampleId the sample id
   * @return sequence index related to the lane and sample name
   */
  public String getSampleName(final int sampleId) {
    return get(SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".name");
  }

  /**
   * Get the demultiplexing name of a sample.
   * @param sampleId the sample id
   * @return sequence index related to the lane and sample name
   */
  public String getSampleDemuxName(final int sampleId) {
    return get(SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".demux.name");
  }

  /**
   * Get the lane of a sample.
   * @param sampleId the sample id
   * @return sequence index related to the lane and sample name
   */
  public int getSampleLane(final int sampleId) {
    return getInt(SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".lane");
  }

  /**
   * Test if the sample is an undetermined sample.
   * @param sampleId the sample id
   * @return sequence index related to the lane and sample name
   */
  public boolean isUndeterminedSample(final int sampleId) {
    return getBoolean(
        SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".undetermined");
  }

  /**
   * Test if the sample is an indexed sample.
   * @param sampleId the sample id
   * @return sequence index related to the lane and sample name
   */
  public boolean isIndexedSample(final int sampleId) {
    return getBoolean(
        SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".indexed");
  }

  /**
   * Get the project related to a sample.
   * @param sampleId the sample id
   * @return the project related to the sample
   */
  public String getProjectSample(final int sampleId) {
    return get(SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".project");
  }

  /**
   * Get the description of a sample.
   * @param sampleId the sample id
   * @return the description of the sample
   */
  public String getSampleDescription(final int sampleId) {
    return this
        .get(SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".description");
  }

  /**
   * Gets the sample genome.
   * @param sampleId the sample id
   * @return the sample genome
   */
  public String getSampleGenome(final int sampleId) {
    return get(SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".ref");
  }

  /**
   * Gets the normalized sample genome.
   * @param sampleId the sample id
   * @return the normalized sample genome
   */
  public String getNormalizedSampleGenome(final int sampleId) {
    return get(
        SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".normalized.ref");
  }

  /**
   * Get the index of a sample.
   * @param sampleId the sample id
   * @return the index of the sample
   */
  public String getSampleIndex(final int sampleId) {

    return get(SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".index");
  }

  /**
   * Get the index of a sample.
   * @param sampleId the sample id
   * @return the index of the sample
   */
  public String getSampleIndex2(final int sampleId) {

    return get(SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".index2");
  }

  /**
   * Checks if is lane indexed.
   * @param lane the lane
   * @return true, if is lane indexed
   */
  public boolean isLaneIndexed(final int lane) {

    return getBoolean(SAMPLESHEET_DATA_PREFIX + ".lane" + lane + ".indexed");
  }

  /**
   * Check if the lane related to the sample name is a control lane.
   * @param sampleId the sample Id
   * @return true if the lane is a control otherwise false
   */
  public boolean isLaneControl(final int sampleId) {
    return getBoolean(
        SAMPLESHEET_DATA_PREFIX + ".sample" + sampleId + ".control");
  }

  /**
   * Get the identifier of sample from its lane and demuxmultiplexing name.
   * @param lane the lane
   * @param demuxName the demultiplexing name
   * @return the id of the sample or -1 if not found
   */
  public int getSampleId(final int lane, final String demuxName) {

    if (demuxName == null || lane < 1 || lane > getLaneCount()) {
      return -1;
    }

    for (int i : getAllSamplesInLane(lane)) {

      if (demuxName.equals(getSampleDemuxName(i))) {
        return i;
      }
    }

    return -1;
  }

  //
  // Pooled sample getters
  //

  /**
   * Get the pooled sample count.
   * @return the pooled sample count
   */
  public int getPooledSampleCount() {
    return getInt(SAMPLESHEET_DATA_PREFIX + ".pooledsample.count");
  }

  /**
   * Get the demultiplexing name of a pooled sample.
   * @param pooledSampleId the id of the pooled sample
   * @return the demultiplexing name of a pooled sample
   */
  public String getPooledSampleDemuxName(final int pooledSampleId) {
    return get(SAMPLESHEET_DATA_PREFIX
        + ".pooledsample" + pooledSampleId + ".demux.name");
  }

  /**
   * Get the project name of a pooled sample.
   * @param pooledSampleId the id of the pooled sample
   * @return the project name of a pooled sample
   */
  public String getPooledSampleProjectName(final int pooledSampleId) {
    return get(SAMPLESHEET_DATA_PREFIX
        + ".pooledsample" + pooledSampleId + ".project.name");
  }

  /**
   * Get the project id of a pooled sample.
   * @param pooledSampleId the id of the pooled sample
   * @return the project id of a pooled sample
   */
  public int getPooledSampleProject(final int pooledSampleId) {
    return getInt(SAMPLESHEET_DATA_PREFIX
        + ".pooledsample" + pooledSampleId + ".project");
  }

  /**
   * Get the index of a pooled sample.
   * @param pooledSampleId the id of the pooled sample
   * @return the index of a pooled sample
   */
  public String getPooledSampleIndex(final int pooledSampleId) {
    return get(
        SAMPLESHEET_DATA_PREFIX + ".pooledsample" + pooledSampleId + ".index");
  }

  /**
   * Get the description of a pooled sample.
   * @param pooledSampleId the id of the pooled sample
   * @return the description of a pooled sample
   */
  public String getPooledSampleDescription(final int pooledSampleId) {
    return get(SAMPLESHEET_DATA_PREFIX
        + ".pooledsample" + pooledSampleId + ".description");
  }

  /**
   * Get the list of sample identifiers in a pooled sample.
   * @param pooledSampleId the id of the pooled sample
   * @return a list of sample identifiers
   */
  public List<Integer> getSamplesInPooledSample(final int pooledSampleId) {

    final List<Integer> result = new ArrayList<Integer>();

    for (String s : Splitter.on(',').trimResults().omitEmptyStrings()
        .split(get(SAMPLESHEET_DATA_PREFIX
            + ".pooledsample" + pooledSampleId + ".samples"))) {
      try {
        result.add(Integer.parseInt(s));
      } catch (NumberFormatException e) {
        // Do nothing
      }
    }

    return result;
  }

  /**
   * Get all samples ids (with undetermined samples) of the run.
   * @return all samples related to the lane
   */
  public List<Integer> getAllPooledSamples() {

    final List<Integer> result = new ArrayList<Integer>();
    final int pooledSampleCount = getPooledSampleCount();

    for (int i = 1; i <= pooledSampleCount; i++) {
      result.add(i);
    }

    return result;
  }

  /**
   * Get all samples ids (without undetermined samples) of the run.
   * @return all samples related to the lane
   */
  public List<Integer> getPooledSamples() {

    final List<Integer> result = new ArrayList<Integer>();
    final int pooledSampleCount = getPooledSampleCount();

    for (int i = 1; i <= pooledSampleCount; i++) {
      if (!isUndeterminedPooledSample(i)) {
        result.add(i);
      }
    }

    return result;
  }

  /**
   * Test if the sample is an undetermined sample.
   * @param pooledSampleId the pooled sample id
   * @return sequence index related to the lane and sample name
   */
  public boolean isUndeterminedPooledSample(final int pooledSampleId) {
    return getBoolean(SAMPLESHEET_DATA_PREFIX
        + ".pooledsample" + pooledSampleId + ".undetermined");
  }

  //
  // Project getters
  //

  /**
   * Get the project count.
   * @return the project count
   */
  public int getProjectCount() {
    return getInt(SAMPLESHEET_DATA_PREFIX + ".project.count");
  }

  /**
   * Get all project identifiers of the run.
   * @return all project identifiers of the run
   */
  public List<Integer> getProjects() {

    final List<Integer> result = new ArrayList<>();

    final int projectCount = getProjectCount();

    for (int i = 1; i <= projectCount; i++) {
      result.add(i);
    }

    return result;
  }

  /**
   * Get the name of a project.
   * @param projectId the id of the project
   * @return the name of a project
   */
  public String getProjectName(final int projectId) {
    return get(SAMPLESHEET_DATA_PREFIX + ".project" + projectId + ".name");
  }

  /**
   * Get the id of a project from its name.
   * @param projectName the name of the project
   * @return the id of the projet or -1 if not found
   */
  public int getProjectId(final String projectName) {

    if (projectName == null) {
      return -1;
    }

    final int projectCount = getProjectCount();

    for (int i = 1; i <= projectCount; i++) {

      if (projectName
          .equals(get(SAMPLESHEET_DATA_PREFIX + ".project" + i + ".name"))) {
        return i;
      }

    }

    return -1;
  }

  /**
   * Get the list of the samples of a project.
   * @param projectId the project id
   * @return a list with the sample identifiers of the project
   */
  public List<Integer> getSamplesInProject(final int projectId) {

    final List<Integer> result = new ArrayList<Integer>();

    for (String s : Splitter.on(',').trimResults().omitEmptyStrings().split(
        get(SAMPLESHEET_DATA_PREFIX + ".project" + projectId + ".samples"))) {
      try {
        result.add(Integer.parseInt(s));
      } catch (NumberFormatException e) {
        // Do nothing
      }
    }

    return result;
  }

  //
  // Run info getters
  //

  /**
   * Get the read count in run.
   * @return read count.
   */
  public int getReadCount() {
    return getInt("run.info.read.count");
  }

  /**
   * Get the indexed read count in run.
   * @return indexed read count.
   */
  public int getIndexedReadCount() {
    return getInt("run.info.indexed.read.count");
  }

  /**
   * Get the lane count in run.
   * @return lane count
   */
  public int getLaneCount() {
    return getInt("run.info.flow.cell.lane.count");
  }

  /**
   * Check if the read is indexed.
   * @param read the read number
   * @return true if the read is indexed,otherwise false
   */
  public boolean isReadIndexed(final int read) {
    return getBoolean("run.info.read" + read + ".indexed");
  }

  /**
   * Gets the run tiles per lane.
   * @return the run tiles per lane
   */
  public int getRunTilesPerLane() {

    return getInt("run.info.tiles.per.lane.count");
  }

  /**
   * Get the run mode.
   * @return run mode
   */
  public String getRunMode() {
    return get("run.info.run.mode");
  }

  /**
   * Gets the sequencer name.
   * @return the sequencer name
   */
  public String getSequencerName() {
    return get("run.info.sequencer.name");
  }

  /**
   * Gets the sequencer family.
   * @return the sequencer name
   */
  public String getSequencerFamily() {
    return get("run.info.sequencer.family");
  }

  /**
   * Gets the sequencer application name.
   * @return the sequencer application name
   */
  public String getSequencerApplicationName() {
    return get("run.info.application.name");
  }

  /**
   * Gets the sequencer application version.
   * @return the sequencer application version
   */
  public String getSequencerApplicationVersion() {
    return get("run.info.application.version");
  }

  /**
   * Gets the RTA version.
   * @return the RTA version
   */
  public String getSequencerRTAVersion() {
    return get("run.info.rta.version");
  }

  /**
   * Gets the bcl2fastq version.
   * @return the bcl2fastq version
   */
  public String getBcl2FastqVersion() {
    return get("demux.bcl2fastq.version");
  }

  /**
   * Get the cycles count in the read.
   * @param read the read number
   * @return cycles count in the read
   */
  public int getReadCyclesCount(final int read) {
    return getInt("run.info.read" + read + ".cycles");
  }

  /**
   * Gets the flowcell id.
   * @return the flowcell id
   */
  public String getFlowcellId() {

    return get("run.info.flow.cell.id");
  }

  //
  // Demux getters
  //

  /**
   * Get the raw cluster count for a sample.
   * @param sampleId the sampleId
   * @param read the read
   * @return the raw cluster count of the sample
   */
  public int getSampleRawClusterCount(final int sampleId, final int read) {

    return getInt(
        "demux.sample" + sampleId + ".read" + read + ".raw.cluster.count");
  }

  /**
   * Get the passing filter cluster count for a sample.
   * @param sampleId the sample id
   * @param read the read
   * @return the passing filter cluster count of the sample
   */
  public int getSamplePFClusterCount(final int sampleId, final int read) {

    return getInt(
        "demux.sample" + sampleId + ".read" + read + ".pf.cluster.count");
  }

  //
  // Read getters
  //

  /**
   * Get the tiles count.
   * @return tiles count
   */
  public int getTilesCount() {
    return this
        .getInt(ReadCollector.READ_DATA_PREFIX + ".read1.lane1.tile.count");
  }

  /**
   * Get the percent align Phix related to the lane and the read.
   * @param lane the lane number
   * @param read the read number
   * @return percent align Phix related to the lane and the read
   */
  public double getReadPrcAlign(final int lane, final int read) {
    return getDouble(
        READ_DATA_PREFIX + ".read" + read + ".lane" + lane + ".prc.align");
  }

  /**
   * Get the raw cluster count related to a lane and a read.
   * @param lane the lane number
   * @param read the read number
   * @return the raw cluster count related to a lane and a read
   */
  public long getReadRawClusterCount(final int lane, final int read) {
    return getLong(
        READ_DATA_PREFIX + ".read" + read + ".lane" + lane + ".clusters.raw");
  }

  /**
   * Get the passing filter cluster count related to a lane and a read.
   * @param lane the lane number
   * @param read the read number
   * @return the passing filter cluster count related to a lane and a read
   */
  public long getReadPFClusterCount(final int lane, final int read) {
    return getLong(
        READ_DATA_PREFIX + ".read" + read + ".lane" + lane + ".clusters.pf");
  }

  //
  // Undetermined getters
  //

  /**
   * Get the raw cluster recovery count for a sample.
   * @param sampleId the sample id
   * @return the raw cluster recovery count of the sample
   */
  public int getSampleRawClusterRecoveryCount(final int sampleId) {

    return getInt("undeterminedindices.sample"
        + sampleId + ".recoverable.raw.cluster.count");
  }

  /**
   * Get the passing filter cluster recovery count for a sample.
   * @param sampleId the sample id
   * @return the passing filter cluster recovery count of the sample
   */
  public int getSamplePFClusterRecoveryCount(final int sampleId) {

    return getInt("undeterminedindices.sample"
        + sampleId + ".recoverable.pf.cluster.count");
  }

  /**
   * Get the raw cluster recovery count for a lane.
   * @param lane the lane
   * @return the raw cluster recovery count of the lane
   */
  public int getLaneRawClusterRecoveryCount(final int lane) {

    return getInt(
        "undeterminedindices.lane" + lane + ".recoverable.raw.cluster.count");
  }

  /**
   * Get the passing filter cluster recovery count for a lane.
   * @param lane the lane
   * @return the passing filter cluster recovery count of the lane
   */
  public int getLanePFClusterRecoveryCount(final int lane) {

    return getInt(
        "undeterminedindices.lane" + lane + ".recoverable.pf.cluster.count");
  }

  //
  // FastqScreen getters
  //

  /**
   * Get the percent mapped read on dataset contaminants.
   * @param sampleId the sample Id
   * @param read the read number
   * @return the percent mapped read on dataset contaminants.
   */
  public double getPercentMappedReadOnContaminationSample(final int sampleId,
      final int read) {

    return getDouble("fastqscreen.sample"
        + sampleId + ".read" + read + ".mappedexceptgenomesample");
  }

  //
  // Getters
  //

  /**
   * Get a key.
   * @param key key name
   * @return the value of the data for the key
   */
  public String get(final String key) {

    if (key == null) {
      return null;
    }

    return this.map.get(key.toLowerCase().trim());
  }

  /**
   * Get a key.
   * @param key key name
   * @param defaultValue value to use if the key does not exist
   * @return the value of the data for the key
   */
  public String get(final String key, final String defaultValue) {

    if (!contains(key)) {
      return defaultValue;
    }

    return get(key);
  }

  /**
   * Get a boolean key.
   * @param key key name
   * @return the value of the data for the key
   */
  public boolean getBoolean(final String key) {

    return Boolean.parseBoolean(get(key));
  }

  /**
   * Get a boolean key.
   * @param key key name
   * @param defaultValue value to use if the key does not exist
   * @return the value of the data for the key
   */
  public boolean getBoolean(final String key, final boolean defaultValue) {

    if (!contains(key)) {
      return defaultValue;
    }

    return getBoolean(key);
  }

  /**
   * Get an integer key.
   * @param key key name
   * @return the value of the data for the key
   */
  public int getInt(final String key) {

    final String value = get(key);

    if (value == null) {
      throw new AozanRuntimeException(
          "DataRun getInt throw NullPointerException on this key " + key);
    }

    try {
      return Integer.parseInt(value);
    } catch (NumberFormatException e) {
      throw new AozanRuntimeException(
          "DataRun getInt throw NumberFormatException on this key "
              + key + " (value is " + value + ")");
    }
  }

  /**
   * Get an integer key.
   * @param key key name
   * @param defaultValue value to use if the key does not exist
   * @return the value of the data for the key
   */
  public int getInt(final String key, final int defaultValue) {

    if (!contains(key)) {
      return defaultValue;
    }

    return getInt(key);
  }

  /**
   * Get an longTable key.
   * @param key key name
   * @return the table of values of the data for the key
   */
  public long[] getLongArray(final String key) {

    final String value = get(key);

    if (value == null) {
      return null;
    }

    String[] values = value.split(",");
    long[] results = new long[values.length];
    for (int i = 0; i < results.length; i++) {
      try {
        results[i] = Long.parseLong(values[i].trim());
      } catch (NumberFormatException e) {
        throw new AozanRuntimeException(
            "DataRun getLongArray throw NumberFormatException on this key "
                + key + " (value is " + value + ")");
      }
    }

    return results;
  }

  /**
   * Get a long array key.
   * @param key key name
   * @param defaultValue value to use if the key does not exist
   * @return the value of the data for the key
   */
  public long[] getLongArray(final String key, final long[] defaultValue) {

    if (!contains(key)) {
      return defaultValue;
    }

    return getLongArray(key);
  }

  /**
   * Get a long key.
   * @param key key name
   * @return the value of the data for the key
   */
  public long getLong(final String key) {

    final String value = get(key);

    if (value == null) {
      throw new AozanRuntimeException(
          "DataRun getLong throw NullPointerException on this key " + key);
    }

    try {
      return Long.parseLong(value);
    } catch (NumberFormatException e) {
      throw new AozanRuntimeException(
          "DataRun getLong throw NumberFormatException on this key "
              + key + " (value is " + value + ")");
    }
  }

  /**
   * Get a long key.
   * @param key key name
   * @param defaultValue value to use if the key does not exist
   * @return the value of the data for the key
   */
  public long getLong(final String key, final long defaultValue) {

    if (!contains(key)) {

      return defaultValue;
    }

    return getLong(key);
  }

  /**
   * Get a float key.
   * @param key key name
   * @return the value of the data for the key
   */
  public float getFloat(final String key) {

    final String value = get(key);

    if (value == null) {
      throw new AozanRuntimeException(
          "DataRun getFloat throw NullPointerException on this key " + key);
    }

    try {
      return Float.parseFloat(value);
    } catch (NumberFormatException e) {
      throw new AozanRuntimeException(
          "DataRun getFloat throw NumberFormatException on this key "
              + key + " (value is " + value + ")");
    }
  }

  /**
   * Get a float key.
   * @param key key name
   * @param defaultValue value to use if the key does not exist
   * @return the value of the data for the key
   */
  public float getFloat(final String key, final float defaultValue) {

    if (!contains(key)) {
      return defaultValue;
    }

    return getFloat(key);
  }

  /**
   * Get a double key.
   * @param key key name
   * @return the value of the data for the key
   */
  public double getDouble(final String key) {

    final String value = get(key);

    if (value == null) {
      throw new AozanRuntimeException(
          "DataRun getDouble throw NullPointerException on this key " + key);
    }

    try {
      return Double.parseDouble(value);
    } catch (NumberFormatException e) {
      throw new AozanRuntimeException(
          "DataRun getDouble throw NumberFormatException on this key "
              + key + " (value is " + value + ")");
    }
  }

  /**
   * Get a double key.
   * @param key key name
   * @param defaultValue value to use if the key does not exist
   * @return the value of the data for the key
   */
  public double getDouble(final String key, final double defaultValue) {

    if (!contains(key)) {
      return defaultValue;
    }

    return getDouble(key);
  }

  //
  // Setters
  //

  /**
   * Set a key.
   * @param key key to set
   * @param value value of the key
   */
  public void put(final String key, final String value) {

    if (key == null) {
      return;
    }

    this.map.put(key.toLowerCase().trim(), value == null ? "" : value.trim());
  }

  /**
   * Set a key with a boolean value.
   * @param key key to set
   * @param boolValue value of the key
   */
  public void put(final String key, final boolean boolValue) {

    put(key, Boolean.toString(boolValue));
  }

  /**
   * Set a key with an integer value.
   * @param key key to set
   * @param intValue value of the key
   */
  public void put(final String key, final int intValue) {

    put(key, Integer.toString(intValue));
  }

  /**
   * Set a key with a long value.
   * @param key key to set
   * @param longValue value of the key
   */
  public void put(final String key, final long longValue) {

    put(key, Long.toString(longValue));
  }

  /**
   * Set a key with a float value.
   * @param key key to set
   * @param floatValue value of the key
   */
  public void put(final String key, final float floatValue) {

    put(key, Float.toString(floatValue));
  }

  /**
   * Set a key with a double value.
   * @param key key to set
   * @param doubleValue value of the key
   */
  public void put(final String key, final double doubleValue) {

    put(key, Double.toString(doubleValue));
  }

  /**
   * Set a key with an array of strings as value.
   * @param key key to set
   * @param strings list of the key
   */
  public void put(final String key, final String... strings) {

    if (strings == null) {
      put(key, (String) null);
    } else {
      put(key, Joiner.on(',').join(strings));
    }
  }

  /**
   * Set a key with an array of longs as value.
   * @param key key to set
   * @param longs list of the key
   */
  public void put(final String key, final long... longs) {

    if (longs == null) {
      put(key, (String) null);
    } else {
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < longs.length; i++) {
        if (i > 0) {
          sb.append(',');
        }
        sb.append(longs[i]);
      }
      put(key, sb.toString());
    }
  }

  /**
   * Set a key with a collection of strings as value.
   * @param key key to set
   * @param strings set of the key
   */
  public void put(final String key, final Collection<String> strings) {

    if (strings == null) {
      put(key, (String) null);
    } else {
      put(key, Joiner.on(',').join(strings));
    }
  }

  /**
   * Set the key and values of a RunData object in the current RunData.
   * @param data Data to add
   */
  public void put(final RunData data) {

    if (data == null) {
      return;
    }

    for (final Map.Entry<String, String> e : data.map.entrySet()) {
      this.map.put(e.getKey(), e.getValue());
    }
  }

  //
  // Other methods
  //

  /**
   * Test if the RunData contains a key.
   * @param key key name
   * @return true if the RunData object contains the key
   */
  public boolean contains(final String key) {

    if (key == null) {
      return false;
    }

    return this.map.containsKey(key.toLowerCase().trim());
  }

  /**
   * Remove a key from the Rundata.
   * @param key key name
   */
  public void remove(final String key) {

    requireNonNull(key);

    if (!this.map.containsKey(key)) {
      throw new IllegalArgumentException("Key does not exists: " + key);
    }

    this.map.remove(key.toLowerCase().trim());
  }

  /**
   * Remove a key from the Rundata if the key exists.
   * @param key key name
   */
  public void removeIfExists(final String key) {

    if (contains(key)) {
      remove(key);
    }
  }

  /**
   * Get the number of entries in RunData.
   * @return the number of entries
   */
  public int size() {

    return this.map.size();
  }

  @Override
  public String toString() {

    final StringBuilder sb = new StringBuilder();
    for (final Map.Entry<String, String> e : this.map.entrySet()) {
      sb.append(e.getKey());
      sb.append('=');
      sb.append(e.getValue());
      sb.append('\n');
    }

    return sb.toString();
  }

  /**
   * Create the data file.
   * @param fileName path destination
   * @throws IOException Signals that an I/O exception has occurred.
   */
  public void createRunDataFile(final String fileName) throws IOException {
    createRunDataFile(new File(fileName));
  }

  /**
   * Create the data file.
   * @param fileName file destination
   * @throws IOException Signals that an I/O exception has occurred.
   */
  public void createRunDataFile(final File fileName) throws IOException {

    if (fileName == null) {
      throw new NullPointerException();
    }

    if (fileName.isDirectory()) {
      throw new IOException();
    }

    BufferedWriter bw;

    bw = Files.newWriter(fileName, Globals.DEFAULT_FILE_ENCODING);
    bw.write(this.toString());
    bw.close();

  }

  /**
   * Add the data file in the rundata.
   * @param fileName file source
   * @throws IOException if an error occurs while reading the data file
   */
  public void addDataFileInRundata(final String fileName) throws IOException {
    addDataFileInRundata(new File(fileName));
  }

  /**
   * Add the data file in the rundata.
   * @param file file source
   * @throws IOException if an error occurs while reading the data file
   */
  public void addDataFileInRundata(final File file) throws IOException {

    if (file == null) {
      throw new NullPointerException("The file parameter is null");
    }

    checkExistingFile(file, " rundata file ");

    final BufferedReader br =
        Files.newReader(file, Globals.DEFAULT_FILE_ENCODING);

    String line;

    while ((line = br.readLine()) != null) {

      final int pos = line.indexOf('=');
      if (pos == -1) {
        continue;
      }

      final String key = line.substring(0, pos);
      final String value = line.substring(pos + 1);

      put(key, value);
    }
    br.close();
  }

  /**
   * Print the content of the object on standard output.
   */
  public void print() {

    for (final Map.Entry<String, String> e : this.map.entrySet()) {
      System.out.println(e.getKey() + "=" + e.getValue());
    }
  }

  /**
   * Gets the map.
   * @return the map
   */
  public Map<String, String> getMap() {

    return Collections.unmodifiableMap(map);
  }

  //
  // Constructor
  //

  /**
   * Public constructor.
   */
  public RunData() {
  }

  /**
   * Public constructor.
   * @param data Data to add
   */
  public RunData(final RunData data) {
    put(data);
  }

  /**
   * Public constructor.
   * @param file file to read
   * @throws IOException if an error occurs while reading the data file
   */
  public RunData(final File file) throws IOException {

    if (file == null) {
      throw new NullPointerException("The file parameter is null");
    }

    final BufferedReader br =
        Files.newReader(file, Globals.DEFAULT_FILE_ENCODING);

    String line;

    while ((line = br.readLine()) != null) {

      final int pos = line.indexOf('=');
      if (pos == -1) {
        continue;
      }

      final String key = line.substring(0, pos);
      final String value = line.substring(pos + 1);

      put(key, value);
    }
    br.close();
  }

}
