package tsp;

import java.applet.Applet;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.FontUIResource;

import tsp.map.TSPMap;
import tsp.solvers.GeneticSolver;
import tsp.solvers.NearestNeigbourSolver;
import tsp.solvers.Tour;
import tsp.solvers.rnearest.NearestNeigbourRandomizedSolver;
import tsp.util.BestList;
import tsp.util.delay;

public class TSPApplet extends Applet {
	private static final long serialVersionUID = 1L;
	public static final int MAX_POINTS = 2048;
	protected static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors();
	TSPMap map = null;
	TSPApplet self = this;
	int pointsCount = 100;
	int iterationCount = 100;
	int popCount = 50;
	int crossFactor = 40;
	int muteFactor = 20;
	TourDisplay display;
	public JLabel lenLabel;
	JTextField inputPoints = new JTextField();
	JTextField inputIterations = new JTextField();
	JTextField outputDistance = new JTextField();
	JTextField outputTime = new JTextField();
	JTextField inputPop = new JTextField();
	JTextField inputCross = new JTextField();
	JTextField inputMute = new JTextField();
	JSlider inputRnnDegree = new JSlider(JSlider.HORIZONTAL, 1, 100, 1);
	JLabel inputRnnDegreeLabel = null;
	final JButton gridButton = new JButton("Grid");
	final JButton newButton = new JButton("Random");
	final JButton nnButton = new JButton("Nearest Neighbor");
	final JButton rnnButton = new JButton("Random Neighbor");
	final JButton optButton = new JButton("2-opt");
	final JButton gaButton = new JButton("Genetic");
	final JButton gaModButton = new JButton("Genetic-M");
	final JButton stopButton = new JButton("STOP");
	Thread worker;
	boolean threadWorking = false;

	private Tour bestTour = null;
	private BestList bestValues = new BestList();
	private JTextArea bestValuesLabel = new JTextArea();
	private long bestTourStarted = 0;
	private String currentTour = "";
	private boolean needToStop = false;

	public TSPApplet() {
		map = null;
		threadWorking = false;
		clearBestTour("");
	}

	public void init() {
		try {
			UIManager.getDefaults().put("Label.font", new FontUIResource("Dialog", Font.PLAIN, 11));
		} catch (Exception e) {
			System.out.println("Failed to set font to labels");
			e.printStackTrace();
		}
		// this.setBackground(Color.lightGray);
		display = new TourDisplay(map);
		display.setMinimumSize(new Dimension(400, 610));
		display.setPreferredSize(new Dimension(400, 610));
		lenLabel = new JLabel("Press 'Random' or 'Grid' button to start...");
		inputRnnDegreeLabel = new JLabel();
	}

	private void toggleOperationButtons(boolean enabled) {
		nnButton.setEnabled(enabled);
		rnnButton.setEnabled(enabled);
		optButton.setEnabled(enabled);
		gaButton.setEnabled(enabled);
		gaModButton.setEnabled(enabled);
		gridButton.setEnabled(enabled);
		newButton.setEnabled(enabled);
	}

