package org.opensourcephysics.cabrillo.tracker;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.swing.JOptionPane;

import org.opensourcephysics.controls.OSPLog;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
import org.opensourcephysics.controls.XMLControlElement;
import org.opensourcephysics.display.Data;
import org.opensourcephysics.display.DataClip;
import org.opensourcephysics.media.core.DataTrack;
import org.opensourcephysics.media.core.DataTrackSupport;
import org.opensourcephysics.media.core.VideoClip;
import org.opensourcephysics.media.core.VideoIO;
import org.opensourcephysics.media.core.VideoPlayer;
import org.opensourcephysics.tools.Job;
import org.opensourcephysics.tools.LocalJob;
import org.opensourcephysics.tools.Resource;
import org.opensourcephysics.tools.ResourceLoader;
import org.opensourcephysics.tools.Tool;

/**
 * A Remote Tool that passes incoming Data and messages to a DataTrack with the same name or ID.
 * If no DataTrack is found, the Data is passed to the TFrame's selected TrackerPanel.
 * 
 * @author Douglas Brown
 */
public class DataTrackTool extends UnicastRemoteObject implements Tool {
	
	/* Define serialVersionUID so RMI serialization can occur reliably.
	The value of this must never change even in future releases */
  private static final long serialVersionUID = 1L;
  
  private TFrame frame;
  private TreeMap<Integer, Tool> replyToTools = new TreeMap<Integer, Tool>();
  private TreeMap<Integer, String> jarPaths = new TreeMap<Integer, String>();
  
  /**
   * Constructor for a TFrame.
   *
   * @param tFrame the TFrame
   * @throws RemoteException
   */
	public DataTrackTool(TFrame tFrame) throws RemoteException {
		frame = tFrame;		
	}

