package nn_assignment;

import javax.swing.UIManager;
import java.awt.*;
import javax.swing.*;
import javax.swing.JComponent.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.text.NumberFormat;


public class SOMDemonstration extends Frame implements Runnable, ActionListener
{
	//We'll hold the width and the height of the window here
	private int width;
	private int height;

	//Reserver the top 40 pixles of the window for our controls.
	// We'll use the second variables when we're drawing the screen
	//  to avoid a lot of messy height - AreaForControls(s).
	// Its value is initialised in the init() method
	private int AreaForControls         = 160;
	private int heightMinusControls;

	//To keep track of the number of training cyles
	private int cycleNumber             = 1;
	private int maxCycles               = 500;

	//Some nice defaults for our map
	private KohonenMap map;
	private int mapWidth                = 10;
	private int mapHeight               = 10;
	private int numberOfInputs          = 4;
	private double initialLearningRate  = 0.4;
	private double initialLearningRadius= 0.3;

	private double[] inputs         = new double[numberOfInputs];
	private double[][][] weights    = new double[mapWidth][mapHeight][numberOfInputs];

	//Somewhere to hold our training and test data
	private Vector trainingDataList = new Vector();
	private Vector testingDataList  = new Vector();

	//A string to store our training history
	private String trainingHistory = "";

	//2d Gui bits
	private Image offscreenBuffer;
	private Thread mapThread = null;
	private Graphics graphicsBuffer;

	//Pick the size of the grid lines
	private Dimension gridDimentions = new Dimension(50, 50);

	//User interface bits and pieces that need to be available to everyone
	private JLabel cycleCounterLabel    = new JLabel("Current Cycle: 00000");
	private JTextField mapSizeText      = new JTextField(String.valueOf(mapWidth),3);
	private JTextField totalCycleText   = new JTextField(String.valueOf(maxCycles),5);
	private JTextField learningRateText = new JTextField(String.valueOf(initialLearningRate),4);
	private JTextField learningRadiusText= new JTextField(String.valueOf(initialLearningRadius),4);
	private JCheckBox drawNodeLinksCbox = new JCheckBox("Nodes?", true);
	private JCheckBox drawGridCbox      = new JCheckBox("GridLines?", true);
	private JTextField trainingFileText = new JTextField("",23);
	private JTextField testingFileText  = new JTextField("",23);
	private Button trainButton          = new Button("Train Map");
	private Button testButton           = new Button("Test Map");
	private JCheckBox updateDisplayCycleCbox = new JCheckBox("Each Cycle?", true);
	private JCheckBox updateDisplayStepCbox  = new JCheckBox("Each Step?", false);