	public void start() {
		this.setBackground(Color.WHITE);
		this.setLayout(new FlowLayout(1, 0, 0));
		JPanel panel = new JPanel();
		panel.setPreferredSize(new Dimension(420, 645));
		panel.setBackground(getBackground());
		panel.add(display);
		Border border = new TitledBorder("Map");
		panel.setBorder(border);
		this.add(panel);

		JPanel rPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
		rPanel.setPreferredSize(new Dimension(170, 645));
		rPanel.setLocation(0, 0);
		panel = new JPanel();
		panel.setBackground(getBackground());
		rPanel.setBackground(getBackground());

		panel.setBorder(new TitledBorder("Parameters"));
		panel.setPreferredSize(new Dimension(170, 180));
		// panel.setBackground(getBackground());
		panel.add(new JLabel("      # of cities:"));
		inputPoints.setPreferredSize(new Dimension(50, 17));
		inputPoints.setText(Integer.toString(pointsCount));
		panel.add(inputPoints);
		panel.add(new JLabel("# of iterations:"));
		inputIterations.setPreferredSize(new Dimension(50, 17));
		inputIterations.setText(Integer.toString(iterationCount));
		panel.add(inputIterations);
		panel.add(new JLabel("Population(GA):"));
		inputPop.setPreferredSize(new Dimension(30, 17));
		inputPop.setText(Integer.toString(popCount));
		panel.add(inputPop);
		panel.add(new JLabel("Cross Factor(GA):"));
		inputCross.setPreferredSize(new Dimension(30, 17));
		inputCross.setText(Integer.toString(crossFactor));
		panel.add(inputCross);
		panel.add(new JLabel("Mute Factor(GA):"));
		inputMute.setPreferredSize(new Dimension(30, 17));
		inputMute.setText(Integer.toString(muteFactor));
		panel.add(inputMute);
		inputRnnDegreeLabel.setAlignmentX(JComponent.CENTER_ALIGNMENT);
		inputRnnDegreeLabel.setPreferredSize(new Dimension(150, 14));
		panel.add(inputRnnDegreeLabel);
		inputRnnDegree.setPreferredSize(new Dimension(150, 20));
		inputRnnDegree.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent e) {
				inputRnnDegreeLabel.setText("Degree (NNR): " + inputRnnDegree.getValue());
			}
		});
		panel.add(inputRnnDegree);
		inputRnnDegree.setValue(100);
		inputRnnDegree.setBackground(getBackground());
		rPanel.add(panel);

		panel = new JPanel();
		panel.setBackground(getBackground());
		panel.setBorder(new TitledBorder("Results"));
		// panel.setBackground(getBackground());
		panel.setPreferredSize(new Dimension(170, 75));
		panel.add(new JLabel("Distance:"));
		outputDistance.setPreferredSize(new Dimension(90, 17));
		outputDistance.setText("0");
		panel.add(outputDistance);
		panel.add(new JLabel("Time(ms):"));
		outputTime.setPreferredSize(new Dimension(80, 17));
		outputTime.setText("0");
		panel.add(outputTime);
		rPanel.add(panel);

		panel = new JPanel();
		panel.setBackground(getBackground());
		panel.setPreferredSize(new Dimension(170, 60));
		panel.setBorder(new TitledBorder("Points"));
		panel.add(newButton);
		panel.add(gridButton);
		rPanel.add(panel);

		panel = new JPanel();
		panel.setBackground(getBackground());
		panel.setPreferredSize(new Dimension(170, 180));
		panel.setBorder(new TitledBorder("Methods"));

		int bW = 150;
		int bH = 20;

		nnButton.setPreferredSize(new Dimension(bW, bH));
		panel.add(nnButton);
		rnnButton.setPreferredSize(new Dimension(bW, bH));
		panel.add(rnnButton);
		optButton.setPreferredSize(new Dimension(bW, bH));
		panel.add(optButton);
		gaButton.setPreferredSize(new Dimension(bW, bH));
		panel.add(gaButton);
		gaModButton.setPreferredSize(new Dimension(bW, bH));
		panel.add(gaModButton);
		stopButton.setPreferredSize(new Dimension(bW, bH));
		panel.add(stopButton);
		rPanel.add(panel);

		panel = new JPanel();
		panel.setBackground(getBackground());
		panel.setPreferredSize(new Dimension(170, 150));
		panel.setBorder(new TitledBorder("Best lengths"));
		bestValuesLabel.setPreferredSize(new Dimension(150, 120));
		panel.add(bestValuesLabel);
		rPanel.add(panel);

		this.add(rPanel);
		this.add(new JLabel("TSP Java Implementation - Ramūnas Dronga IT-4/1, Ramūnas Kraujutis."));
		this.add(lenLabel);

		newButton.addActionListener(new ActionListener() {
			public void actionPerformed(final ActionEvent e) {
				doNew();
			}

		});
		gridButton.addActionListener(new ActionListener() {
			public void actionPerformed(final ActionEvent e) {
				doGrid();
			}

		});
		nnButton.addActionListener(new ActionListener() {
			public void actionPerformed(final ActionEvent e) {
				doNearestNeighbour();
			}

		});
		rnnButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				doRNearestNeighbour();
			}
		});
		optButton.addActionListener(new ActionListener() {
			public void actionPerformed(final ActionEvent e) {
				do2Opt();
			}

		});
		gaButton.addActionListener(new ActionListener() {
			public void actionPerformed(final ActionEvent e) {
				doGeneticAlgorithm();
			}

		});
		gaModButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				doGeneticAlgorithmModified();
			}

		});
		stopButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				needToStop = true;
				stopButton.setEnabled(false);
				while (threadWorking) {
					delay.pause(10);
				}
				toggleOperationButtons(!threadWorking);
				stopButton.setEnabled(true);
			}
		});
	}

	public void doNew() {
		if (threadWorking) {
			return;
		} else {
			bestValues.clear();
			lenLabel.setText("Choose algorithm to solve map...");
			pointsCount = Integer.parseInt(inputPoints.getText());
			if (pointsCount > TSPApplet.MAX_POINTS) {
				pointsCount = TSPApplet.MAX_POINTS;
				inputPoints.setText(Integer.toString(pointsCount));

			}
			map = new TSPMap(pointsCount, display.getWidth(), display.getHeight());
			display.setMap(map);
			return;
		}
	}

	public void doGrid() {
		if (threadWorking) {
			return;
		} else {
			bestValues.clear();
			lenLabel.setText("Choose method to solve map...");
			pointsCount = Integer.parseInt(inputPoints.getText());
			map = new TSPMap();
			map.createGridCities(pointsCount, display.getWidth(), display.getHeight());

			display.setMap(map);
			return;
		}
	}

	public void doRNearestNeighbour() {
		if (threadWorking || map == null) {
			return;
		} else {
			worker = new Thread() {
				public void run() {
					clearBestTour("NNR-" + getDegree());
					toggleOperationButtons(!threadWorking);
					iterationCount = Integer.parseInt(inputIterations.getText());
					ArrayList<NearestNeigbourRandomizedSolver> solvers = new ArrayList<NearestNeigbourRandomizedSolver>();
					for (int i = 0; i < PROCESSOR_COUNT; i++) {
						solvers.add(new NearestNeigbourRandomizedSolver(self, map, (int) Math.round(Math.ceil(iterationCount / PROCESSOR_COUNT))));
						solvers.get(solvers.size() - 1).start();
					}
					long iter;
					boolean allFinished;
					do {
						iter = 0;
						allFinished = true;
						for (NearestNeigbourRandomizedSolver solver : solvers) {
							if (solver.isAlive()) {
								allFinished = false;
							}
							iter += solver.getIteration();
						}
						lenLabel.setText("Calculating (" + iter + ")");
						delay.pause(100);
					} while (!allFinished);
					lenLabel.setText("NNR finished. Best length: " + bestTour.getLength());
					displayTourInfo();
					threadWorking = false;
					toggleOperationButtons(!threadWorking);
				}
			};
			threadWorking = true;
			toggleOperationButtons(!threadWorking);
			worker.start();
		}
	}

	public void doNearestNeighbour() {
		if (threadWorking || map == null) {
			return;
		} else {
			worker = new Thread() {
				public void run() {
					clearBestTour("NN");
					NearestNeigbourSolver tsp = new NearestNeigbourSolver(self, map);
					tsp.start();
					while (tsp.isAlive()) {
						lenLabel.setText("Calculating " + tsp.getIteration());
						delay.pause(10);
					}
					lenLabel.setText("NN done: " + bestTour.getLength());
					displayTourInfo();
					threadWorking = false;
					toggleOperationButtons(!threadWorking);
				}
			};
			threadWorking = true;
			toggleOperationButtons(!threadWorking);
			worker.start();
		}
	}

	public void do2Opt() {
		if (threadWorking || map == null) {
			return;
		} else {
			worker = new Thread() {
				public void run() {
					clearBestTour("2-opt");
					iterationCount = Integer.parseInt(inputIterations.getText());
					Tour tour = Tour.getRandomTour(map);
					for (int i = 1; !needStop() && i <= iterationCount; i++) {
						tour.randomize();
						tour.do2Opt();
						addTour(tour);
						lenLabel.setText("Calculating (" + i + ")");
						delay.pause(10);
					}
					lenLabel.setText("2-opt done: " + bestTour.getLength());
					displayTourInfo();
					threadWorking = false;
					toggleOperationButtons(!threadWorking);
				}
			};
			threadWorking = true;
			toggleOperationButtons(!threadWorking);
			worker.start();
		}
	}

	public void doGeneticAlgorithm() {
		if (map == null)
			return;
		if (!threadWorking) {
			worker = new Thread() {
				public void run() {
					clearBestTour("GA");
					iterationCount = Integer.parseInt(inputIterations.getText());
					popCount = Integer.parseInt(inputPop.getText());
					crossFactor = Integer.parseInt(inputCross.getText());
					muteFactor = Integer.parseInt(inputMute.getText());

					final GeneticSolver tsp = new GeneticSolver(map, popCount, crossFactor, muteFactor);
					for (int i = 1; !needStop() && i < iterationCount; i++) {
						tsp.doOneGeneration();
						addTour(tsp.getSolution());
						lenLabel.setText("Calculating (" + i + ")");
					}
					lenLabel.setText("GA done: " + bestTour.getLength());
					displayTourInfo();
					threadWorking = false;
					toggleOperationButtons(!threadWorking);
				}

			};
			threadWorking = true;
			toggleOperationButtons(!threadWorking);
			worker.start();
		}
	}

	public void doGeneticAlgorithmModified() {
		if (map == null)
			return;
		if (!threadWorking) {
			worker = new Thread() {

				public void run() {
					clearBestTour("GA-M");
					iterationCount = Integer.parseInt(inputIterations.getText());
					popCount = Integer.parseInt(inputPop.getText());
					crossFactor = Integer.parseInt(inputCross.getText());
					muteFactor = Integer.parseInt(inputMute.getText());
					final GeneticSolver tsp = new GeneticSolver(map, popCount, crossFactor, muteFactor);
					for (int i = 1; !needStop() && i < iterationCount; i++) {
						tsp.doOneGenerationModified();
						addTour(tsp.getSolution());
						lenLabel.setText("Calculating (" + i + ")");
					}
					lenLabel.setText("GA-M done: " + bestTour.getLength());
					displayTourInfo();
					threadWorking = false;
					toggleOperationButtons(!threadWorking);
				}

			};
			threadWorking = true;
			toggleOperationButtons(!threadWorking);
			worker.start();
		}
	}

	public int getDegree() {
		return inputRnnDegree.getValue();
	}

	private boolean betterTour(Tour tour) {
		return tour != null && (bestTour == null || bestTour.getLength() > tour.getLength());
	}

	public void addTour(Tour tour) {
		if (betterTour(tour)) {
			synchronized (this) {
				if (betterTour(tour)) {
					bestTour = (Tour) tour.clone();
					bestValues.put(currentTour, bestTour.getLength());
					displayTour(bestTour);
					displayTourInfo();
				}
			}
		}
	}

	public void displayTourInfo() {
		if (bestTour != null) {
			outputDistance.setText(String.format("%4.2f", bestTour.getLength()));
			outputTime.setText(String.format("%d", System.currentTimeMillis() - bestTourStarted));
			StringBuilder text = new StringBuilder();
			for (String method : bestValues.keySet()) {
				text.append(method).append(": ");
				text.append(String.format("%4.2f", bestValues.get(method))).append("\n");
			}
			bestValuesLabel.setText(text.toString());
		}
	}

	public Tour displayTour(Tour tour) {
		display.setTour(bestTour);
		return tour;
	}

	public TourDisplay getTourDisplay() {
		return display;
	}

	public void clearBestTour(String currentTour) {
		this.currentTour = currentTour;
		bestTour = null;
		bestTourStarted = System.currentTimeMillis();
		needToStop = false;
		outputDistance.setText("");
		outputTime.setText("");
	}

	public boolean needStop() {
		return needToStop;
	}
}