  /**
   * Sends a job to this tool and specifies a tool to reply to. The job xml must
   * define either a Data object or DataTrackSupport.Message object. Data must
   * include (x, y) coordinates and and may define other properties as well. The optional
   * replyTo Tool may also be a (JPanel) source control panel for the DataTrack.
   *
   * @param job the Job
   * @param replyTo the tool to notify when the job is complete (may be null)
   * @throws RemoteException
   */
  public void send(Job job, Tool replyTo) throws RemoteException {
    // read the job's XML into an XMLControl
    XMLControl control = new XMLControlElement();
    control.readXML(job.getXML());
    if (control.failedToRead()) {
      return;
    }
    
    // get the source ID and check for handshake
		int sourceID = control.getInt("sourceID"); //$NON-NLS-1$
  	if (control.getBoolean("handshake")) { //$NON-NLS-1$
  		// save replyTo tool
			replyToTools.put(sourceID, replyTo);
  		// save jarPath
			jarPaths.put(sourceID, control.getString("jar_path")); //$NON-NLS-1$
			// send handshake reply
	  	job.setXML(control.toXML());
			replyTo.send(job, null);
			return;
  	}
  	
  	// see if a video path has been sent
		String videoFullPath = control.getString("video"); //$NON-NLS-1$
		
		// extract the video if needed, before loading TRK
		File videoFile = null;
		if (videoFullPath!=null) {
  		videoFile = findFile(videoFullPath, sourceID);
  	}

		boolean videoLoadedByTRK = false;
  	
		// load TRK file
  	if (control.getPropertyNames().contains("trk")) { //$NON-NLS-1$
  		String path = control.getString("trk"); //$NON-NLS-1$
  		File trkFile = findFile(path, sourceID);
  		if (trkFile==null || !trkFile.exists()) {
        int result = JOptionPane.showConfirmDialog(frame,
        		TrackerRes.getString("DataTrackTool.Dialog.FileNotFound.Message1") //$NON-NLS-1$
    				+" \""+path+"\"" //$NON-NLS-1$ //$NON-NLS-2$
        		+"\n"+TrackerRes.getString("DataTrackTool.Dialog.VideoNotFound.Message2"), //$NON-NLS-1$ //$NON-NLS-2$
    				TrackerRes.getString("DataTrackTool.Dialog.FileNotFound.Title"),  //$NON-NLS-1$
    				JOptionPane.ERROR_MESSAGE);
        if (result==JOptionPane.YES_OPTION) {
          java.io.File[] files = TrackerIO.getChooserFiles("open trk");  //$NON-NLS-1$
          if (files!=null && files.length>0) {
          	trkFile = files[0];
          }
        }
  		}
  		if (trkFile!=null) {
  	    XMLControlElement trkControl = new XMLControlElement(trkFile.getAbsolutePath());
  	    Class<?> type = trkControl.getObjectClass();
  	    if (!TrackerPanel.class.equals(type)) {
  	      JOptionPane.showMessageDialog(frame, 
  	          TrackerRes.getString("DataTrackTool.Dialog.InvalidTRK.Message") //$NON-NLS-1$
  	  				+ ": \""+ trkFile.getAbsolutePath()+"\"", //$NON-NLS-1$ //$NON-NLS-2$
  	      		TrackerRes.getString("DataTrackTool.Dialog.InvalidTRK.Title"), //$NON-NLS-1$
  	      		JOptionPane.WARNING_MESSAGE);
  	    }
  	    else {
  	    	OSPLog.fine("loading TRK file "+trkFile.getAbsolutePath()); //$NON-NLS-1$
  	    	// extract video if needed
  	    	XMLControl childControl = trkControl.getChildControl("videoclip"); //$NON-NLS-1$
  	    	if (childControl!=null) {
  	    		childControl = childControl.getChildControl("video"); //$NON-NLS-1$
  	    	}
  	    	if (childControl!=null) {
	  	  		String trkVidPath = childControl.getString("path"); //$NON-NLS-1$
	  	  		File vidFile = null;
	  	  		// video path relative to EjsS base, videoPath==full path?	  	  		
	  	  		// check to see if the full path contains base from which TRK path is relative
	  	    	if (videoFullPath!=null) {
	  	    		vidFile = new File(videoFullPath);
  	    			if (vidFile.exists()) {
  	    				File parentFile = vidFile.getParentFile();
  	    				while (parentFile!=null) {
  	    					String base = parentFile.getAbsolutePath();
  	    					String target = XML.getResolvedPath(trkVidPath, base);
  	    					if (new File(target).exists()) {
  	    						videoLoadedByTRK = true;
	  	    					ResourceLoader.addSearchPath(base);
	  	    					break;
  	    					}
  	    					parentFile = parentFile.getParentFile();
  	    				}
  	    			}  	    			
	  	    	}
	  	    	
	  	    	if (vidFile==null) {
		  	  		vidFile = findFile(trkVidPath, sourceID);	  	    		
		  	  		if (vidFile!=null) {
		  	  			String jarPath = jarPaths.get(sourceID);
		  	  			if (jarPath!=null) {
		  	  				ResourceLoader.addSearchPath(XML.getDirectoryPath(jarPath));
		  	  			}
		  	  		}
	  	    	}
  	    	}
  	    	
  	    	// create and load new TrackerPanel
  				TrackerPanel trackerPanel = new TrackerPanel();
	        trkControl.loadObject(trackerPanel);
	        trackerPanel.defaultFileName = XML.getName(path);
	        trackerPanel.openedFromPath = trkFile.getAbsolutePath();
	        trackerPanel.setDataFile(trkFile);
					frame.addTab(trackerPanel);
					frame.setSelectedTab(trackerPanel);
	        Tracker.addRecent(trkFile.getAbsolutePath(), false); // add at beginning
  	    }
  		}
  	}
  	
  	// get the target TrackerPanel
		TrackerPanel trackerPanel = frame.getTrackerPanel(frame.getSelectedTab());		
		if (trackerPanel==null) {
			trackerPanel = new TrackerPanel();
			frame.addTab(trackerPanel);
		}
		
		// import video only if not loaded by TRK
		if (!videoLoadedByTRK && videoFullPath!=null) {
			if (videoFile==null || !videoFile.exists()) {
	      int result = JOptionPane.showConfirmDialog(trackerPanel,
	      		TrackerRes.getString("DataTrackTool.Dialog.VideoNotFound.Message1") //$NON-NLS-1$
	  				+" \""+videoFullPath+"\"" //$NON-NLS-1$ //$NON-NLS-2$
	      		+"\n"+TrackerRes.getString("DataTrackTool.Dialog.VideoNotFound.Message2"), //$NON-NLS-1$ //$NON-NLS-2$
	  				TrackerRes.getString("DataTrackTool.Dialog.VideoNotFound.Title"),  //$NON-NLS-1$
	  				JOptionPane.ERROR_MESSAGE);
	      if (result==JOptionPane.YES_OPTION) {
	        java.io.File[] files = VideoIO.getChooserFiles("open video");  //$NON-NLS-1$
	        if (files!=null && files.length>0) {
	        	videoFile = files[0];
	        }
	      }
			}
			if (videoFile!=null) {
	    	OSPLog.fine("importing video file "+videoFile.getAbsolutePath()); //$NON-NLS-1$
				TrackerIO.importVideo(videoFile, trackerPanel, null);
			}
		}
		// set video properties
  	if (control.getPropertyNames().contains("videoStartFrame")) { //$NON-NLS-1$
  		int start = control.getInt("videoStartFrame"); //$NON-NLS-1$
    	OSPLog.fine("setting start frame to "+start); //$NON-NLS-1$
  		trackerPanel.getPlayer().getVideoClip().setStartFrameNumber(start);
  	}
  	if (control.getPropertyNames().contains("videoEndFrame")) { //$NON-NLS-1$
  		int end = control.getInt("videoEndFrame"); //$NON-NLS-1$
    	OSPLog.fine("setting end frame to "+end); //$NON-NLS-1$
  		trackerPanel.getPlayer().getVideoClip().setEndFrameNumber(end);
  	}
  	if (control.getPropertyNames().contains("videoStepSize")) { //$NON-NLS-1$
  		int size = control.getInt("videoStepSize"); //$NON-NLS-1$
  		trackerPanel.getPlayer().getVideoClip().setStepSize(size);
  	}
  	if (control.getPropertyNames().contains("stepNumber")) { //$NON-NLS-1$
  		int step = control.getInt("stepNumber"); //$NON-NLS-1$
    	OSPLog.fine("setting step size to "+step); //$NON-NLS-1$
  		trackerPanel.getPlayer().setStepNumber(step);
  	}
  	if (control.getPropertyNames().contains("frameNumber")) { //$NON-NLS-1$
  		int frame = control.getInt("frameNumber"); //$NON-NLS-1$
  		int step = trackerPanel.getPlayer().getVideoClip().frameToStep(frame);
    	OSPLog.fine("setting step number to "+step); //$NON-NLS-1$
  		trackerPanel.getPlayer().setStepNumber(step);
  	}
  	if (control.getPropertyNames().contains("deleteTracks")) { //$NON-NLS-1$
  		String[] trackNames = (String[])control.getObject("deleteTracks"); //$NON-NLS-1$
  		for (String next: trackNames) {
  			ParticleDataTrack track = findParticleDataTrack(trackerPanel, next, -1);
  			if (track!=null) {
  	    	OSPLog.fine("deleting track "+track.getName()); //$NON-NLS-1$
  				track.delete();
  			}
  		}
  	}
//  	if (control.getPropertyNames().contains("trk")) { //$NON-NLS-1$
//  		String path = control.getString("trk"); //$NON-NLS-1$
//  		File trkFile = findFile(path, sourceID);
//  		if (trkFile==null || !trkFile.exists()) {
//        int result = JOptionPane.showConfirmDialog(trackerPanel,
//        		TrackerRes.getString("DataTrackTool.Dialog.FileNotFound.Message1") //$NON-NLS-1$
//    				+" \""+path+"\"" //$NON-NLS-1$ //$NON-NLS-2$
//        		+"\n"+TrackerRes.getString("DataTrackTool.Dialog.VideoNotFound.Message2"), //$NON-NLS-1$ //$NON-NLS-2$
//    				TrackerRes.getString("DataTrackTool.Dialog.FileNotFound.Title"),  //$NON-NLS-1$
//    				JOptionPane.ERROR_MESSAGE);
//        if (result==JOptionPane.YES_OPTION) {
//          java.io.File[] files = TrackerIO.getChooserFiles("open trk");  //$NON-NLS-1$
//          if (files!=null && files.length>0) {
//          	trkFile = files[0];
//          }
//        }
//  		}
//  		if (trkFile!=null) {
//  	    XMLControlElement trkControl = new XMLControlElement(trkFile.getAbsolutePath());
//  	    Class<?> type = trkControl.getObjectClass();
//  	    if (!TrackerPanel.class.equals(type)) {
//  	      JOptionPane.showMessageDialog(trackerPanel.getTFrame(), 
//  	          TrackerRes.getString("DataTrackTool.Dialog.InvalidTRK.Message") //$NON-NLS-1$
//  	  				+ ": \""+ trkFile.getAbsolutePath()+"\"", //$NON-NLS-1$ //$NON-NLS-2$
//  	      		TrackerRes.getString("DataTrackTool.Dialog.InvalidTRK.Title"), //$NON-NLS-1$
//  	      		JOptionPane.WARNING_MESSAGE);
//  	    }
//  	    else {
//  	    	OSPLog.fine("loading TRK file "+trkFile.getAbsolutePath()); //$NON-NLS-1$
//	        trackerPanel.changed = true;
//	        trkControl.loadObject(trackerPanel);
//	        trackerPanel.defaultFileName = XML.getName(path);
//	        trackerPanel.openedFromPath = trkFile.getAbsolutePath();
//	        trackerPanel.setDataFile(trkFile);
//	        Tracker.addRecent(trkFile.getAbsolutePath(), false); // add at beginning
//  	    }
//  		}
//  	}
  	  	
  	// get the data, if any
		Data data = (Data)control.getObject("data"); //$NON-NLS-1$
		boolean append = control.getBoolean("append"); //$NON-NLS-1$
		
  	// get the target DataTrack
    DataTrack dataTrack = loadData(trackerPanel, data, append);	
    if (dataTrack==null && data!=null) {
    	dataTrack = trackerPanel.importData(data, replyTo);
    }
    if (dataTrack==null) {
    	String name = control.getString("dataName"); //$NON-NLS-1$
    	int dataID = control.getInt("dataID"); //$NON-NLS-1$
    	dataTrack = findParticleDataTrack(trackerPanel, name, dataID);
    	if (dataTrack!=null) {
    		try {
					dataTrack.setData(data, replyTo);
				} catch (Exception e) {
				}
    	}
    }
    
    // set DataTrack properties
    if (dataTrack!=null) {
    	if (control.getPropertyNames().contains("useDataTime") && dataTrack.getVideoPanel()!=null) { //$NON-NLS-1$
	    	boolean useTrackTime = control.getBoolean("useDataTime"); //$NON-NLS-1$
    		VideoPlayer player = dataTrack.getVideoPanel().getPlayer();
    		player.getClipControl().setTimeSource(useTrackTime? dataTrack: null);
    		player.refresh();
    		if (dataTrack instanceof ParticleDataTrack) {
    			((ParticleDataTrack)dataTrack).refreshInitialTime();
    		}
    	}

    }
  }
  
