package unileipzig.isys.neuronviz.viz;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.RenderingHints;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.swing.JPanel;

import unileipzig.isys.neuronviz.kernel.DataListener;
import unileipzig.isys.neuronviz.kernel.DataEvent;
import unileipzig.isys.neuronviz.kernel.DataStreamer;
import unileipzig.isys.neuronviz.kernel.NeuronComponent;
import unileipzig.isys.neuronviz.kernel.ParseEvent;
import unileipzig.isys.neuronviz.kernel.ParseListener;

/**
 * Class NetVisualizer
 * 
 */
public class NetVisualizer extends JPanel implements ParseListener, ComponentListener, MouseListener, Runnable
{
	private DataStreamer dataStreamer;
	private String neuralNetName;
	private Vector netLayers;
	private Vector neuronConnections;
	private NetLayer actualLayer = null;
	private Hashtable netComponent;
	

	/**
	 * A class to save a neural net layer.
	 * @author tozeus
	 *
	 */
	class NetLayer extends Vector
	{
		private String layerName = "";
		
		public NetLayer(String layerName)
		{
			this.layerName = layerName;
		}

		public String getLayerName()
		{
			return layerName;
		}
	}
	
	/**
	 * A class to handle a connection between 2 neurons 
	 * @author tozeus
	 *
	 */
	class Connection
	{
		private NeuronComponent fromNeuron, toNeuron;
		private String connectionName;
		private double connectionWeight;
		private boolean channelVisibility;
		
		public Connection(NeuronComponent from, NeuronComponent to, String connectionName)
		{
			this.fromNeuron = from;
			this.toNeuron = to;
			this.connectionName = connectionName;
		}

		public boolean isChannelVisibility()
		{
			return channelVisibility;
		}

		public void setChannelVisibility(boolean channelVisibility)
		{
			this.channelVisibility = channelVisibility;
		}

		public double getConnectionWeight()
		{
			return connectionWeight;
		}

		public void setConnectionWeight(double connectionWeight)
		{
			this.connectionWeight = connectionWeight;
		}

		public NeuronComponent getFromNeuron()
		{
			return fromNeuron;
		}

		public NeuronComponent getToNeuron()
		{
			return toNeuron;
		}

		public String getConnectionName()
		{
			return connectionName;
		}
	}	

	/**
	 * Constructor of the NetVisualizer object
	 *
	 */
	public NetVisualizer()
	{
		netLayers = new Vector();
		neuronConnections = new Vector();
		netComponent = new Hashtable();
		
		setLayout(null);
		
		addComponentListener(this);
		addMouseListener(this);
		
		setToolTipText("Global network view");
		setName("Net Visualizer");
	}
	
	/**
	 * Paints the component.
	 */
	public void paintComponent(Graphics g)
	{
		// use the extended possibilities of Graphics2D
		Graphics2D g2 = (Graphics2D)(g);

		// switch on antialising for pretty rendering results
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

		// draw the background of the panel
		g2.setColor(new Color(230,230,250));
		g2.fillRect(0, 0, getSize().width, getSize().height);
		
		// draw the connections between the neurons
		for (int i=0; i<neuronConnections.size(); i++)
		{
			Connection conn = (Connection)(neuronConnections.get(i));

			// get the connected neurons
			NeuronComponent from = conn.getFromNeuron();
			NeuronComponent to = conn.getToNeuron();
			
			// is that connection in the actual received channel?
			if(conn.isChannelVisibility())
				g2.setColor(new Color(100,100,250));
			else
				g2.setColor(new Color(200,200,200));
			
			g.drawLine(from.getX()+from.getWidth()/2, from.getY()+from.getHeight()/2, to.getX()+to.getWidth()/2, to.getY()+to.getHeight()/2);
		}
	}

	/**
	 * interface implementation
	 */
	public void createNeuralNet(ParseEvent e)
	{
		System.out.println("create neural net "+e.getParameterValues()[0]);
		
		// save the name of the net
		neuralNetName = e.getParameterValues()[0];
	}

	/**
	 * interface implementation
	 */
	public void createLayer(ParseEvent e)
	{
		System.out.println("create layer "+e.getParameterValues()[0]);
		
		// add a new layer to the existing ones
		actualLayer = new NetLayer(e.getParameterValues()[0]);
		netLayers.add(actualLayer);
	}

