/*
 * Copyright 2006-2015 The MZmine 2 Development Team
 * 
 * This file is part of MZmine 2.
 * 
 * MZmine 2 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 2 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 2; if not, write to the Free Software Foundation, Inc., 51 Franklin St,
 * Fifth Floor, Boston, MA 02110-1301 USA
 */

package net.sf.mzmine.modules.visualization.ida;

import java.awt.Color;

import net.sf.mzmine.datamodel.DataPoint;
import net.sf.mzmine.datamodel.RawDataFile;
import net.sf.mzmine.datamodel.Scan;
import net.sf.mzmine.main.MZmineCore;
import net.sf.mzmine.parameters.parametertypes.tolerances.MZTolerance;
import net.sf.mzmine.taskcontrol.Task;
import net.sf.mzmine.taskcontrol.TaskPriority;
import net.sf.mzmine.taskcontrol.TaskStatus;

import org.jfree.data.xy.AbstractXYDataset;

import com.google.common.collect.Range;

/**
 * 
 */
class IDADataSet extends AbstractXYDataset implements Task {

    private static final long serialVersionUID = 1L;

    // For comparing small differences.
    private static final double EPSILON = 0.0000001;

    private RawDataFile rawDataFile;
    private Range<Double> totalRTRange, totalMZRange;
    private int allScanNumbers[], msmsScanNumbers[], totalScans,
	    totalmsmsScans, processedScans, allProcessedScans, processedColors,
	    totalEntries, lastMSIndex;
    private final double[] rtValues, mzValues, intensityValues;
    private IntensityType intensityType;
    private NormalizationType normalizationType;
    private final int[] scanNumbers;
    private double minPeakInt, maxIntensity;
    private Color[] colorValues;

    private TaskStatus status = TaskStatus.WAITING;

    IDADataSet(RawDataFile rawDataFile, Range<Double> rtRange,
	    Range<Double> mzRange, IntensityType intensityType,
	    NormalizationType normalizationType, Double minPeakInt,
	    IDAVisualizerWindow visualizer) {

	this.rawDataFile = rawDataFile;

	totalRTRange = rtRange;
	totalMZRange = mzRange;
	this.intensityType = intensityType;
	this.normalizationType = normalizationType;
	this.minPeakInt = minPeakInt - EPSILON;

	allScanNumbers = rawDataFile.getScanNumbers();
	msmsScanNumbers = rawDataFile.getScanNumbers(2, rtRange);

	totalScans = allScanNumbers.length;
	totalmsmsScans = msmsScanNumbers.length;
	totalEntries = totalmsmsScans;

	scanNumbers = new int[totalScans];
	rtValues = new double[totalmsmsScans];
	mzValues = new double[totalmsmsScans];
	intensityValues = new double[totalmsmsScans];
	colorValues = new Color[totalmsmsScans];

	MZmineCore.getTaskController().addTask(this, TaskPriority.HIGH);

    }