	//main constructor
	public SOMDemonstration()
	{
		// define frame title
		super("Kohonen Map Application - 1230288");
		// define Menubar
		MenuBar mb = new MenuBar();
		setMenuBar(mb);

		// Define File menu and with Exit menu item
		Menu fileMenu = new Menu("File");
		mb.add(fileMenu);
		MenuItem historyMenuItem = new MenuItem("Show History");
		fileMenu.add(historyMenuItem);
		historyMenuItem.addActionListener (this);
		MenuItem historyClearMenuItem = new MenuItem("Clear History");
		fileMenu.add(historyClearMenuItem);
		historyClearMenuItem.addActionListener (this);
		fileMenu.add("--------");
		MenuItem exitMenuItem = new MenuItem("Exit");
		fileMenu.add(exitMenuItem);
		exitMenuItem.addActionListener (this);

		//Hard code the window size
		setBounds(10, 10, 500, 550);
		this.setResizable(false);
		width   = this.getWidth();
		height  = this.getHeight();
		heightMinusControls = height - AreaForControls;


		//Setup the background colours (including the tick boxes which
		// default to gray).
		setBackground(Color.white);
		updateDisplayStepCbox.setBackground(Color.white);
		updateDisplayCycleCbox.setBackground(Color.white);
		drawNodeLinksCbox.setBackground(Color.white);
		drawGridCbox.setBackground(Color.white);

		//Initialise a SOM with the default settings
		initSOM();

		//Default the input values 0 and 1 to l5. The paint method will draw
		// the first X and O to show the real value and winning value even
		// before we've started using the map. If they are both at 0.0 the
		// X will get drawn off the edge of the screen.
		//See the paint() method for a better understanding.
		inputs[0] = .5;
		inputs[1] = .5;

		//Setup the size of our grid
		gridDimentions.width = width / mapWidth;
		gridDimentions.height = height / mapHeight;

		//setup the layout
		FlowLayout flow = new FlowLayout();
		setLayout(flow);


		//Initialise the buttons and labels and things

		//Add the tick boxes
		add(new JLabel("Update Display:"));
		add(updateDisplayStepCbox);
		add(updateDisplayCycleCbox);
		add(new JLabel("Draw:"));
		add(drawNodeLinksCbox);
		add(drawGridCbox);

		//Map size
		add(new JLabel("LEARNING "));
		add(new JLabel("Map Size:"));
		add(mapSizeText);

		//Total Cycles Label and Text box
		add(new JLabel("Cycles:"));
		add(totalCycleText);

		//Learning Rate and radius label and text field
		add(new JLabel("Rate:"));
		add(learningRateText);
		add(new JLabel("Radius:"));
		add(learningRadiusText);

		//Training and Testing buttons and text files
		// Training
		trainButton.addActionListener(this);
		trainButton.setEnabled(false);

		add(trainButton);
		add(trainingFileText);

		Button loadTrainingDataButton = new Button("Find Training File");
		loadTrainingDataButton.addActionListener(this);
		add(loadTrainingDataButton);


		// Testing
		testButton.addActionListener(this);
		testButton.setEnabled(false);

		add(testButton);
		add(testingFileText);

		Button loadTestingDataButton = new Button("Find Testing File ");
		loadTestingDataButton.addActionListener(this);
		add(loadTestingDataButton);

		//Cycles so Far label
		add(cycleCounterLabel);


		//pack();
		setVisible(true);

	} //default constructor


	public static void main (String[] args)
	{
		Frame myFrame = new SOMDemonstration();
	}


	//This method starts a thread that will carry the training of the map
	// and its display to the user.
	public void startTraining()
	{
		//Make sure no training is already happening
		stopTraining();

		//Setup the area for our 2d graphics
		offscreenBuffer = createImage(width, height);
		graphicsBuffer  = offscreenBuffer.getGraphics();


		//Parse our Training data file
		try
		{
			parseDataFile(trainingFileText.getText(), trainingDataList);

		}
		catch(Exception e)
		{
			System.out.println(e);
			e.printStackTrace();
		}

		initSOM();

		if (mapThread == null)
		{
			mapThread = new Thread(this);
			mapThread.setPriority(Thread.MIN_PRIORITY);
			mapThread.start();
		}
	}

	//This is the main processing loop in the program. It handles (calls)
	// the training of the network and the display code.
	public void run()
	{
		while (mapThread != null)
		{
			//Update the Cycle Counter
			this.cycleNumber++;
			cycleCounterLabel.setText("Current Cycle: " + cycleNumber);

			//Train the network for another cycle.
			updateSOM();

			//We'll only draw the map if they ask us to
			if(updateDisplayCycleCbox.isSelected() )
			{
				repaint();
			}

			//Check to see if we're at the end of the training process
			if(cycleNumber == maxCycles)
			{
				stopTraining();
			}
		}
	}


	//A little method to stop the training thread (it checks the value of the
	// mapThread variable.
	public void stopTraining()
	{
		mapThread = null;
		cycleNumber = 0;
	}



	public void actionPerformed(ActionEvent event)
	{
		// close application, when exit is selected from the menubar
		if (event.getSource() instanceof MenuItem)
		{
			String menuLabel = ((MenuItem)event.getSource()).getLabel();
			if(menuLabel.equals("Exit"))
			{
				dispose();
				System.exit(0);
			}

			// If the user wants to look at the messy training history
			if(menuLabel.equals("Show History"))
			{
				JOptionPane.showMessageDialog(this, trainingHistory, "Testing History", JOptionPane.INFORMATION_MESSAGE);
			}

			//Clear out the history string
			if(menuLabel.equals("Clear History"))
			{
				trainingHistory = "";
			}

		}

		//Check for GUI actions/events
		String eventSource = event.getActionCommand();

		if(eventSource.trim().equals("Train Map"))
		{
			startTraining();
		}

		if(eventSource.trim().equals("Test Map"))
		{
		   //Parse our testing data file
			try
			{
				//parseDataFile("G:\\Daniel\\NeuralNets\\Assignment\\iris\\IRIS_TES.DAT", testingDataList);
				parseDataFile(testingFileText.getText(), testingDataList);
				//System.out.println("So it seems we have " + testingDataList.size() + " testing records.");
			}

			catch(Exception e)
			{
				System.out.println(e);
				e.printStackTrace();
			}

			//Test the map to see how accurate the results are
			//This method will check against the testing file and pop a results
			// dialog up on the screen
			testSOM();

		}

		//User clicked the Find Training File button
		if(eventSource.trim().equals("Find Training File"))
		{
			trainingFileText.setText(findFile("Select your Training File"));
			trainButton.setEnabled(true);
		}

		//User selected the Testing file button
		if(eventSource.trim().equals("Find Testing File"))
		{
			testingFileText.setText(findFile("Select your Testing File"));
			testButton.setEnabled(true);
		}

	}

