package bsearch.app;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

import bsearch.algorithms.SearchParameterException;
import bsearch.evaluation.ResultListener;
import bsearch.evaluation.SearchManager;
import bsearch.nlogolink.ModelRunResult;
import bsearch.nlogolink.ModelRunner.ModelRunnerException;
import bsearch.representations.Chromosome;
import bsearch.util.GeneralUtils;

public class GUIProgressDialog extends JDialog implements PropertyChangeListener, ActionListener, WindowListener {

	private static final long serialVersionUID = 1L;
	private JProgressBar progressBar;
	private JLabel labelMessage;
	private JLabel labelTimeRemaining;
	private JTextPane infoTextPane;
	private JButton buttonCancel;
    private SearchTask task;
    private long taskStartTime;
    
    JFreeChart fitnessPlot;
    private XYSeriesCollection fitnessPlotDataset; 
    
    private boolean done = false;

    public GUIProgressDialog(Frame parent) {
        //super("BehaviorSearch - *running* "); //TODO: show name of search in the title bar
    	super(parent,true);

        this.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
        this.addWindowListener(this);

        progressBar = new JProgressBar(0, 100);
        progressBar.setValue(0);
        progressBar.setStringPainted(true);
        
        labelMessage = new JLabel("Performing search 0000 out of 0000");
        labelMessage.setFont(new Font("Arial", Font.BOLD, 12));

        labelTimeRemaining = new JLabel("Elapsed: ? Remaining: ?");
        labelTimeRemaining.setFont(new Font("Arial", Font.PLAIN, 12));

        infoTextPane = new JTextPane();
        infoTextPane.setEditable(false);
        infoTextPane.setBorder(BorderFactory.createEmptyBorder(30,5,10,12));
		infoTextPane.setContentType("text/html");
		infoTextPane.setText("<B><I>Search result status</I></B><BR>");
        
        buttonCancel = new JButton("Cancel");
        buttonCancel.addActionListener(this);
        
        fitnessPlotDataset = new XYSeriesCollection();

        fitnessPlot = ChartFactory.createXYLineChart("Search Progress",
        					"# of model runs", "Fitness", fitnessPlotDataset,
        					PlotOrientation.VERTICAL, true, false, false);
        final XYPlot plot = fitnessPlot.getXYPlot();
        plot.setBackgroundPaint(Color.lightGray);
        
        final XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
        renderer.setBaseShapesVisible(false);
        //renderer.setAutoPopulateSeriesShape(false);  // don't use different shapes for each series...
        //renderer.setBaseShape(new java.awt.geom.Ellipse2D.Double(-1,-1,3,3));  // smaller datapoint size...
        renderer.setBaseLegendTextFont(new Font("Arial",Font.PLAIN, 10));
        plot.setRenderer(renderer);
        
        ChartPanel cPanel = new ChartPanel(fitnessPlot);
        cPanel.setPreferredSize(new Dimension(500,400));

        JPanel panelSouth = new JPanel();
        panelSouth.setLayout(new BorderLayout());
        
        panelSouth.add(labelMessage, BorderLayout.WEST);
        panelSouth.add(progressBar, BorderLayout.CENTER);
        panelSouth.add(labelTimeRemaining, BorderLayout.EAST);
        panelSouth.add(buttonCancel, BorderLayout.SOUTH);

        JPanel panelEast = new JPanel();
        panelEast.setLayout(new BorderLayout());
        panelEast.add(new JScrollPane(infoTextPane),  BorderLayout.CENTER);
        
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        
        panel.add(panelEast, BorderLayout.EAST);
        panel.add(cPanel, BorderLayout.CENTER);
        panel.add(panelSouth,BorderLayout.SOUTH);
        
        panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));        
        this.getContentPane().setLayout(new BorderLayout());
        this.getContentPane().add(panel, BorderLayout.CENTER);
        this.pack();
    }

    public void startSearchTask(SearchProtocol protocol, BehaviorSearch.RunOptions runOptions)
    {
    	

        if (protocol.useBestChecking())
        {
        	ValueAxis rangeAxis = fitnessPlot.getXYPlot().getRangeAxis(); 
        	rangeAxis.setLabel(rangeAxis.getLabel() + " (rechecked)");
        }

    	labelMessage.setText("Search 0 of " + runOptions.numSearches);
        progressBar.setIndeterminate(true);
        progressBar.setMaximum(protocol.evaluationLimit);
        taskStartTime = System.currentTimeMillis(); 
        task = new SearchTask(protocol, runOptions);
        task.addPropertyChangeListener(this);
        task.execute();
    }

    public void propertyChange(PropertyChangeEvent evt) {
        if ("state".equals(evt.getPropertyName()))
        {
        	if (evt.getNewValue().equals(SwingWorker.StateValue.DONE))
        	{
        		try {
        			if (task.isCancelled() )
        			{
            			javax.swing.JOptionPane.showMessageDialog(this, "You canceled the search.  \nPartial results may have been saved to output files.",  "Canceled", JOptionPane.WARNING_MESSAGE);			
        			}
        			else if (task.fatalException != null)
        			{
       					throw task.fatalException;
        			}
        		}
        		catch (ModelRunnerException e) {
        			e.printStackTrace();
        			BehaviorSearchGUI.handleError("Error running the model:\n" + e.getMessage(), this.getParent());			
        		} catch (SearchParameterException e) {
        			e.printStackTrace();
        			BehaviorSearchGUI.handleError("Error setting search method parameters:\n" + e.getMessage(), this.getParent());						
        		}
        		catch (IOException e) {
        			BehaviorSearchGUI.handleError("Error reading or writing files:\n" + e, this.getParent());						
        			e.printStackTrace();
        		} catch (Exception e) {
        			e.printStackTrace();
        			BehaviorSearchGUI.handleError("Error: " + e, this.getParent());						
        		} catch (Throwable e) {
        			e.printStackTrace();
        			BehaviorSearchGUI.handleError("Serious Error: " + e, this.getParent());						
        		}
        		done = true;
        		buttonCancel.setText("Done");
        	}
        }
    }
	public void actionPerformed(ActionEvent arg0) {
		if (arg0.getSource().equals(buttonCancel))
		{
			doCancel();
		}		
	}
	private void doCancel()
	{
		if (done)
		{
			setVisible(false);
		}
		else
		{
			progressBar.setIndeterminate(false);
			task.cancel(true);
		}
	}
    
    
    class SearchTask extends SwingWorker<Void, Void>  implements ResultListener
    {
    	private SearchProtocol protocol;
    	private BehaviorSearch.RunOptions runOptions;
    	private Throwable fatalException = null; 
    	
    	public SearchTask(SearchProtocol protocol, BehaviorSearch.RunOptions runOptions)
    	{
    		this.protocol = protocol;
    		this.runOptions = runOptions;
    	}
    	public void updateGUIForNextSearch(final int searchNum)
    	{
			SwingUtilities.invokeLater(new Runnable() {
			    public void run() {
					labelMessage.setText("Performing search " + (searchNum - runOptions.firstSearchNumber + 1) + " of " + runOptions.numSearches + ":  ");
		            progressBar.setValue(0);

			    	XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) fitnessPlot.getXYPlot().getRenderer();
			    	if (searchNum > runOptions.firstSearchNumber)
			    	{
			    		renderer.setSeriesStroke(searchNum - runOptions.firstSearchNumber - 1, new BasicStroke(1.0f));
			    	}
		    		renderer.setSeriesStroke(searchNum - runOptions.firstSearchNumber, new BasicStroke(3.0f));
			    }
			});
		}
    	public void updateGUIWhenFinished()
    	{
			SwingUtilities.invokeLater(new Runnable() {
			    public void run() {
					labelMessage.setText("Finished search " + runOptions.numSearches + " of " + runOptions.numSearches + ":  ");
		            progressBar.setValue(progressBar.getMaximum());

			    	XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) fitnessPlot.getXYPlot().getRenderer();
		    		renderer.setSeriesStroke(runOptions.numSearches - 1, new BasicStroke(1.0f));
			    }
			});    		
    	}
        @Override
        public Void doInBackground() {
    		try {
    			List<ResultListener> listeners = new ArrayList<ResultListener>();
    			listeners.add(this);
            	BehaviorSearch.runWithOptions(runOptions, protocol, listeners);
            	updateGUIWhenFinished();
            	
        	} catch (Throwable e) {
				fatalException = e;  // handle it later, back in the regular Swing event thread
			}
            return null;
        }

		public void modelRunOccurred(SearchManager manager, Chromosome point, ModelRunResult result ) {
			long currentTime = System.currentTimeMillis();
			long elapsed = currentTime-taskStartTime;
			String elapsedStr = GeneralUtils.formatTimeNicely(elapsed);
			int searchesCompleted = (manager.getSearchIDNumber() - runOptions.firstSearchNumber);
			int totalSearches = runOptions.numSearches;
			double runsCompleted = manager.getEvaluationCount() + manager.getAuxilliaryEvaluationCount();
			double totalRuns = protocol.evaluationLimit + manager.getAuxilliaryEvaluationCount();
			double searchProgress = (searchesCompleted + runsCompleted / totalRuns) / totalSearches;
			
			long remaining = (long) (elapsed / searchProgress - elapsed); // in milliseconds
			String remainingStr = GeneralUtils.formatTimeNicely(remaining);
			labelTimeRemaining.setText("  (" + elapsedStr + " elapsed - " + remainingStr + " remaining)");			
		}

		public void fitnessComputed(SearchManager manager, Chromosome point, double fitness) {
			final int searchNumber = manager.getSearchIDNumber();
			final double bestFitnessSoFar = protocol.useBestChecking() ? manager.getCurrentBestFitnessCheckedEstimate() :  manager.getCurrentBestFitness();
			
			final int evaluationCount = manager.getEvaluationCount();
			SwingUtilities.invokeLater(new Runnable() {
			    public void run() {
			        progressBar.setIndeterminate(false);
		            progressBar.setValue(evaluationCount);

		            // enlarge the x-axis range if necessary, to show progress
			    	ValueAxis axis = fitnessPlot.getXYPlot().getDomainAxis();			    	
			    	if (axis.getRange().getUpperBound() < evaluationCount + 1)
			    	{
			    		axis.setRange(0, evaluationCount + 1);
			    	}

			    	if (searchNumber - runOptions.firstSearchNumber + 1 > fitnessPlotDataset.getSeriesCount())
			    	{
			    		XYSeries series = new XYSeries("Search " + searchNumber);
			    		fitnessPlotDataset.addSeries(series);
			    	}
			    	XYSeries series = fitnessPlotDataset.getSeries(searchNumber - runOptions.firstSearchNumber);
			    	int itemCount = series.getItemCount();
			    	if (itemCount == 0 || evaluationCount > series.getX(itemCount - 1).intValue() 
			    			|| (bestFitnessSoFar != series.getY(itemCount - 1).doubleValue()))
			    	{
			    		// This is a preventive measure to not bog down real-time graphing in JFreeChart:
			    		// if the fitness is on a plateau, we don't need a million XY data points to show a flat line 
			    		// we can use just 2 points (one at the beginning of the plateau, and one at the end)
			    		if (itemCount > 1 
			    				&& series.getY(itemCount - 1).doubleValue() == bestFitnessSoFar
			    				&& series.getY(itemCount - 2).doubleValue() == bestFitnessSoFar)
			    		{
			    			series.remove(itemCount - 1);
			    		}			    			
			    		series.addOrUpdate(evaluationCount, bestFitnessSoFar);
			    	}
				}
			});	
		}
		private void updateInfoText(String title, Chromosome c, double fitness, double checkedFitness)
		{
			String bestText = GeneralUtils.getParamSettingsTextHTML(c.getParamSettings());
			StringBuilder sb = new StringBuilder();
			sb.append("<B><I>"+title+"</I></B>");
			sb.append("<BR><BR><B>Best found so far:</B><BR>");
			sb.append(bestText);
			sb.append("<BR><B>Fitness</B>=");
			sb.append(String.format("%10.6g",fitness));
			sb.append("<BR>");
			if (protocol.useBestChecking())
			{
				sb.append("<B>(re-checked)</B>=");
				sb.append(String.format("%10.6g",checkedFitness));
				sb.append("<BR>");
			}
			infoTextPane.setText(sb.toString());
			
		}
		public void newBestFound(SearchManager manager) {
			updateInfoText("In Search #" + manager.getSearchIDNumber() + ":", manager.getCurrentBest(), manager.getCurrentBestFitness(), manager.getCurrentBestFitnessCheckedEstimate());
		}
		public void initListener(bsearch.space.SearchSpace space) {
			// do nothing			
		}
		public void searchStarting(SearchManager archive) {
			updateGUIForNextSearch(archive.getSearchIDNumber());
		}
		
		Chromosome overallBest = null;
		double overallBestFitness;
		double overallBestFitnessChecked;
		
		public void searchFinished(SearchManager manager) {
			double bestFitness = manager.getCurrentBestFitness();
			if (overallBest == null || manager.fitnessStrictlyBetter(bestFitness, overallBestFitness))
			{
				overallBest = manager.getCurrentBest();
				overallBestFitness = bestFitness;
				if (protocol.useBestChecking())
				{
					overallBestFitnessChecked = manager.getCurrentBestFitnessCheckedEstimate();
				}
			}
		}
		public void allSearchesFinished() {
			updateInfoText("From all searches:", overallBest, overallBestFitness, overallBestFitnessChecked);
		}
		public void searchesAborted() {
			// do nothing			
		}
    }

	public void windowClosing(WindowEvent arg0) {
		doCancel();		
	}

	public void windowActivated(WindowEvent arg0) {
	}

	public void windowClosed(WindowEvent arg0) {
	}

	public void windowDeactivated(WindowEvent arg0) {
	}

	public void windowDeiconified(WindowEvent arg0) {
	}

	public void windowIconified(WindowEvent arg0) {
	}

	public void windowOpened(WindowEvent arg0) {
	}


}