  /**
   * Sends a message to a replyTo tool in the form of a String-to-String mapping.
   *
   * @param id the replyTo identifier
   * @param message the information to send
   * @return true if sent successfully
   */
  public boolean reply(int id, Map<String, String> message) {
  	Tool tool = replyToTools.get(id);
  	if (tool==null) return false;
		// get message control and set properties based on info map
		XMLControl control = DataTrackSupport.getMessageControl(id);
		for (String key: message.keySet()) {
			control.setValue(key, message.get(key));
		}
  	try {
			tool.send(new LocalJob(control.toXML()), null);
		} catch (RemoteException e) {
			return false;
		}
  	return true;
  }
    
  /**
   * Informs all replyTo tools that Tracker is exiting.
   */
  protected void trackerExiting() {
  	XMLControl control = new XMLControlElement();
  	control.setValue("exiting", true); //$NON-NLS-1$
  	Job job = new LocalJob(control.toXML());
  	try {
			for (Tool tool: replyToTools.values()) {
				tool.send(job, this);
			}
		} catch (RemoteException e) {
		}
  }
  
  /**
   * Attempts to find a file specified by path and source ID.
   * Extracts the file from the source jar if required.
   * 
   * @param path the file path, usually relative
   * @param ID the source ID
   * @return the file, or null if not found
   */
  private File findFile(String path, int ID) {
		File file = null;
		// try to load file directly from path
		Resource res = ResourceLoader.getResource(path);
		if (res==null) {
			String jarPath = jarPaths.get(ID);
			if (jarPath!=null) {
	  		// try to load file from path relative to jar path
				String target = XML.getResolvedPath(path, XML.getDirectoryPath(jarPath));
	  		res = ResourceLoader.getResource(target);
	  		if (res==null) {
  	  		// try to find and extract file entry in the jar file
	  			String name = XML.getName(path);
  				JarEntry entry = null;
					try {
						JarFile jar = new JarFile(jarPath);
						for (Enumeration<JarEntry> en = jar.entries(); en.hasMoreElements();) {
							JarEntry next = en.nextElement();
							if (!next.isDirectory() && next.getName().endsWith(name)) {
								entry = next;
								break;
							}
						}
						jar.close();
					} catch (Exception e) {
					}
  				if (entry!=null) {	  					
  					String source = jarPath+"!/"+entry.getName(); //$NON-NLS-1$
  					source = ResourceLoader.getURIPath(source);
  					file = ResourceLoader.extractFileFromZIP(source, new File(target), false);
  				}
	  		}
			}
		}
		if (file==null && res!=null && res.getFile()!=null) {
			file = res.getFile();
		}
  	OSPLog.fine("file found for path \""+path+"\": "+file); //$NON-NLS-1$ //$NON-NLS-2$
		return file;
  }
  