	/**
	 * interface implementation
	 */
	public void createNeuron(ParseEvent e)
	{
		System.out.println("create neuron "+e.getParameterValues()[0]);
				
		// add a neuron component to the panel
		// proceed only if the neuron name is unique
		if(!netComponent.containsKey(e.getParameterValues()[0]))
		{
			NeuronComponent nc = new NeuronComponent();
			int y = getSize().height - (nc.getPreferredSize().height + 10) * netLayers.size();
			int x = (nc.getPreferredSize().width + 10) * (actualLayer.size()+1);
			
			if (x>getWidth()) x = getWidth()-nc.getPreferredSize().width + (int)(Math.random()*10);
			if (y<0) y = (int)(Math.random()*10);
			
			nc.setLocation(x, y);
			nc.setNeuronName(e.getParameterValues()[0]);
			add(nc);
			
			// add a new neuron to the actual layer
			actualLayer.add(nc);
			
			// add the neuron to the hashtable for faster processing
			netComponent.put(nc.getNeuronName(), nc);
	
			//System.out.println("x="+x+", y="+y+getSize().toString());
			
			// is an optional bias defined?
			if(e.getParameterValues().length > 1)
			{
				// add the bias to the neuron
				nc.setBiasName(e.getParameterValues()[1]);
				
				// add the bias name to the hashtable
				netComponent.put(nc.getBiasName(), nc);
			}
			
			
			// repaint the panel
			repaint();
		}
		else
		{
			System.out.println("A neuron \""+e.getParameterValues()[0]+"\" was already inserted. The command was ignored.");
		}
	}

	/**
	 * interface implementation
	 */
	public void createConnection(ParseEvent e)
	{
		NeuronComponent from = getNeuron(e.getParameterValues()[1]);
		NeuronComponent to = getNeuron(e.getParameterValues()[2]);
		
		// if both neurons exist and the connection name is unique, add a connection
		if (from != null && to != null && !netComponent.containsKey(e.getParameterValues()[0]))
		{
			System.out.println("create connection \""+e.getParameterValues()[0]+"\": "+e.getParameterValues()[1]+","+e.getParameterValues()[2]);
			
			// add a connection to the list
			Connection c = new Connection(from, to, e.getParameterValues()[0]);
			neuronConnections.add(c);
			
			// add the connection to the hashtable for faster processing
			netComponent.put(c.getConnectionName(), c);
		}
		else
		{
			System.out.println("The neuron \""+e.getParameterValues()[1]+"\" and/or \""+e.getParameterValues()[2]+"\" does not exist. The command was ignored.");
		}
		// repaint the panel
		repaint();
	}

	/**
	 * interface implementation
	 */
	public void descriptionTerminated(ParseEvent e)
	{
		System.out.println("description terminated.\n"+ e.getParseMessage());
		componentResized(null);
//		(new Thread(this)).start();
	}

	/**
	 * interface implementation
	 */
	public void unknownCommand(ParseEvent e)
	{
		System.out.println("Unknown command, ignoring. \n"+e.getParseMessage());
	}

	/**
	 * interface implementation
	 */
	public void quitReceived(ParseEvent e)
	{
	    System.out.println("Quit command \n");
	    System.exit(0);
	}