	//Slight extension to the update method, here we paint our 2D graphics
	// area aswell as the normal GUI bits and pieces.
	public void update(Graphics g)
	{
		paint(graphicsBuffer);
		g.drawImage(offscreenBuffer, 0, 0, this);
	}


	//The Paint() method. There is a lot of work to do here.
	//Basically we...
	public void paint(Graphics g)
	{
		//Get the current state of the Map
		map.getWeights(weights);

		//Prepare our map drawing space
		g.clearRect(0, 0, width-1, height-1);
		g.setColor(Color.white);
		g.drawRect(0, AreaForControls, width-1, height-1);

		//Update the buttons, tickboxes etc
		super.paint(g);

		//Draw the background grid lines on the window
		// Dont draw any lines over the area for the controls
		if(drawGridCbox.isSelected() )
		{
			g.setColor(Color.lightGray);
			//Vertical
			for (int i = 0; i < width; i += gridDimentions.width)
			{
				g.drawLine(i, AreaForControls, i, height-1);
			}
			//Horizontal
			for (int j = AreaForControls; j < height; j += gridDimentions.height)
			{
				g.drawLine(0, j, width, j);
			}
		}

		//In the next few stages we're going to be drawing a lot of things
		// depending on the positions of the nodes in the map, for that reason
		// we'll need a few temporary values to hold positions for two different
		// nodes.
		int node1x, node1y, node2x, node2y;


		//Draw links between nodes
		if(drawNodeLinksCbox.isSelected() )
		{
			// 'vertical' links
			g.setColor(Color.gray);
			for (int i = 0; i < map.mapWidth; i++)
			{
				for (int j = 0; j < map.mapHeight -1; j++)
				{
					node1x = (int)(weights[i][j][0] * width);
					node1y = AreaForControls + ((int)(weights[i][j][1] * heightMinusControls));
					node2x = (int)(weights[i][j+1][0] * width);
					node2y = AreaForControls + ((int)(weights[i][j+1][1] * heightMinusControls));
					g.drawLine(node1x, node1y, node2x, node2y);
				}
			}

			// 'horizontal' links
			g.setColor(Color.gray);
			for (int j = 0; j < map.mapHeight; j++)
			{
				for (int i = 0; i < map.mapWidth-1; i++)
				{
					node1x = (int)(weights[i][j][0] * width);
					node1y = AreaForControls + ((int)(weights[i][j][1] * heightMinusControls));
					node2x = (int)(weights[i+1][j][0] * width);
					node2y = AreaForControls + ((int)(weights[i+1][j][1] * heightMinusControls));
					g.drawLine(node1x, node1y, node2x, node2y);
				}
			}
		}


		//Draw the nodes themselves
		g.setColor(Color.blue);
		for (int j = 0; j < map.mapHeight; j++)
		{
			for (int i = 0; i < map.mapWidth; i++)
			{
				node1x = (int)(weights[i][j][0] * width);
				node1y = AreaForControls + ((int)(weights[i][j][1] * heightMinusControls));
				g.fillOval(node1x-1, node1y-1, 3, 3);
			}
		}


		//We'll only draw the map each step if they ask us to
		if(updateDisplayStepCbox.isSelected() )
		{
			//Draw the cross to show where the real value would have been in the
			g.setColor(Color.blue);
			node1x = (int)(inputs[0] * width);
			node1y = AreaForControls + ((int)(inputs[1] * heightMinusControls));
			g.drawLine(node1x-10, node1y-10, node1x+10, node1y+10);
			g.drawLine(node1x-10, node1y+10, node1x+10, node1y-10);


			//Work out the winning node based on the last record we trained with
			int[] winner = map.getWinner();

			//Draw the circle to show the node that won
			g.setColor(Color.red);
			node1x = (int)(weights[winner[0]][winner[1]][0] * width);
			node1y = AreaForControls + ((int)(weights[winner[0]][winner[1]][1] * heightMinusControls));
			g.drawOval(node1x-20, node1y-20, 20, 20);

		} // if(updateDisplayStepCbox.isSelected() )
	} //paint(Graphics g)