  /**
   * Finds an existing ParticleDataTrack with a specified name and/or Data ID.
   * 
   * @param trackerPanel the TrackerPanel to search
   * @param name the desired name (may be null)
   * @param dataID the Data ID
   * @return the ParticleDataTrack, or null if none found
   */
  private ParticleDataTrack findParticleDataTrack(TrackerPanel trackerPanel, String name, int dataID) {  	
  	if (trackerPanel!=null) {
    	if (name==null || name.trim().equals("")) { //$NON-NLS-1$
    		name = TrackerRes.getString("ParticleDataTrack.New.Name"); //$NON-NLS-1$
    	}
    	name = name.replaceAll("_", " "); //$NON-NLS-1$ //$NON-NLS-2$
    	TTrack track = trackerPanel.getTrack(name);
    	if (track!=null && track instanceof ParticleDataTrack) {
    		return (ParticleDataTrack)track;
    	}
  		for (ParticleDataTrack dataTrack: trackerPanel.getDrawables(ParticleDataTrack.class)) {
  			Data existingData = dataTrack.getData();
  			if (existingData!=null && dataID==existingData.getID()) {
  				return dataTrack;
  			}
  		}
  	}
    return null;
  }
  
  /**
   * Loads data into an existing DataTrack in a TrackerPanel.
   * 
   * @param trackerPanel the TrackerPanel
   * @param data the Data to load
   * @param append true to append data
   * @return the loaded DataTrack, or null if no DataTrack found
   */
  private DataTrack loadData(TrackerPanel trackerPanel, Data data, boolean append) {
  	if (data==null || trackerPanel==null) return null;
  	
  	// look for existing ParticleDataTrack with matching name or data ID
		ParticleDataTrack dataTrack = findParticleDataTrack(trackerPanel, data.getName(), data.getID());
  	if (dataTrack!=null) {
			try {
				if (append) {
					// following call throws exception if (x, y) data not found
  				dataTrack.appendData(data);
  				// display the last point appended
  				VideoPlayer player = trackerPanel.getPlayer();
  				VideoClip videoClip = player.getVideoClip();
  				DataClip dataClip = dataTrack.getDataClip();
  				dataClip.setClipLength(-1); // set clip length to data length
  				int dataEndFrame = dataTrack.getStartFrame()+dataClip.getDataLength()-1;
  				player.setStepNumber(videoClip.frameToStep(dataEndFrame));
				}
				else {
					// following call throws exception if (x, y) data not found
					dataTrack.setData(data);
				}
			} catch (Exception e) {
				// inform user
				JOptionPane.showMessageDialog(frame, 
						TrackerRes.getString("DataTrackTool.Dialog.InvalidData.Message"), //$NON-NLS-1$
						TrackerRes.getString("DataTrackTool.Dialog.InvalidData.Title"), //$NON-NLS-1$
						JOptionPane.WARNING_MESSAGE);
			}
  	}
  	return dataTrack;
  }
  
//______________________________  static methods  ____________________________
  