    @Override
    public void run() {

	status = TaskStatus.PROCESSING;
	double totalScanIntensity, maxPeakIntensity;

	for (int index = 0; index < totalScans; index++) {

	    // Cancel?
	    if (status == TaskStatus.CANCELED)
		return;

	    Scan scan = rawDataFile.getScan(allScanNumbers[index]);

	    if (scan.getMSLevel() == 1) {
		// Store info about MS spectra for MS/MS to allow extraction of
		// intensity of precursor ion in MS scan.
		lastMSIndex = index;
	    } else {
		Double precursorMZ = scan.getPrecursorMZ(); // Precursor m/z
							    // value
		Double scanRT = scan.getRetentionTime(); // Scan RT

		// Calculate total intensity
		totalScanIntensity = 0;
		if (intensityType == IntensityType.MS) {
		    // Get intensity of precursor ion from MS scan
		    Scan msscan = rawDataFile
			    .getScan(allScanNumbers[lastMSIndex]);
		    Double mzTolerance = precursorMZ * 10 / 1000000;
		    Range<Double> precursorMZRange = Range.closed(precursorMZ
			    - mzTolerance, precursorMZ + mzTolerance);
		    DataPoint scanDataPoints[] = msscan
			    .getDataPointsByMass(precursorMZRange);
		    for (int x = 0; x < scanDataPoints.length; x++) {
			totalScanIntensity = totalScanIntensity
				+ scanDataPoints[x].getIntensity();
		    }
		} else if (intensityType == IntensityType.MSMS) {
		    // Get total intensity of all peaks in MS/MS scan
		    DataPoint scanDataPoints[] = scan.getDataPoints();
		    for (int x = 0; x < scanDataPoints.length; x++) {
			totalScanIntensity = totalScanIntensity
				+ scanDataPoints[x].getIntensity();
		    }
		}

		maxPeakIntensity = 0;
		DataPoint scanDataPoints[] = scan.getDataPoints();
		for (int x = 0; x < scanDataPoints.length; x++) {
		    if (maxPeakIntensity < scanDataPoints[x].getIntensity()) {
			maxPeakIntensity = scanDataPoints[x].getIntensity();
		    }
		}

		if (totalRTRange.contains(scanRT)
			&& totalMZRange.contains(precursorMZ)
			&& maxPeakIntensity > minPeakInt) {
		    // Add values to arrays
		    rtValues[processedScans] = scanRT;
		    mzValues[processedScans] = precursorMZ;
		    intensityValues[processedScans] = totalScanIntensity;
		    scanNumbers[processedScans] = index + 1; // +1 because loop
							     // runs from 0 not
							     // 1
		    processedScans++;
		}

	    }
	    allProcessedScans++;

	}

	// Update max Z values
	for (int row = 0; row < totalmsmsScans; row++) {
	    if (maxIntensity < intensityValues[row]) {
		maxIntensity = intensityValues[row];
	    }
	}

	// Update color table for all spots
	totalEntries = processedScans - 1;
	for (int index = 0; index < processedScans - 1; index++) {

	    // Cancel?
	    if (status == TaskStatus.CANCELED)
		return;

	    double maxIntensityVal = 1;

	    if (normalizationType == NormalizationType.all) {
		// Normalize based on all m/z values
		maxIntensityVal = maxIntensity;
	    } else if (normalizationType == NormalizationType.similar) {
		// Normalize based on similar m/z values
		double precursorMZ = mzValues[index];
		Double mzTolerance = precursorMZ * 10 / 1000000;
		Range<Double> precursorMZRange = Range.closed(precursorMZ
			- mzTolerance, precursorMZ + mzTolerance);
		maxIntensityVal = (double) getMaxZ(precursorMZRange);
	    }

	    // Calculate normalized intensity
	    double normIntensity = (double) intensityValues[index]
		    / maxIntensityVal;
	    if (normIntensity > 1) {
		normIntensity = 1;
	    }

	    // Convert normIntensity into gray color tone
	    // RGB tones go from 0 to 255 - we limit it to 220 to not include
	    // too light colors
	    int rgbVal = (int) Math.round(220 - normIntensity * 220);

	    // Update color table
	    colorValues[index] = new Color(rgbVal, rgbVal, rgbVal);

	    processedColors++;
	}

	fireDatasetChanged();
	status = TaskStatus.FINISHED;

    }

    public int getSeriesCount() {
	return 1;
    }

    public Comparable<?> getSeriesKey(int series) {
	return rawDataFile.getName();
    }

    public int getItemCount(int series) {
	return totalmsmsScans;
    }

    public Number getX(int series, int item) {
	return rtValues[item];
    }

    public Number getY(int series, int item) {
	return mzValues[item];
    }

    public Number getZ(int series, int item) {
	return intensityValues[item];
    }

    public Number getMaxZ() {
	return maxIntensity;
    }

    public Number getMaxZ(Range<Double> mzRange) {
	double max = 1.0;
	for (int row = 0; row < totalmsmsScans; row++) {
	    if (mzRange.contains(mzValues[row])) {
		if (max < intensityValues[row]) {
		    max = intensityValues[row];
		}
	    }
	}
	return max;
    }

    public Color getColor(int series, int item) {
	return colorValues[item];
    }

    public void setColor(int series, int item, Color c) {
	colorValues[item] = c;
    }