	//Here we actually create an instance of our Kohoen map class and
	// setup its values. This normally gets called when the program starts
	// (with default values) and then again when the user presses the test
	// button (because of my sloppy code)
	public void initSOM()
	{
		//Get the values from the user for Number of cycleNumber, the map
		// size and learning rate.
		maxCycles           = Integer.parseInt(totalCycleText.getText());
		mapWidth            = Integer.parseInt(mapSizeText.getText());
		mapHeight           = Integer.parseInt(mapSizeText.getText());
		initialLearningRate = Double.parseDouble(learningRateText.getText());
		initialLearningRadius= Double.parseDouble(learningRadiusText.getText());

		//Now make a new map
		map = new KohonenMap(numberOfInputs, mapWidth, mapHeight);

		//Set the map parameters
		map.initialLearningRate     = initialLearningRate;
		map.initialUpdateRadius     = initialLearningRadius;
		map.wrapAroundMapBorders    = false;

		map.init();

		// reset the size of our map storage areas
		inputs  = new double[numberOfInputs];
		weights = new double[mapWidth][mapHeight][numberOfInputs];

		//Store the default values in the map we've just made
		map.getWeights(weights);

		//Default the input values 0 and 1 to .5
		//The paint method will draw the first X and O to show
		// the real value and winning value even before we've
		// started using the map. If they are both at 0.0 the
		// X will get drawn off the edge of the screen.
		//See the paint() method for a better understanding.
		inputs[0] = .5;
		inputs[1] = .5;
	}


	//Training the Map with data from the test file
	private void updateSOM()
	{
		dataUnit tempDataUnit;

		//Step through every element of the vector of dataUnits and train
		// the network with each one.
		for (Enumeration e = trainingDataList.elements(); e.hasMoreElements(); )
		{
			tempDataUnit = (dataUnit)e.nextElement();

			for(int i=0; i < numberOfInputs; i++)
			{
				inputs[i] = tempDataUnit.values[i];
			}

			map.inputLabel = tempDataUnit.label;
			map.train(inputs);

			//We'll only draw the map each step if they ask us to
			if(updateDisplayStepCbox.isSelected() )
			{
				repaint();

				//sleep for a moment so that they can see what we've just
				// done to the map
				try
				{
					Thread.currentThread().sleep(400);
				}
				catch (InterruptedException sleepE)
				{
					System.out.println(sleepE);
				}

			}
		}

	}//updateSOM()


	//Here we're going to step through every element in the testing data file
	// and grab its input data and data label. Then we're going to find the
	// input data's position in the map and find out if the label are the same.
	public void testSOM()
	{
		dataUnit tempDataUnit;
		int correctCounter = 0;
		int errorCounter = 0;
		double answer = 0.0;

		//Step through every element of the vector of testing dataUnits
		for (Enumeration e = testingDataList.elements(); e.hasMoreElements(); )
		{
			tempDataUnit = (dataUnit)e.nextElement();

			for(int i=0; i < numberOfInputs; i++)
			{
				inputs[i] = tempDataUnit.values[i];
			}

			answer = map.test(inputs);

			map.inputLabel = tempDataUnit.label;
			if(tempDataUnit.label == answer)
			{
				correctCounter++;
			}
			else
			{
				errorCounter++;
			}
		}

		//Work out the message that we'll show to the user
		String resultMessage;

		//Get how many cycles we've made. If the map has finnished training
		// the cycle counter will have been reset, so get the max cycles instead
		int resultCycles = cycleNumber;

		if(resultCycles == 0)
		{
			resultCycles = maxCycles;
		}

		//Work out the percentage acurracy of the map
		double resultAccuracy;
		resultAccuracy = ((Double.parseDouble(String.valueOf(correctCounter)) - Double.parseDouble(String.valueOf(errorCounter))) / Double.parseDouble(String.valueOf(correctCounter))) * 100;
		//I have no idea how to format numbers in Java. Its official
		NumberFormat nfi = NumberFormat.getInstance();

		//Create a result message to show to the user
		resultMessage = resultCycles + " Training Cycles.";
		resultMessage += "\n" + initialLearningRate + " was the initial learning rate.";
		resultMessage += "\n" + initialLearningRadius + " was the initial learning radius.";
		resultMessage += "\n" + correctCounter + " records were correctly identified.";
		resultMessage += "\n" + errorCounter + " records were wrong.";
		resultMessage += "\n" + nfi.format(resultAccuracy) + "% Accuracy.";

		//Now show a messagebox to the user to tell them how many we got right
		JOptionPane.showMessageDialog(this, resultMessage, "Testing Results", JOptionPane.INFORMATION_MESSAGE);

		trainingHistory += "\n\n";
		trainingHistory += resultMessage;
	}