	/**
	 * interface implementation
	 */
	public void channelReceived(ParseEvent e)
	{
	    // System.out.println(e.getParseMessage());
		
		// get the channel description and the corresponding values out of the stream
		String [] cd = e.getChannelDescription();
		String [] val = e.getParameterValues();
		
		// exists for each value a channel description?
		if (cd.length != val.length)
		{
			System.out.println("The size of the channel description is unequal to the received channel values! Ignoring that line.");
//			System.out.println("descr="+cd.length+", val="+val.length);
			return;
		}
		
		Enumeration enumeration = netComponent.elements();
		while(enumeration.hasMoreElements())
		{
			Object obj = enumeration.nextElement();
			if (obj instanceof NeuronComponent)
			{
				((NeuronComponent)(obj)).clearInputList();
				((NeuronComponent)(obj)).setActivationVisibility(false);
				((NeuronComponent)(obj)).setBiasVisibility(false);
			}
			if (obj instanceof Connection)
			{
				((Connection)(obj)).setChannelVisibility(false);
			}
		}
		
		// at first process only the neurons
		for (int i=0; i<cd.length; i++)
		{
			// get the neuron with the name set in the channel description and update the output value
			Object obj = netComponent.get(cd[i]); 
			if(obj != null)
			{
				if(obj instanceof NeuronComponent)
				{
					NeuronComponent nc = (NeuronComponent)(obj);
					try
					{
						// parse the string to a value
						double output = Double.parseDouble(val[i]);
						
						// is the channel description a neuron or its bias?
						if(nc.getNeuronName().equals(cd[i]))
						{
							// channel is a neuron
							nc.setOutputValue(output);
							nc.setActivationVisibility(true);
						}
						else
						{
							// channel is a bias
							nc.setBiasValue(output);
							nc.setBiasVisibility(true);
						}
					}
					catch(Exception exc)
					{
						System.out.println("An error occured while converting \""+val[i]+"\" to a double. Ignoring that value.");
					}
				}
			}
			else
			{
			    // FIXME: add some verbose level
			    // System.out.println("\""+cd[i]+"\" in the channel description is not a valid component name of the net. Ignoring that value.");
			}
			
		}
		
		// now process the connections
		for (int i=0; i<cd.length; i++)
		{
			// get the connection with the name set in channel description and update connection weight and the inputs for the connected neurons
			Object obj = netComponent.get(cd[i]); 
			if(obj != null)
			{
				if(obj instanceof Connection)
				{
					Connection c = (Connection)(obj);
					try
					{
						double weight = Double.parseDouble(val[i]);
						c.setConnectionWeight(weight);
						c.setChannelVisibility(true);
						
						NeuronComponent from = c.getFromNeuron();
						NeuronComponent to = c.getToNeuron();
						
						//double inputValue = from.getOutputValue() * weight;
						double inputValue = weight;
						to.addInputValue(inputValue, from);
					}
					catch(Exception exc)
					{
						System.out.println("An error occured while converting \""+val[i]+"\" to a double. Ignoring that value.");
					}
				}
			}
		}

		repaint();
//		dataStreamer.stopStreaming();
	}

	/**
	 * Returns a neuron with the specified name.
	 * @param name
	 * @return
	 */
	private NeuronComponent getNeuron(String name)
	{
		for (int i=0; i<netLayers.size(); i++)
		{
			NetLayer l = (NetLayer)(netLayers.get(i));
			for (int j=0; j<l.size(); j++)
			{
				NeuronComponent n = (NeuronComponent)(l.get(j));
				if (n.getNeuronName().equals(name)) return n;
			}
		}
		return null;
	}
	

	public void componentResized(ComponentEvent e)
	{
		for (int i=0; i<netLayers.size(); i++)
		{
			NetLayer l = (NetLayer)(netLayers.get(i));

			for (int j=0; j<l.size(); j++)
			{
				NeuronComponent n = (NeuronComponent)(l.get(j));

				int x_offset = (getWidth() - l.size()*n.getPreferredSize().width) / (l.size());
				int y_offset = (getHeight() - netLayers.size()*n.getPreferredSize().height) / (netLayers.size());
				
				int x = (n.getPreferredSize().width + x_offset) * j + x_offset/2;
				int y = getSize().height - ((n.getPreferredSize().height + y_offset) * i + y_offset);
				
				if (x>getWidth()) x = getWidth()-n.getPreferredSize().width + (int)(Math.random()*10);
				if (y<0) y = (int)(Math.random()*10);
				
				n.setLocation(x,y);
			}
		}
		repaint();
	}

	public void componentMoved(ComponentEvent e)
	{
	}

	public void componentShown(ComponentEvent e)
	{
	}

	public void componentHidden(ComponentEvent e)
	{
	}

	public void mouseClicked(MouseEvent e)
	{
//		if(!dataStreamer.isRunning()) 
//			dataStreamer.startStreaming(100);
	}

	public void mousePressed(MouseEvent e)
	{
	}

	public void mouseReleased(MouseEvent e)
	{
	}

	public void mouseEntered(MouseEvent e)
	{
	}

	public void mouseExited(MouseEvent e)
	{
	}

	public void run()
	{
		while(true)
		{
			if(!dataStreamer.isRunning()) 
				dataStreamer.startStreaming(100);
			try
			{
				Thread.sleep(100);
			} catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
	}

	/////////////////////////////////
	// {{{ Getter- and Setter-Methods
	
	public DataStreamer getDataStreamer()
	{
		return dataStreamer;
	}

	public void setDataStreamer(DataStreamer dataStreamer)
	{
		this.dataStreamer = dataStreamer;
	}

	// Getter- and Setter-Methods }}}
	/////////////////////////////////
}