    /**
     * Highlights all MS/MS spots for which a peak is found in the MS/MS
     * spectrum with the m/z value
     *
     * @param mz
     *            m/z.
     * @param ppm
     *            ppm value.
     * @param neutralLoss
     *            true or false.
     * @param c
     *            color.
     * 
     */
    public void highlightSpectra(double mz, MZTolerance searchMZTolerance, double minIntensity,
	    boolean neutralLoss, Color c) {
	// mzRange
	searchMZTolerance.getToleranceRange(mz);
	Range<Double> precursorMZRange = searchMZTolerance.getToleranceRange(mz);

	// Loop through all scans
	for (int row = 0; row < scanNumbers.length; row++) {
	    Scan msscan = rawDataFile.getScan(scanNumbers[row]);

	    // Get total intensity of all peaks in MS/MS scan
	    if (scanNumbers[row] > 0) {
		DataPoint scanDataPoints[] = msscan.getDataPoints();
		double selectedIons[] = new double[scanDataPoints.length];
		int ions = 0;
		boolean colorSpectra = false;

		// Search for neutral loss in ms/ms spectrum
		if (neutralLoss) {
		    for (int x = 0; x < scanDataPoints.length; x++) {
			if (scanDataPoints[x].getIntensity() > minIntensity) {
			    selectedIons[ions] = scanDataPoints[x].getMZ();
			    ions++;
			}
		    }

		    if (ions > 1) {
			double ionDiff[][] = new double[ions][ions];
			for (int x = 0; x < ions; x++) {
			    for (int y = 0; y < ions; y++) {
				ionDiff[x][y] = Math.abs(selectedIons[x]
					- selectedIons[y]);
			    }
			}

			for (int x = 0; x < ions; x++) {
			    for (int y = 0; y < ions; y++) {
				if (precursorMZRange.contains(ionDiff[x][y])) {
				    colorSpectra = true;
				    break;
				}
			    }
			}
		    }
		}

		// Search for specific ion in ms/ms spectrum
		else {
		    for (int x = 0; x < scanDataPoints.length; x++) {
			if (precursorMZRange
				.contains(scanDataPoints[x].getMZ())
				&& scanDataPoints[x].getIntensity() > minIntensity) {
			    colorSpectra = true;
			    break;
			}
		    }
		}

		if (colorSpectra) {
		    // If color is red green or blue then use toning from
		    // current color
		    int rgb = getColor(0, row).getRed();
		    if (c == Color.red) {
			setColor(0, row, new Color(255, rgb, rgb));
		    } else if (c == Color.green) {
			setColor(0, row, new Color(rgb, 255, rgb));
		    } else if (c == Color.blue) {
			setColor(0, row, new Color(rgb, rgb, 255));
		    } else {
			setColor(0, row, c);
		    }
		}

	    }

	}

	fireDatasetChanged();
    }

    public RawDataFile getDataFile() {
	return rawDataFile;
    }

    public int getScanNumber(final int item) {
	return scanNumbers[item];
    }

    /**
     * Returns index of data point which exactly matches given X and Y values
     *
     * @param retentionTime
     *            retention time.
     * @param mz
     *            m/z.
     * @return the nearest data point index.
     */
    public int getIndex(final double retentionTime, final double mz) {

	int index = -1;
	for (int i = 0; index < 0 && i < processedScans; i++) {

	    if (Math.abs(retentionTime - rtValues[i]) < EPSILON
		    && Math.abs(mz - mzValues[i]) < EPSILON) {

		index = i;
	    }
	}

	return index;
    }

    @Override
    public void cancel() {
	status = TaskStatus.CANCELED;
    }

    @Override
    public String getErrorMessage() {
	return null;
    }

    @Override
    public double getFinishedPercentage() {
	if (totalScans == 0) {
	    return 0;
	}
	return (double) 0.5 * (allProcessedScans / totalScans) + 0.5
		* (100 * processedColors / totalEntries) / 100;
    }

    @Override
    public TaskStatus getStatus() {
	return status;
    }

    @Override
    public String getTaskDescription() {
	return "Updating IDA visualizer of " + rawDataFile;
    }

}