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.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.SynapseComponent;
import unileipzig.isys.neuronviz.kernel.ParseEvent;
import unileipzig.isys.neuronviz.kernel.ParseListener;

/**
 * Class MatrixView
 */
public class MatrixView extends JPanel implements ParseListener, ComponentListener, 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.
    */
  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 
   */
  class Connection {
    private SynapseComponent fromNeuron, toNeuron;
    private String connectionName;
    private double connectionWeight;
    private boolean channelVisibility;
    public Connection(SynapseComponent from, SynapseComponent 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 SynapseComponent getFromNeuron() {
      return fromNeuron;
    }
    public SynapseComponent getToNeuron() {
      return toNeuron;
    }
    public String getConnectionName() {
      return connectionName;
    }
  }

  /**
   * Constructor of the MatrixView object
   */
  public MatrixView() {
    netLayers = new Vector();
    neuronConnections = new Vector();
    netComponent = new Hashtable();
    setLayout(null);
    addComponentListener(this);
    setToolTipText("Matrix View");
    setName("Matrix View");
  }

  /**
   * 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(250,230,230));
    g2.fillRect(0, 0, getSize().width, getSize().height);

    int[] layer_size = new int[3];
    for(int i=0; i<3; i++) layer_size[i] = ((NetLayer)netLayers.get(i)).size() * 20;
    for(int i=0; i<2; i++) {
      int max_x = layer_size[i];
      int max_y = layer_size[i+1];
      int offset_x = (i+1)*(getSize().width - (layer_size[0]+layer_size[1])) / 3 + i*layer_size[0];
      int offset_y = (getSize().height - max_y) / 2;
      g2.setColor(new Color(96,96,96));
      g2.fillRect(offset_x, offset_y, max_x, max_y);
      g2.setColor(Color.black);
      for(int y=0; y<max_y; y+=20) {
        boolean set = false;
        for(int x=0; x<max_x; x+=20) {
          int n = (x*max_y/20 + y + i*layer_size[0]*layer_size[1]/20) / 20;	  
	  try{
	      Connection conn = (Connection)(neuronConnections.get(n));
	      if(conn.isChannelVisibility() && !set) {
		  SynapseComponent sc = conn.getToNeuron();
		  sc.setLocation(x + offset_x, y + offset_y);
		  set = true;
		  
		  paintOutput(sc.getOutputValue(), x + offset_x - 21, y + offset_y, g2);
	      }
	  }catch(ArrayIndexOutOfBoundsException e){
	      // ignore
	  }
          g2.drawRect(x + offset_x, y + offset_y, 20, 20);
        }
        if(y<200) g2.drawString(""+y/20, offset_x - 15, y + offset_y + 15);
        else g2.drawString(""+y/20, offset_x - 19, y + offset_y + 15);
      }
      for(int x=0; x<max_x; x+=20) {
        if(x<200) g2.drawString(""+x/20, x + offset_x + 6, max_y + offset_y + 16);
        else g2.drawString(""+x/20, x + offset_x + 2, max_y + offset_y + 16);

        NetLayer nl = (NetLayer)(netLayers.get(i));
        SynapseComponent sc = (SynapseComponent)(nl.get(x/20));
//          sc.setLocation(j*20 + offset_x, max_y + offset_y + 1);
        paintOutput(sc.getOutputValue(), x + offset_x, max_y + offset_y + 1, g2);
      }
      if(i==0) {
        g2.drawString("y", offset_x - 15, offset_y - 5);
        g2.drawString("x", max_x + offset_x + 6, max_y + offset_y + 16);
      } else {
        g2.drawString("xp", offset_x - 21, offset_y - 5);
        g2.drawString("y", max_x + offset_x + 6, max_y + offset_y + 16);
      }
    }
  }

  private void paintOutput(double value, int x, int y, Graphics2D g2) {
    int c = (int)(255*value);
    if(c>255) c = 255;
    if(c<-255) c = -255;

    if(c<0) g2.setColor(new Color(255+c, 255+c, 255));
    else g2.setColor(new Color(255, 255-c, 255-c));

    g2.drawOval(x + 1, y + 1, 18, 18);
    g2.drawOval(x + 2, y + 2, 16, 16);
    g2.drawOval(x + 2, y + 3, 16, 14);
    g2.setColor(Color.black);
    g2.drawOval(x, y, 20, 20);
  }

  /**
   * 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])) {
      SynapseComponent sc = new SynapseComponent();
      sc.setSynapseName(e.getParameterValues()[0]);
      add(sc);
      // add a new synapse to the actual layer
      actualLayer.add(sc);
      // add the synapse to the hashtable for faster processing
      netComponent.put(sc.getSynapseName(), sc);
      // 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) {
    SynapseComponent from = getNeuron(e.getParameterValues()[1]);
    SynapseComponent 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);
  }

  /**
   * 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) {
    // 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.");
      return;
    }
    Enumeration enumeration = netComponent.elements();
    while(enumeration.hasMoreElements()) {
      Object obj = enumeration.nextElement();
      if(obj instanceof SynapseComponent) {
        ((SynapseComponent)(obj)).clearInputList();
        ((SynapseComponent)(obj)).setActivationVisibility(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 SynapseComponent) {
          SynapseComponent sc = (SynapseComponent)(obj);
          try {
            // parse the string to a value
            double output = Double.parseDouble(val[i]);
            // is the channel description a neuron or its bias?
            if(sc.getSynapseName().equals(cd[i])) {
              // channel is a neuron
              sc.setOutputValue(output);
              sc.setActivationVisibility(true);
            }
          }
          catch(Exception exc) {
            System.out.println("An error occured while converting \""+val[i]+"\" to a double. 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);
            SynapseComponent from = c.getFromNeuron();
            SynapseComponent to = c.getToNeuron();
            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();
  }

  /**
   * Returns a neuron with the specified name
   * @param name
   * @return
   */
  private SynapseComponent 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++) {
        SynapseComponent n = (SynapseComponent)(l.get(j));
        if(n.getSynapseName().equals(name)) return n;
      }
    }
    return null;
  }

  public void componentResized(ComponentEvent e) {}
  public void componentMoved(ComponentEvent e) {}
  public void componentShown(ComponentEvent e) {}
  public void componentHidden(ComponentEvent 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 }}}
  /////////////////////////////////
}
