/**Class controls the Fly Carousel using serialIO, a QuickTime stream, and 
 *ImageJ. It is an ImageJ plugin. See  
 *ImageJ for documentation and 
 *to download ImageJ.
 * @version 1.0
 * @author Jim Burnette
 * Copyright (C), Jim Burnette and Jay Hirsh, University of Virginia. 
 * All rights reserved.
 */

import ij.*;
import ij.plugin.PlugIn;
import ij.gui.*;
import ij.process.*;
import ij.io.*;
import ij.text.*;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;

import edu.virginia.jburnette.imageProcessing.*;
import edu.virginia.jburnette.io.*;

public class Carousel_Imaging implements PlugIn {

    private MQTCapture          mqt;            //the Frame
    private Carousel_Imaging    thePlugIn;      
    private SerialReadWrite     readWrite;      //serial connection
    private String              savePath;       //image save path
    private int                 jpegQuailty = 75;   //used by ImageJ
    private int                 repeatCount;    //Number of revolutions completed
    private Vector      imageQueue;     //holds the imageHolder objects for processing
    private static final long   MINUTE = 60000;  //millisecs
    private boolean             countFlies, saveImages; //user preferences
    private double              dose, thwampAmt, numFlies;  //user specified stuff, not critical
    private double              numRepeats;     //minutes of image capture or total number of revolutions
    private String []           vialNames;      //user supplied info, not critical
    private File                file;
    private FileWriter          fout;
    private BufferedWriter      bout;
    private int                 imageNum = 0;   //equal to repeatCount
    /**
     *Called by ImageJ when the Plugin is started. Creates the viewing frame
     *by calling MQTCapture and 
     *sets up the parameters needed by the Plugin.
     *@param Ignore. This is used by ImageJ.
     */                 
    public void run(String arg) {
        thePlugIn = this;                           //keep track of plugin
        mqt = new MQTCapture("Fly Carousel", this); //start the QuickTime stream
        mqt.pack();                                 
        mqt.setVisible(true);                       //open the Frame
        openSerialPort();                           //try default port first.
        showDialog();                               //get user supplied info
        fileGetter();                               //get save location
        if(saveImages) {                            //user wants to save images
            createDirectories();
            createResultsFile();            //so create the individual vial 
        }               //directories. createResultsFile() will also be called.
        if(countFlies) {                    //user wants to count flies but not
            createResultsFile();            //save. Save only results
        }
        imageQueue = new Vector();
        IJ.register(Carousel_Imaging.class);        //IJ requirement
    }
    /**
     *A convience method to get help and debugging information to the frame.
     *@param message the message to display
     */
    public void showMessage(String message) {
       
       mqt.addMessage(message);
    }
    /**
     *Opens the serial port. Eventually it will pass a port number to the 
     *constructor. If the port did not open an 
     *edu.virginia.jburnette.PortNotFoundException is thrown by the 
     *SerialReadWrite constructor. This method must catch or throw
     *this exception.
     */
    public void openSerialPort() {
        try {
            readWrite = new SerialReadWrite();  //try to open the serial port.
            showMessage("The serial port is open.");  //if no exception then port opened
        } catch (PortNotFoundException e) {
    //If the default port failed to open a PortNotFoundException will be thrown.
    //Try to get a list of ports and try again.
            getPort();  
        } catch(Exception e) {
    //PortInUseException so just die. 
            //showMessage("Quit ImageJ and try again.");
            String s = e.getMessage();
            if (!(s == null)) {
            reportError("Cannot open serial port.\nCheck that motor controller box is on.\nCheck that the serial to USB cable is connected to the computer.\n Check that the serial cable is connected to the motor controller box and to the serial-to-USB connector.\nCheck for an Image Stream Error.\nIgnore all others. ", e);
        }
        }
    }
    public void reportError(String s, Exception e) {
       String es = e.getMessage();
       new TextWindow("Error", s, 400, 300);
       new TextWindow("Error", es, 400, 300);
    }
    /**
     *Opens the serial port with a specified port. If the port did not open an 
     *edu.virginia.jburnette.PortNotFoundException is thrown by the 
     *SerialReadWrite. Since the user specified port did not work
     *either then just kill the program.
     *@param port the port to open
     */
    public void openSerialPort(String port) {
        try {
        //try to open the serial port using specified port.
            readWrite = new SerialReadWrite(port);
        //if no exception then port opened
            showMessage("The serial port is open.");  
        } catch (Exception e) {
            //Give up
            String s = e.getMessage();
            if (!(s == null)) {
                 reportError("Cannot open serial port.\nCheck that motor controller box is on.\nCheck that the serial to USB cable is connected to the computer.\n Check that the serial cable is connected to the motor controller box and to the serial to USB connector.\nQuit ImageJ and restart computer.", e);
            }
        } 
    }
    //Get a list of serial ports and try opening that one.
    private void getPort() {
        GetPortList gl = new GetPortList();
        String[] list = gl.getPortList();
        GenericDialog gd = new GenericDialog("Choose a Port", IJ.getInstance());
            gd.addChoice("Choose a port with 'USA' in the name.", list, "");
        gd.showDialog();
        String port = gd.getNextChoice();
        openSerialPort(port);
    }
    private void showDialog() {
        GenericDialog gd = new GenericDialog(" Setup ", IJ.getInstance());
            gd.addCheckbox("Save Images?", true);
            //gd.addCheckbox("Count Flies?", false);
            gd.addNumericField("Number of minutes to record?", 5.0, 10);
            gd.addMessage("In the blanks below enter a label or genotype of each sample. The directories will be labeled Vial_#.");
            gd.addStringField("Vial 1", "1", 20);
            gd.addStringField("Vial 2", "2", 20);
            gd.addStringField("Vial 3", "3", 20);
            gd.addStringField("Vial 4", "4", 20);
            gd.addStringField("Vial 5", "5", 20);
            gd.addStringField("Vial 6", "6", 20);
            gd.addMessage("Enter the following for your documentation only. It is not used in any calculation.");
            gd.addNumericField("What cocaine dose are you using (in micrograms)?", 85, 10);
            gd.addNumericField("How much stimulation are you using (in Volts)?", 8, 10);
            gd.addNumericField("Number of flies in each vial?", 30, 10);
        gd.showDialog();
        vialNames =  new String[6];
        for (int i = 0; i < vialNames.length; i++) {
            vialNames[i] = gd.getNextString();
        }
        //if the dialog is cancelled defaults will be used.
        saveImages = gd.getNextBoolean();
        //countFlies = gd.getNextBoolean();
        numRepeats = gd.getNextNumber();
        dose = gd.getNextNumber();
        thwampAmt = gd.getNextNumber();
        numFlies = gd.getNextNumber();
    }
    private void fileGetter() {
            FileDialog fd = new FileDialog(mqt, "Choose a directory where to save images?", FileDialog.SAVE);   
            fd.show();
            savePath = fd.getDirectory();   //any file name entered is ignored.
            fd = null;
    }
    private void createDirectories() {      //create the six directories
        File f;
        if (savePath == null) {
            savePath = "/Users/flyuser/Desktop/no_name/";
            f = new File(savePath);
            f.mkdir();
        }
        for (int i = 0; i < 6; i++) {
            String newDir = (savePath + "Vial_" + Integer.toString(i+1));
            f = new File(newDir);
            f.mkdir();
        }
        f = null;   
    }
    private void createResultsFile() {
        try {
            file = new File(savePath, "results.csv");
            fout = new FileWriter(file);
            bout = new BufferedWriter(fout);
            //Print out the date and time
            String date = new Date().toString();
            bout.write("Results generated on " + date + "\n\n");
            bout.write("Dose (ug)," + Double.toString(dose) + "\n");
            bout.write("Thwamp amount (V)," + Double.toString(thwampAmt) + "\n");
            bout.write("Number flies per vial," + Double.toString(numFlies) + "\n");
            bout.write("Record time," + Double.toString(numRepeats) + "\n");
            bout.write("Vial 1, Vial 2, Vial 3, Vial 4, Vial 5, Vial 6\n");
            for (int i = 0; i < vialNames.length; i++) {
                bout.write(vialNames[i] + ",");
            }
            bout.write("\n");
            bout.flush();
        } catch(IOException ioe) {ioe.printStackTrace();}       
    }
    /**
     *Attempts to close the port and release the QuickTime stream. The plugin 
     *instance is then destroyed. This
     *method does not work. The port and streams cannot be re-aquired by the 
     *plugin.
     */
    public void close() {
        showMessage("Close called.");
        //try {
        //readWrite.stop();     //try to release the serial port.
        //readWrite = null;     //Doesn't seem to work.
        //  bout.close();
            //fout.close();
        //} catch(IOException ioe) {ioe.printStackTrace();}
        mqt.shutDown();         //Release the QuickTime stream.
    }                       //It is not possible to re-open a functional plugin
                            //without quiting ImageJ first.
    /**
     *Method talks to the serial device, sleeps for 3 seconds, captures an 
     *image. After 6 images are taken, images in queue are processed and then t
     *method waits for the remainder of a minuter before beginning again. The
     *method loops the number of times equal to the number of minutes specified 
     *for image capture. This method is not synchronized so the sleeps stop 
     *everything.
     *<p>
     *<ol>Sequence of events:<p>
     *<li>Listen for number from motor controller</li>
     *<li>Take image. Put it in a Vector queue.</li>
     *<li>Send a number then a letter to motor controller. Motor turns.</li>
     *<li>Sleep 3 secs while motor turns.</li>
     *<li>Repeat 6 times. Then process images.</li>
     *<li>Sleep the remainder of 1 minute.</li></ol>
     *<p>
     *Message "All Done. Results and images are located in 
     *path-to-images displayed after all images are processed.
     */
    public void tap() {
        byte [] nums = new byte[2];     //for the read
        nums[0] = (byte)0;              //set to 0
        repeatCount = 0;            //number of revolutions
        while (repeatCount < numRepeats) {  
            long startTime = System.currentTimeMillis(); //for sleeping time
            for (int i = 0; i < 6; i ++) {          //six moves per revolution.
                while((int)nums[0] != 65){          //wait for the byte 65 
                                                    //signal from
                    try {                           //the motor controller
                        nums = readWrite.read();
                    } catch (IOException ioe) {}
                }
                //        vial#  image# for saving
                grabImage(i, (imageNum+1));         
                int out = 0;    //send to the motor controller
                readWrite.write(out);   //send number.
                readWrite.write("Z");   //send letter.
                try {       
                    Thread.sleep(3000); //sleep while motor turns.
                } catch (InterruptedException ie) {}
            } //count loop
            process_images();       //process the six images
            long stopTime = System.currentTimeMillis(); //get time
            long difference = stopTime - startTime;     
            long sleepTime = MINUTE - difference;   //calculate sleep time
            try {
                Thread.sleep(sleepTime);            //sleep
            } catch (InterruptedException ie) {}
            repeatCount ++;
            imageNum ++;
        }
        showMessage("All Done. Results and images are located in " + savePath);
    }
    private void grabImage(int vialNum, int imageNum) {
        Image img = null;
        img = mqt.grabFrame();      //get the image
        ImageHolder ih = new ImageHolder(vialNum, imageNum, img);
        imageQueue.add(ih);     //create object and put it in the queue.
    }
    private void process_images() {
        if (countFlies) {               //user wants flies counted.
            //count and save flies
            while(!imageQueue.isEmpty()) {  //process all images
                ImageHolder ih = (ImageHolder)imageQueue.remove(0);
                ProcessImage procImage = new ProcessImage(ih);
                saveImage(procImage);
                saveResults(procImage);     //update results file
            }
        } else {
            saveImages();       //user did not want flies counted. Just save.
        }
    }
    /**
     *Use ImageJ FileSaver to save the images in the jpeg format.
     */
    private void saveImages() {
        while (!imageQueue.isEmpty()) {     //save all images when called.
            ImageHolder ir = (ImageHolder)imageQueue.remove(0);     
            Image img = ir.getImage();
            int vialNum = (ir.getVialNum() + 1);
            int imageNum = ir.getImageNum();
            //save path user specified directory/Vial_#/image#.jpeg
            String save = savePath + "Vial_" + vialNum + "/" + imageNum + ".jpeg";
            ImagePlus theImage = new ImagePlus(save, img);
            ImageConverter ic = new ImageConverter(theImage);
            ic.convertToGray8();
            ImageProcessor ip = theImage.getProcessor();
            ip = ip.convertToByte(false);
            FileSaver fsaver = new FileSaver(theImage);
            fsaver.saveAsJpeg(save);
            ic = null;
            img = null;
            ir = null;
        }
    }
    /**
     *Uses a ProcessImage. It should use ImageHolder. 
     *Save path is the same as above.
     */
    private void saveImage(ProcessImage procImage) {
        ImagePlus theImage = procImage.getTheImagePlus();
        int vialNum = (procImage.getVialNum() + 1);
        int imageNum = procImage.getImageNum();
        String save = savePath + "Vial_" + vialNum + "/" + imageNum + ".jpeg";
        FileSaver fsaver = new FileSaver(theImage);
        fsaver.saveAsJpeg(save);
        theImage = null;
    }
    /**
     *Write out the results from each count.
     */
    private void saveResults(ProcessImage procImage) {
        int result = procImage.getFlyCount();
        try {
            bout.write(Integer.toString(result));
            int vialNum = procImage.getVialNum();
            if (vialNum == 5) {
                bout.write("\n");
            } else {
                bout.write(",");
            }
            bout.flush();
        } catch(IOException ioe) {} 
    }
}   //closes class Carousel_Imaging



//G4 compile
//javac -classpath "/Applications/ImageJ1.30/ImageJ.app/Contents/Resources/Java/ij.jar:/Library/Java/Extensions:/System/Library/Java/Extensions/QTJava.zip:." Carousel_Imaging.java
//Sleepy compile
//javac -classpath "/Applications/ImageJ1.30/ImageJ.app/Contents/Resources/Java/ij.jar:/System/Library/Java/Extensions/QTJava.zip:/Library/Java/Extensions:./" Carousel_Imaging.java
//Compile packages G4
//javac -d '/Library/Java/Extensions' -classpath "/Applications/ImageJ1.30/ImageJ.app/Contents/Resources/Java/ij.jar:/Library/Java/Extensions:/System/Library/Java/Extensions/QTJava.zip:." Carousel_Imaging.java