/*
 * Copyright 2006-2020 The MZmine Development Team
 *
 * This file is part of MZmine.
 *
 * MZmine is free software; you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * MZmine is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with MZmine; if not,
 * write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
 * USA
 */
package io.github.mzmine.modules.dataprocessing.align_path.functions;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Vector;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CyclicBarrier;
import io.github.mzmine.datamodel.PeakList;
import io.github.mzmine.datamodel.PeakListRow;
import io.github.mzmine.datamodel.RawDataFile;
import io.github.mzmine.datamodel.impl.SimplePeakList;
import io.github.mzmine.modules.dataprocessing.align_path.PathAlignerParameters;
import io.github.mzmine.modules.dataprocessing.align_path.scorer.RTScore;
import io.github.mzmine.parameters.ParameterSet;

public class ScoreAligner implements Aligner {

  public final static String name = "Aligner";
  private int peaksTotal;
  private int peaksDone;
  private volatile boolean aligningDone;
  private volatile Thread[] threads;
  private volatile List<List<PeakListRow>> peakList;
  private final List<PeakList> originalPeakList;
  private ScoreCalculator calc;
  private PeakList alignment;
  private ParameterSet params;

  public ScoreAligner(PeakList[] dataToAlign, ParameterSet params) {
    this.params = params;
    this.calc = new RTScore();
    originalPeakList = java.util.Collections.unmodifiableList(Arrays.asList(dataToAlign));
    copyAndSort(Arrays.asList(dataToAlign));
  }

  private void copyAndSort(List<PeakList> dataToAlign) {
    Comparator<PeakList> c = new Comparator<PeakList>() {

      @Override
      public int compare(PeakList o1, PeakList o2) {
        return o2.getNumberOfRows() - o1.getNumberOfRows();
      }
    };

    if (dataToAlign != null) {
      List<PeakList> copyOfData = new ArrayList<PeakList>(dataToAlign);
      java.util.Collections.sort(copyOfData, c);
      peakList = new ArrayList<List<PeakListRow>>();
      for (int i = 0; i < copyOfData.size(); i++) {
        PeakListRow[] peakData = copyOfData.get(i).getRows().toArray(PeakListRow[]::new);
        List<PeakListRow> peaksInOneFile = new LinkedList<PeakListRow>();
        peaksInOneFile.addAll(Arrays.asList(peakData));
        peakList.add(peaksInOneFile);
      }
    } else {
      peakList = null;
    }
  }

  private List<AlignmentPath> generatePathsThreaded(final ScoreCalculator c,
      final List<List<PeakListRow>> peaksToUse) throws CancellationException {
    final List<AlignmentPath> paths = Collections.synchronizedList(new LinkedList<AlignmentPath>());
    final List<AlignmentPath> completePaths = new ArrayList<AlignmentPath>();
    final int numThreads = Runtime.getRuntime().availableProcessors();
    final AlignerThread aligners[] = new AlignerThread[numThreads];

    Runnable barrierTask = new Runnable() {

      @Override
      public void run() {
        Collections.sort(paths);
        while (paths.size() > 0) {
          Iterator<AlignmentPath> iter = paths.iterator();
          AlignmentPath best = iter.next();
          iter.remove();
          while (iter.hasNext()) {
            AlignmentPath cand = iter.next();
            if (best.containsSame(cand)) {
              iter.remove();
            }
          }
          completePaths.add(best);
          removePeaks(best, peaksToUse);
          peaksDone += best.nonEmptyPeaks();
        }

        // Empty the list for further use
        paths.clear();
        int currentCol = -1;
        for (int i = 0; i < peaksToUse.size(); i++) {
          if (peaksToUse.get(i).size() > 0) {
            currentCol = i;
            break;
          }
        }
        if (currentCol == -1) {
          aligningDone = true;
          return;
        }

        ThreadInfo threadInfos[] = calculateIntervals(numThreads, currentCol, peaksToUse);
        for (int i = 0; i < numThreads; i++) {
          aligners[i].setThreadInfo(threadInfos[i]);
        }
      }
    };

    CyclicBarrier barrier = new CyclicBarrier(numThreads, barrierTask);

    // Preliminary setup of thread working
    {
      int currentCol = 0;
      ThreadInfo threadInfos[] = calculateIntervals(numThreads, currentCol, peaksToUse);
      for (int i = 0; i < numThreads; i++) {
        aligners[i] = new AlignerThread(threadInfos[i], barrier, paths, c, peaksToUse);
      }
    }

    threads = new Thread[aligners.length];
    for (int i = 0; i < aligners.length; i++) {
      threads[i] = (new Thread(aligners[i]));
      threads[i].start();
    }
    for (int i = 0; i < aligners.length; i++) {
      try {
        threads[i].join();
      } catch (InterruptedException e) {
        break;
        // TODO Add perhaps more resilence to unforeseen turns of
        // events.
        // At least now there should not be any case when this main
        // thread
        // would be interrupted.
      }
    }
    return completePaths;
  }