	//Read in formatted data from a text file. This method needs to be passed
	// a file name and a vector to store the results in, but it will work out
	// the depth of the data itself.
	private void parseDataFile(String strFilename, Vector recordsVector) throws Exception
	{
		//clearout the vector of training/testing Data
		recordsVector.clear();

		FileReader fr = new FileReader(strFilename);

		BufferedReader br = new BufferedReader(fr);

		StringTokenizer st;

		//Temp counters for when we parse
		int numberOfElements = 0;
		int elementCount = 0;

		//Temp strings for when we parse
		String tmpLine;
		String tmpElement;

		//Temp variable to hold each new record
		double tmpRecordLabel;
		double[] tmpRecordData;


		//Now we are ready to step through the text file and parse all its numbers
		// into data units. We dont know ahead of time how many data records are in
		// the text file, or how many elements they have, but we do know that there
		// is one record per line, and a record consists of a number of data values
		// and one label value (the last one on the line). So if we know how many
		// elements are on the line we can just split it up as necessary.
		while(br.ready())
		{

		  tmpLine = br.readLine();

		  if (tmpLine != null)
		  {

			//The first time around the loop we will iterate through the elements
			// of the first line (that we get from the string tokenizer) and count
			// how many we get. That way we know how many values there are. Then we
			// can make new data units.
			if(numberOfElements == 0)
			{
				st = new StringTokenizer(tmpLine, " ");

				while(st.hasMoreTokens())
				{
					numberOfElements++;
					st.nextToken();
				}
			}

			//Now that we know how many elements the data set has set the
			// global variable (minus 1 because we dont want to include the label)
			numberOfInputs = numberOfElements -1;

			//Parse the line into some temporary variables
			st = new StringTokenizer(tmpLine, " ");

			//Get our temporary record array/variable read
			tmpRecordData  = new double[numberOfElements - 1];
			tmpRecordLabel = 0.0;

			//Reset our in-record element counter
			elementCount = 0;

			while(st.hasMoreTokens())
			{
				elementCount++;
				tmpElement = "";
				tmpElement = st.nextToken().trim();

				//The first few times we loop we are getting datavalues
				// but the last one we get is a label, so it has to be stored
				// differently.
				if(elementCount == numberOfElements)
				{
					tmpRecordLabel = Double.parseDouble(tmpElement);
				}
				else
				{
					tmpRecordData[elementCount - 1] = Double.parseDouble(tmpElement);
				}
			}

			//Add the new record we've created to the vector we've picked for storage
			recordsVector.add(new dataUnit(tmpRecordData, tmpRecordLabel));

		  }

		}
	}


	//This is just a wrapper around the java file browser code to let the user
	// find their training and testing files. It is all just java101 code so its
	// not commented really.
	private String findFile(String searchPhrase)
	{
		JFileChooser filechooser = new JFileChooser();

		filechooser.setCurrentDirectory( new File(".") );
		filechooser.setApproveButtonText("Select");
		filechooser.setDialogTitle(searchPhrase);

		if ( filechooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION ) return "";

		File file = filechooser.getSelectedFile();

		String fname = file.getAbsolutePath();

		return fname;
	}




}