/**
   * Writes the result HTML pages for the evaluation of multiple jobs/algorithms
   *
   * @param results The NemaEvaluationResultSet to write results pages for.
   * @param jobIDToResultPlotFileList map of a jobId to the results plots for that job.
   * @param summaryCsv the summary csv file that summarizes all jobs.
   * @param jobIDToPerTrackCSV map of jobId to individual per-track results csv files for that job.
   * @param jobIDToTgz map of jobId to the tar-balls of individual job results.
   * @param outputDir directory to write the HTML pages to.
   */
  private void writeHtmlAnalysisPages(NemaEvaluationResultSet results, File outputDir) {
    String jobId;
    Map<NemaTrackList, List<NemaData>> sysResults;
    List<Page> resultPages = new ArrayList<Page>();
    List<PageItem> items;
    Page aPage;
    int numJobs = results.getJobIds().size();

    TableItem legendTable = createLegendTable(results);

    // do intro page to describe task
    {
      resultPages.add(createIntroHtmlPage(results, legendTable));
    }

    /* Do per system pages */
    {
      for (Iterator<String> it = results.getJobIds().iterator(); it.hasNext(); ) {
        jobId = it.next();
        items = new ArrayList<PageItem>();
        sysResults = results.getPerTrackEvaluationAndResults(jobId);

        /* Plot onset transcription against GT for each track result for each system */
        PageItem[] plots = plotTranscriptionForJob(jobId, results);
        for (int i = 0; i < plots.length; i++) {
          items.add(plots[i]);
        }

        aPage =
            new Page(
                results.getJobName(jobId) + "_results", results.getJobName(jobId), items, true);
        resultPages.add(aPage);
      }
    }

    if (results.getJobIds().size() > 1) {
      // do comparative plot page
      {
        getLogger().info("Creating comparison plots page...");
        items = new ArrayList<PageItem>();
        PageItem[] plots = plotTranscriptionForAllJobs(results);
        for (int i = 0; i < plots.length; i++) {
          items.add(plots[i]);
        }
        getLogger().info("\tdone.");
        aPage = new Page("comparisonPlots", "Comparative plots", items, true);
        resultPages.add(aPage);
      }
    }

    Page.writeResultPages(results.getTask().getName(), outputDir, resultPages);
  }
  /**
   * Plots the chord transcriptions for all jobs, for each file.
   *
   * @param testSets the list of test sets.
   * @param results The results Object containing the data to plot.
   * @return an array of page items that will produce the plots.
   */
  @SuppressWarnings("unchecked")
  private PageItem[] plotTranscriptionForAllJobs(NemaEvaluationResultSet results) {
    NemaData groundtruth = null;

    /* Plot each result */
    Map<String, Map<NemaTrackList, List<NemaData>>> perTrackResults =
        results.getJobIdToPerTrackEvaluationAndResults();
    List<PageItem> plotItems = new ArrayList<PageItem>();

    Map<String, NemaData[]> trackIDToTranscripts = new HashMap<String, NemaData[]>();
    // get job names and sort
    List<String> jobNames = new ArrayList<String>(perTrackResults.keySet());
    Collections.sort(jobNames);

    for (Iterator<NemaTrackList> foldIt = results.getTestSetTrackLists().iterator();
        foldIt.hasNext(); ) {
      NemaTrackList testSet = foldIt.next();

      // map IDs for tracks to an array of NemaData Objects for each system, use nulls in case of
      // missing results
      for (Iterator<String> systemIt = perTrackResults.keySet().iterator(); systemIt.hasNext(); ) {
        String system = systemIt.next();
        int systemIdx = jobNames.indexOf(system);
        Map<NemaTrackList, List<NemaData>> sysResults = perTrackResults.get(system);
        List<NemaData> sysSetResults = sysResults.get(testSet);
        for (Iterator<NemaData> trackIt = sysSetResults.iterator(); trackIt.hasNext(); ) {
          NemaData track = trackIt.next();
          NemaData[] transcripts = trackIDToTranscripts.get(track.getId());
          if (transcripts == null) {
            transcripts = new NemaData[jobNames.size()];
            trackIDToTranscripts.put(track.getId(), transcripts);
          }
          transcripts[systemIdx] = track;
        }
      }
    }

    // iterate over tracks (in alphabetical order) and produce each plot
    List<String> trackIds = new ArrayList<String>(trackIDToTranscripts.keySet());
    Collections.sort(trackIds);

    for (Iterator<String> trackIt = trackIds.iterator(); trackIt.hasNext(); ) {
      String trackId = trackIt.next();
      NemaData[] transcripts = trackIDToTranscripts.get(trackId);
      getLogger().info("\t\tplotting track " + trackId + "...");

      // setup data-series to plot
      Map<String, double[]> series = new HashMap<String, double[]>(2);
      List<String> seriesNames = new ArrayList<String>(1 + transcripts.length);
      List<Boolean> isGroundtruth = new ArrayList<Boolean>(1 + transcripts.length);

      // setup time line for for X-axis
      double startTimeSecs = 0.0;
      double endTimeSecs = 0;

      if (results.getTrackIDToGT() != null) {
        groundtruth = results.getTrackIDToGT().get(trackId);
      }
      if (groundtruth == null) {
        getLogger().warning("No ground-truth found for '" + trackId + "' to be used in plotting");
      }
      double[][] rawGtData2D = null;
      String[] annotators = null;
      if (groundtruth != null && groundtruth.hasMetadata(NemaDataConstants.ONSET_DETECTION_DATA)) {
        rawGtData2D = (double[][]) groundtruth.getMetadata(NemaDataConstants.ONSET_DETECTION_DATA);

        if (groundtruth.hasMetadata(NemaDataConstants.ONSET_DETECTION_ANNOTATORS)) {
          annotators =
              groundtruth.getStringArrayMetadata(NemaDataConstants.ONSET_DETECTION_ANNOTATORS);
        }

        int numGt = rawGtData2D[0].length;
        for (int i = 0; i < numGt; i++) {
          double[] rawGtData = new double[rawGtData2D.length];
          int trim = -1;
          for (int j = 0; j < rawGtData2D.length; j++) {
            rawGtData[j] = rawGtData2D[j][i];
            if (Double.isNaN(rawGtData[j])) {
              trim = j;
              break;
            }
            endTimeSecs = Math.max(rawGtData[j], endTimeSecs);
          }
          if (trim != -1) {
            double[] trimmedData = new double[trim];
            for (int j = 0; j < trimmedData.length; j++) {
              trimmedData[j] = rawGtData[j];
            }
            rawGtData = trimmedData;
          }
          if (annotators != null) {
            String annotator = annotators[i];
            series.put(annotator, rawGtData);
            seriesNames.add(annotator);
          } else {
            series.put("Ground-truth " + i, rawGtData);
            seriesNames.add("Ground-truth " + i);
          }
          isGroundtruth.add(true);
        }
      } else {
        getLogger().warning("No ground-truth found for '" + trackId + "' to be used in plotting");
      }

      // iterate through systems
      for (int i = 0; i < transcripts.length; i++) {
        NemaData nemaData = transcripts[i];
        Object rawDataObj = nemaData.getMetadata(NemaDataConstants.ONSET_DETECTION_DATA);
        if (rawDataObj != null) {
          double[][] rawData2D = (double[][]) rawDataObj;
          double[] rawData = new double[rawData2D.length];
          for (int j = 0; j < rawData.length; j++) {
            rawData[j] = rawData2D[j][0];

            endTimeSecs = Math.max(rawData[j], endTimeSecs);
          }

          series.put(jobNames.get(i), rawData);
          seriesNames.add(jobNames.get(i));
          isGroundtruth.add(false);
        }
      }

      try {
        ProtovisOnsetPlotItem plot =
            new ProtovisOnsetPlotItem(
                // plotname
                "transcription_" + trackId,
                // plot caption
                "Onset transcriptions for track " + trackId,
                // start time for x axis
                startTimeSecs,
                // end time for x axis
                endTimeSecs,
                // map of annotator/system names to the data
                series,
                // ordered list of annotator/system names
                seriesNames,
                // flags indicating whether a series is ground-truth
                isGroundtruth,
                // Directory to write JS data files to
                outputDir);
        plotItems.add(plot);
      } catch (IOException e) {
        getLogger().log(Level.SEVERE, "Failed to plot results for track " + trackId, e);
      }
    }
    return plotItems.toArray(new PageItem[plotItems.size()]);
  }
  @Override
  public void renderResults(NemaEvaluationResultSet results) throws IOException {

    getLogger().info("Creating system result directories...");
    Map<String, File> jobIDToResultDir = makeSystemResultDirs(results);
    String jobId;

    /* Write out leaderboard CSV file */
    getLogger().info("Writing out leaderboard CSV...");
    File leaderboardCSV =
        this.writeLeaderBoardCSVFile(
            NemaDataConstants.ONSET_DETECTION_AVG_FMEASURE, results, false);

    /* Write out summary CSV and per-class CSV */
    getLogger().info("Writing out CSV result files over whole task...");
    List<String> metrics = new ArrayList<String>();
    metrics.add(NemaDataConstants.ONSET_DETECTION_AVG_FMEASURE);
    metrics.add(NemaDataConstants.ONSET_DETECTION_AVG_PRECISION);
    metrics.add(NemaDataConstants.ONSET_DETECTION_AVG_RECALL);

    File summaryCsv = new File(outputDir.getAbsolutePath() + File.separator + "summaryResults.csv");
    WriteCsvResultFiles.writeTableToCsv(
        WriteCsvResultFiles.prepSummaryTable(
            results.getJobIdToOverallEvaluation(), results.getJobIdToJobName(), metrics),
        summaryCsv);

    jobId = results.getJobIds().iterator().next();
    NemaData aggregateEval = results.getOverallEvaluation(jobId);
    List<String> classNames =
        (List<String>) aggregateEval.getMetadata(NemaDataConstants.ONSET_DETECTION_CLASSES);

    File perClassFMeasureCsv =
        new File(outputDir.getAbsolutePath() + File.separator + "PerClassFMeasure.csv");
    WriteCsvResultFiles.writeTableToCsv(
        WriteCsvResultFiles.prepTableDataOverClassArrays(
            results.getJobIdToOverallEvaluation(),
            results.getJobIdToJobName(),
            classNames,
            NemaDataConstants.ONSET_DETECTION_AVG_FMEASURE_BY_CLASS),
        perClassFMeasureCsv);

    File perClassPrecisionCsv =
        new File(outputDir.getAbsolutePath() + File.separator + "PerClassPrecision.csv");
    WriteCsvResultFiles.writeTableToCsv(
        WriteCsvResultFiles.prepTableDataOverClassArrays(
            results.getJobIdToOverallEvaluation(),
            results.getJobIdToJobName(),
            classNames,
            NemaDataConstants.ONSET_DETECTION_AVG_PRECISION_BY_CLASS),
        perClassPrecisionCsv);

    File perClassRecallCsv =
        new File(outputDir.getAbsolutePath() + File.separator + "PerClassRecall.csv");
    WriteCsvResultFiles.writeTableToCsv(
        WriteCsvResultFiles.prepTableDataOverClassArrays(
            results.getJobIdToOverallEvaluation(),
            results.getJobIdToJobName(),
            classNames,
            NemaDataConstants.ONSET_DETECTION_AVG_RECALL_BY_CLASS),
        perClassRecallCsv);

    /* Write out per track CSV for each system */
    getLogger().info("Writing out per-system result files...");
    Map<String, File> jobIDToPerTrackCSV = writePerTrackSystemResultCSVs(results, jobIDToResultDir);

    /* Create tar-balls of individual result directories */
    getLogger().info("Preparing evaluation data tarballs...");
    Map<String, File> jobIDToTgz = compressResultDirectories(jobIDToResultDir);

    /* Write result HTML pages */
    getLogger().info("Creating result HTML files...");
    writeResultHtmlPages(
        results,
        classNames,
        /*jobIDToResultPlotFileList, */ summaryCsv,
        perClassFMeasureCsv,
        perClassPrecisionCsv,
        perClassRecallCsv,
        jobIDToPerTrackCSV,
        jobIDToTgz,
        outputDir);

    getLogger().info("Done.");
  }
  /**
   * Plots the onset transcriptions for each job, for each file
   *
   * @param jobId the jobId we wish to plot results for.
   * @param results The results Object containing the data to plot.
   * @return an array of page items that will produce the plots.
   */
  private PageItem[] plotTranscriptionForJob(String jobId, NemaEvaluationResultSet results) {
    NemaData result, groundtruth = null;

    /* Plot each result */
    Map<NemaTrackList, List<NemaData>> job_results = results.getPerTrackEvaluationAndResults(jobId);
    List<PageItem> plotItems = new ArrayList<PageItem>();

    for (Iterator<NemaTrackList> foldIt = job_results.keySet().iterator(); foldIt.hasNext(); ) {
      NemaTrackList testSet = foldIt.next();
      for (Iterator<NemaData> iterator = job_results.get(testSet).iterator();
          iterator.hasNext(); ) {
        result = iterator.next();
        if (results.getTrackIDToGT() != null) {
          groundtruth = results.getTrackIDToGT().get(result.getId());
        }
        if (groundtruth == null) {
          getLogger()
              .warning("No ground-truth found for '" + result.getId() + "' to be used in plotting");
        }

        //				File plotFile = new File(sysDir.getAbsolutePath()
        //						+ File.separator + "track_" + result.getId() + MELODY_PLOT_EXT);
        //				plotItems.add(plotFile);

        double[][] rawData2D =
            result.get2dDoubleArrayMetadata(NemaDataConstants.ONSET_DETECTION_DATA);
        double[][] rawGtData2D = null;
        if (groundtruth != null) {
          rawGtData2D =
              groundtruth.get2dDoubleArrayMetadata(NemaDataConstants.ONSET_DETECTION_DATA);
        }
        String[] annotators = null;
        if (groundtruth != null
            && groundtruth.hasMetadata(NemaDataConstants.ONSET_DETECTION_ANNOTATORS)) {
          annotators =
              groundtruth.getStringArrayMetadata(NemaDataConstants.ONSET_DETECTION_ANNOTATORS);
        }
        // setup time line for for X-axis
        double startTimeSecs = 0.0;
        double endTimeSecs = 0.0;

        // Load in the prediction data
        double[] rawData = new double[rawData2D.length];
        for (int i = 0; i < rawData.length; i++) {
          rawData[i] = rawData2D[i][0];
          if (rawData2D[i][0] > endTimeSecs) {
            endTimeSecs = rawData2D[i][0];
          }
        }

        // setup hash map for to plot
        Map<String, double[]> series = new HashMap<String, double[]>(rawGtData2D[0].length + 1);
        List<String> seriesNames = new ArrayList<String>(rawGtData2D[0].length + 1);
        List<Boolean> isGroundtruth = new ArrayList<Boolean>(rawGtData2D[0].length + 1);
        if (groundtruth != null) {
          for (int curGT = 0; curGT < rawGtData2D[0].length; curGT++) {
            ArrayList<Double> gtDataArr = new ArrayList<Double>();
            for (int t = 0; t < rawGtData2D.length; t++) {
              double onTime = rawGtData2D[t][curGT];
              if (!Double.isNaN(onTime)) {
                gtDataArr.add(new Double(onTime));
                if (onTime > endTimeSecs) {
                  endTimeSecs = onTime;
                }
              }
            }
            double[] rawGtData = new double[gtDataArr.size()];
            for (int t = 0; t < rawGtData.length; t++) {
              rawGtData[t] = gtDataArr.get(t).doubleValue();
            }
            if (groundtruth.hasMetadata(NemaDataConstants.ONSET_DETECTION_ANNOTATORS)) {
              String annotator = annotators[curGT];
              series.put(annotator, rawGtData);
              seriesNames.add(annotator);
            } else {
              series.put("Ground-truth " + curGT, rawGtData);
              seriesNames.add("Ground-truth " + curGT);
            }
            isGroundtruth.add(true);
          }
        }

        series.put(results.getJobName(jobId), rawData);
        seriesNames.add(results.getJobName(jobId));
        isGroundtruth.add(false);

        try {
          ProtovisOnsetPlotItem plot =
              new ProtovisOnsetPlotItem(
                  // plotname
                  results.getJobName(jobId) + "_onset_transcript_" + result.getId(),
                  // plot caption
                  results.getJobName(jobId) + ": Onset transcription for track " + result.getId(),
                  // start time for x axis
                  startTimeSecs,
                  // end time for x axis
                  endTimeSecs,
                  // map of annotator/system names to the data
                  series,
                  // ordered list of annotator/system names
                  seriesNames,
                  // flags indicating whether a series is ground-truth
                  isGroundtruth,
                  // Directory to write JS data files to
                  outputDir);
          plotItems.add(plot);
        } catch (IOException e) {
          getLogger()
              .log(
                  Level.SEVERE,
                  "Failed to plot results for job "
                      + results.getJobName(jobId)
                      + " ("
                      + jobId
                      + ") for track "
                      + result.getId(),
                  e);
        }
      }
    }
    return plotItems.toArray(new PageItem[plotItems.size()]);
  }
  /**
   * Writes the result HTML pages for the evaluation of multiple jobs/algorithms
   *
   * @param results The NemaEvaluationResultSet to write results pages for.
   * @param jobIDToResultPlotFileList map of a jobId to the results plots for that job.
   * @param summaryCsv the summary csv file that summarizes all jobs.
   * @param jobIDToPerTrackCSV map of jobId to individual per-track results csv files for that job.
   * @param jobIDToTgz map of jobId to the tar-balls of individual job results.
   * @param outputDir directory to write the HTML pages to.
   */
  private void writeResultHtmlPages(
      NemaEvaluationResultSet results,
      List<String> classNames,
      /*Map<String, File[]> jobIDToResultPlotFileList, */ File summaryCsv,
      File perClassFMeasureCsv,
      File perClassPrecisionCsv,
      File perClassRecallCsv,
      Map<String, File> jobIDToPerTrackCSV,
      Map<String, File> jobIDToTgz,
      File outputDir) {
    String jobId;
    Map<NemaTrackList, List<NemaData>> sysResults;
    List<Page> resultPages = new ArrayList<Page>();
    List<PageItem> items;
    Page aPage;
    int numJobs = results.getJobIds().size();

    TableItem legendTable = createLegendTable(results);

    // do intro page to describe task
    {
      resultPages.add(createIntroHtmlPage(results, legendTable));
    }

    /* Do summary page */
    {
      items = new ArrayList<PageItem>();
      items.add(legendTable);

      List<String> metrics = new ArrayList<String>();
      metrics.add(NemaDataConstants.ONSET_DETECTION_AVG_FMEASURE);
      metrics.add(NemaDataConstants.ONSET_DETECTION_AVG_PRECISION);
      metrics.add(NemaDataConstants.ONSET_DETECTION_AVG_RECALL);

      Table summaryTable =
          WriteCsvResultFiles.prepSummaryTable(
              results.getJobIdToOverallEvaluation(), results.getJobIdToJobName(), metrics);

      items.add(
          new TableItem(
              "summary_results",
              "Summary Results",
              summaryTable.getColHeaders(),
              summaryTable.getRows()));
      aPage = new Page("summary", "Summary", items, false);
      resultPages.add(aPage);
    }

    /* Do per class page */
    {
      items = new ArrayList<PageItem>();
      items.add(legendTable);

      Table perClassFMeasureTable =
          WriteCsvResultFiles.prepTableDataOverClassArrays(
              results.getJobIdToOverallEvaluation(),
              results.getJobIdToJobName(),
              classNames,
              NemaDataConstants.ONSET_DETECTION_AVG_FMEASURE_BY_CLASS);

      Table perClassPrecisionTable =
          WriteCsvResultFiles.prepTableDataOverClassArrays(
              results.getJobIdToOverallEvaluation(),
              results.getJobIdToJobName(),
              classNames,
              NemaDataConstants.ONSET_DETECTION_AVG_PRECISION_BY_CLASS);

      Table perClassRecallTable =
          WriteCsvResultFiles.prepTableDataOverClassArrays(
              results.getJobIdToOverallEvaluation(),
              results.getJobIdToJobName(),
              classNames,
              NemaDataConstants.ONSET_DETECTION_AVG_RECALL_BY_CLASS);

      items.add(
          new TableItem(
              "fmeasure_class",
              "F-Measure per Class",
              perClassFMeasureTable.getColHeaders(),
              perClassFMeasureTable.getRows()));

      items.add(
          new TableItem(
              "precision_class",
              "Pecision per Class",
              perClassPrecisionTable.getColHeaders(),
              perClassPrecisionTable.getRows()));

      items.add(
          new TableItem(
              "recall_class",
              "Recall per Class",
              perClassRecallTable.getColHeaders(),
              perClassRecallTable.getRows()));

      aPage = new Page("results_per_class", "Results per Class", items, false);
      resultPages.add(aPage);
    }

    /* Do per system pages */
    {
      for (Iterator<String> it = results.getJobIds().iterator(); it.hasNext(); ) {
        jobId = it.next();
        items = new ArrayList<PageItem>();
        TableItem filtLegend = filterLegendTable(legendTable, jobId);
        if (filtLegend != null) {
          items.add(filtLegend);
        }
        sysResults = results.getPerTrackEvaluationAndResults(jobId);

        /* Add per track table */
        Table perTrackTable =
            WriteCsvResultFiles.prepTableDataOverTracks(
                results.getTestSetTrackLists(),
                sysResults,
                results.getTrackEvalMetricsAndResultsKeys());

        items.add(
            new TableItem(
                results.getJobName(jobId) + "_results",
                results.getJobName(jobId) + " Per Track Results",
                perTrackTable.getColHeaders(),
                perTrackTable.getRows()));

        /* Plot onset transcription against GT for each track result for each system */
        PageItem[] plots = plotTranscriptionForJob(jobId, results);
        for (int i = 0; i < plots.length; i++) {
          items.add(plots[i]);
        }

        aPage =
            new Page(
                results.getJobName(jobId) + "_results", results.getJobName(jobId), items, true);
        resultPages.add(aPage);
      }
    }

    if (results.getJobIds().size() > 1) {
      // do comparative plot page
      {
        getLogger().info("Creating comparison plots page...");
        items = new ArrayList<PageItem>();
        items.add(legendTable);
        PageItem[] plots = plotTranscriptionForAllJobs(results);
        for (int i = 0; i < plots.length; i++) {
          items.add(plots[i]);
        }
        getLogger().info("\tdone.");
        aPage = new Page("comparisonPlots", "Comparative plots", items, true);
        resultPages.add(aPage);
      }
    }

    /* Do files page */
    {
      items = new ArrayList<PageItem>();

      /* CSVs */
      List<String> CSVPaths = new ArrayList<String>(numJobs + 4);
      CSVPaths.add(IOUtil.makeRelative(summaryCsv, outputDir));
      CSVPaths.add(IOUtil.makeRelative(perClassFMeasureCsv, outputDir));
      CSVPaths.add(IOUtil.makeRelative(perClassPrecisionCsv, outputDir));
      CSVPaths.add(IOUtil.makeRelative(perClassRecallCsv, outputDir));
      for (Iterator<String> it = results.getJobIds().iterator(); it.hasNext(); ) {
        jobId = it.next();
        CSVPaths.add(IOUtil.makeRelative(jobIDToPerTrackCSV.get(jobId), outputDir));
      }

      items.add(new FileListItem("dataCSVs", "CSV result files", CSVPaths));

      /* System tar-balls */
      List<String> tarballPaths = new ArrayList<String>(numJobs);
      for (Iterator<String> it = results.getJobIds().iterator(); it.hasNext(); ) {
        jobId = it.next();
        tarballPaths.add(IOUtil.makeRelative(jobIDToTgz.get(jobId), outputDir));
      }
      items.add(new FileListItem("tarballs", "Per algorithm evaluation tarball", tarballPaths));
      aPage = new Page("files", "Raw data files", items, true);
      resultPages.add(aPage);
    }

    Page.writeResultPages(results.getTask().getName(), outputDir, resultPages);
  }