  private AlignmentPath generatePath(int col, ScoreCalculator c, PeakListRow base,
      List<List<PeakListRow>> listOfPeaksInFiles) {
    int len = listOfPeaksInFiles.size();
    AlignmentPath path = new AlignmentPath(len, base, col);
    for (int i = (col + 1) % len; i != col; i = (i + 1) % len) {

      PeakListRow bestPeak = null;
      double bestPeakScore = c.getWorstScore();
      for (PeakListRow curPeak : listOfPeaksInFiles.get(i)) {
        if (curPeak == null || !c.matches(path, curPeak, params)) {
          // Either there isn't any peak left or it doesn't fill
          // requirements of current score calculator (for example,
          // it doesn't have a name).
          continue;
        }
        double score = c.calculateScore(path, curPeak, params);

        if (score < bestPeakScore) {
          bestPeak = curPeak;
          bestPeakScore = score;
        }

      }

      double gapPenalty = 1.25;

      if (bestPeak != null && bestPeakScore < gapPenalty) {
        path.add(i, bestPeak, bestPeakScore);
      } else {
        path.addGap(i, gapPenalty);
      }

    }
    return path;
  }

  private void removePeaks(AlignmentPath p, List<List<PeakListRow>> listOfPeaks) {
    for (int i = 0; i < p.length(); i++) {
      PeakListRow d = p.getPeak(i);
      if (d != null) {
        listOfPeaks.get(i).remove(d);
      }
    }
  }

  private boolean aligningDone() {
    return aligningDone;
  }

  private ThreadInfo[] calculateIntervals(int threads, int col,
      List<List<PeakListRow>> listOfPeaks) {
    int diff = listOfPeaks.get(col).size() / threads;
    ThreadInfo threadInfos[] = new ThreadInfo[threads];
    for (int i = 0; i < threads; i++) {
      threadInfos[i] = new ThreadInfo(i * diff,
          ((i == threads - 1) ? listOfPeaks.get(col).size() : (i + 1) * diff), col);
    }
    return threadInfos;
  }

  @Override
  public double getProgress() {
    return ((double) this.peaksDone / (double) this.peaksTotal);
  }

  private List<AlignmentPath> getAlignmentPaths() throws CancellationException {
    List<AlignmentPath> paths = new ArrayList<AlignmentPath>();
    paths = generatePathsThreaded(calc, peakList);
    return paths;
  }

  /*
   * (non-Javadoc)
   *
   * @see gcgcaligner.AbstractAligner#align()
   */
  @Override
  public PeakList align() {

    if (alignment == null) // Do the actual alignment if we already do not
    // have the result
    {
      Vector<RawDataFile> allDataFiles = new Vector<RawDataFile>();
      for (PeakList list : this.originalPeakList) {
        allDataFiles.addAll(list.getRawDataFiles());
      }

      peaksTotal = 0;
      for (int i = 0; i < peakList.size(); i++) {
        peaksTotal += peakList.get(i).size();
      }
      alignment =
          new SimplePeakList(params.getParameter(PathAlignerParameters.peakListName).getValue(),
              allDataFiles.toArray(new RawDataFile[0]));

      List<AlignmentPath> addedPaths = getAlignmentPaths();
      int ID = 1;
      for (AlignmentPath p : addedPaths) {
        // Convert alignments to original order of files and add them to
        // final
        // Alignment data structure
        PeakListRow row = p.convertToAlignmentRow(ID++);
        alignment.addRow(row);

      }
    }

    PeakList curAlignment = alignment;
    return curAlignment;
  }

  @Override
  public String toString() {
    return getName();
  }

  @Override
  public String getName() {
    return calc == null ? name : calc.name();
  }

  public ParameterSet getParameters() {
    return params;
  }

  private class AlignerThread implements Runnable {

    private ThreadInfo ti;
    private CyclicBarrier barrier;
    private List<AlignmentPath> readyPaths;
    private ScoreCalculator calc;
    private List<List<PeakListRow>> listOfAllPeaks;

    public void setThreadInfo(ThreadInfo ti) {
      this.ti = ti;
    }

    public AlignerThread(ThreadInfo ti, CyclicBarrier barrier, List<AlignmentPath> readyPaths,
        ScoreCalculator c, List<List<PeakListRow>> peakList) {
      this.ti = ti;
      this.barrier = barrier;
      this.readyPaths = readyPaths;
      this.calc = c;
      this.listOfAllPeaks = peakList;
    }

    private void align() {
      List<PeakListRow> myList =
          listOfAllPeaks.get(ti.currentColumn()).subList(ti.startIx(), ti.endIx());
      Queue<AlignmentPath> myPaths = new LinkedList<AlignmentPath>();
      for (PeakListRow cur : myList) {
        if (cur != null) {
          AlignmentPath p = generatePath(ti.currentColumn(), calc, cur, listOfAllPeaks);
          if (p != null) {
            myPaths.offer(p);
          }
        }
      }
      readyPaths.addAll(myPaths);
    }

    @Override
    public void run() {
      while (!aligningDone()) {
        align();
        // Exceptions cause wrong results but do not report
        // that in any way
        try {
          barrier.await();
        } catch (InterruptedException e) {
          return;
        } catch (BrokenBarrierException e2) {
          return;
        } catch (CancellationException e3) {
          return;
        }
      }
    }
  }

  private static class ThreadInfo {

    /**
     * Start index is inclusive, end index exclusive.
     */
    private int startIx;
    private int endIx;
    private int column;

    public ThreadInfo(int startIx, int endIx, int col) {
      this.startIx = startIx;
      this.endIx = endIx;
      this.column = col;
    }

    public int currentColumn() {
      return column;
    }

    public int endIx() {
      return endIx;
    }

    public int startIx() {
      return startIx;
    }

  }

  public boolean isConfigurable() {
    return true;
  }

  protected void resetThings() {
    copyAndSort(originalPeakList);
    alignment = null;
  }

  protected void doCancellingActions() {
    for (Thread th : threads) {
      if (th != null) {
        th.interrupt();
      }
    }

  }
}