  /**
   * Determines if a jar file is a (likely) data source.
   *
   * @param jarPath the path to a jar file
   * @return true if the jar contains the DataTrackSupport class
   */
  public static boolean isDataSource(String jarPath) {
		try {
			JarFile jar = new JarFile(jarPath);
			String classPath = DataTrackSupport.class.getName().replace(".", "/"); //$NON-NLS-1$ //$NON-NLS-2$
			JarEntry entry = jar.getJarEntry(classPath+".class"); //$NON-NLS-1$
			jar.close();
			return entry!=null;
		} catch (IOException ex) {
		}
		return false;
  }
  
  /**
   * Launches a data source and optionally requests that it send data.
   *
   * @param jarPath the path to a data source jar file
   * @param requestData true to request data
   */
  public static void launchDataSource(String jarPath, boolean requestData) {
  	if (!isDataSource(jarPath)) {
  		// inform user
  		String jarName = TrackerRes.getString("TActions.Action.DataTrack.Unsupported.JarFile") //$NON-NLS-1$
  				+ " \""+XML.getName(jarPath)+"\" "; //$NON-NLS-1$ //$NON-NLS-2$
			JOptionPane.showMessageDialog(null, 
					jarName+TrackerRes.getString("TActions.Action.DataTrack.Unsupported.Message")+".", //$NON-NLS-1$ //$NON-NLS-2$
					TrackerRes.getString("TActions.Action.DataTrack.Unsupported.Title"), //$NON-NLS-1$
					JOptionPane.WARNING_MESSAGE);
  		return;
  	}
		// assemble the command
		final ArrayList<String> cmd = new ArrayList<String>();
		cmd.add("java"); //$NON-NLS-1$
		cmd.add("-jar"); //$NON-NLS-1$
		cmd.add(jarPath);

		// create ProcessBuilder to execute the command
		final ProcessBuilder builder = new ProcessBuilder(cmd);
		
		if (requestData) {
			// set DATA_REQUESTED environment variable
			Map<String, String> env = builder.environment();
			env.put("DATA_REQUESTED", "true"); //$NON-NLS-1$ //$NON-NLS-2$
		}
		
		// log the command
		String command = ""; //$NON-NLS-1$
		for (String next: cmd) {
			command += next + " "; //$NON-NLS-1$
		}
		OSPLog.config(command);
							
		// start the process
		startProcess(builder);
  }
  
  /**
   * Starts a ProcessBuilder and handles its output and error streams.
   *
   * @param builder the ProcessBuilder
   */
  private static void startProcess(final ProcessBuilder builder) {
		// start the process and wait for it to finish
		Runnable runner = new Runnable() {
			public void run() {
				try {
					Process process = builder.start();
					// read output stream from the process--important so process doesn't block
	        InputStream is = process.getInputStream();
	        InputStreamReader isr = new InputStreamReader(is);
	        BufferedReader br = new BufferedReader(isr);
	        String line;
	        while ((line = br.readLine()) != null) {
	            System.out.println(line);
	        }
			    br.close();
	        
					int result = process.waitFor();
					// if process returns with exit code > 0, print it's error stream
					if (result > 0) {
						isr = new InputStreamReader(process.getErrorStream());
						br = new BufferedReader(isr);
		        while ((line = br.readLine()) != null) {
	            System.err.println(line);
		        }
				    br.close();
					}
				} catch (Exception ex) {
					ex.printStackTrace();
				}					
			}
		};
		
		new Thread(runner).start();  	
  }
  
}