/**
 * --Copyright notice-- 
 *
 * Copyright (c) School of Geography, University of Leeds. 
 * http://www.geog.leeds.ac.uk/
 * This software is licensed under 'The Artistic License' which can be found at 
 * the Open Source Initiative website at... 
 * http://www.opensource.org/licenses/artistic-license.php
 * Please note that the optional Clause 8 does not apply to this code.
 *
 * The Standard Version source code, and associated documentation can be found at... 
 * [online] http://mass.leeds.ac.uk/
 * 
 *
 * --End of Copyright notice-- 
 *
 */

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.io.Serializable;

/**
* Main class for an agent-based model.<P>
* The model has an Environment, containing raster data.<P>
* It also has a series of Agent objects.<P>
* Each iteration of the model, the agent's run method is called.<P>
* The model also implements a model-wide stopping criterion.<P>
* @version 1.0
* @author <A href="http://www.geog.leeds.ac.uk/people/a.evans/">Andy Evans</A>
*/
public class Model extends Frame implements ActionListener, Serializable {

	
	// We keep all the model parameters here in one place.
	// Agent and Environment parameters are kept in those classes, except initial default Environment size.
	
	/** Number of timesteps in model. */
	private int numberOfIterations = 10000;
	
	/** Number of agents in model. */
	private int numberOfAgents = 10;
	
	
	/** Reader/writer. */
	private IO io = new IO();
	
	/** For drawing. */
	private Canvas c = new Canvas();
	
	/** For enabling. */
	MenuItem runMenuItem = new MenuItem("Run");
	
	/** For enabling. */
	MenuItem saveMenuItem = new MenuItem("Save Results...");
	
	/** For eating environment data. */
	private double eatingRate = 10.0;
	
	/** For eating environment data. */
	private double fullUp = -1.0;

	
	/** 
	* List of all agents. 
	* Note that the use of the Agent interface allows for heterogeneous agents.
	*/
	private ArrayList <Agent> agents = new ArrayList <Agent>();
	
	/** Environment to store raster data. */
	private Environment world = new Environment();
	
	/** Extra agent that the user can control. **/
	private Protector protector = null;
	

	
	
	/**
	* Model constructor. 
	* The arguments, if used, should be:
	* java Model numberOfAgents:int numberOfIterations:int eatingRate:double fullUp:double fileIn:String fileOut:String 
	* @param args String sequence. 
	*/
	public Model (String args[]) {

		if (args.length == 0) {

			buildGui(); 

		} else {

			numberOfAgents = Integer.parseInt(args[0]);
			numberOfIterations = Integer.parseInt(args[1]);
			eatingRate = Double.parseDouble(args[2]);
			fullUp = Double.parseDouble(args[3]);
			String fileIn = args[4];
			String fileOut = args[5];
			File fIn = new File(fileIn);
			File fOut = new File(fileOut);

			world.setData(io.readData(fIn));
			setSize(world.getWidth() + getInsets().left + getInsets().right,
			world.getHeight() + getInsets().top + getInsets().bottom);
			buildAgents();

			runAgents();

			io.writeData(world.getData(), fOut);

		}

	}
	
	
	
	
	/**
	* Builds the GUI.
	*/
	private void buildGui() {
	
		
		add(c);
	
		setSize(300,300);
		
		addWindowListener(new WindowAdapter(){
			public void windowClosing(WindowEvent e){
				((Frame)e.getSource()).dispose();
				// System.exit(0);
			}
		});
		MenuBar menuBar = new MenuBar();
		Menu fileMenu = new Menu("File");
		menuBar.add(fileMenu);
		
		MenuItem openMenuItem = new MenuItem("Open...");
		fileMenu.add(openMenuItem);
		openMenuItem.addActionListener(this);
		
		
		fileMenu.add(saveMenuItem);
		saveMenuItem.addActionListener(this);
		saveMenuItem.setEnabled(false);
		
		Menu modelMenu = new Menu("Model");
		menuBar.add(modelMenu);
		
		
		modelMenu.add(runMenuItem);
		runMenuItem.addActionListener(this);
		runMenuItem.setEnabled(false);
		
		setMenuBar(menuBar);
		
		setLocation(
			(int)(Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 2) - (getWidth() / 2), 
			(int)(Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2) - (getHeight() / 2)
		);
		
		setVisible(true);
		
	}
	
	
	
	/**
	* Paints agents and data to the screen.
	* @param g Graphics context (actually draws on a canvas, so unused)
	*/
	public void paint(Graphics g) {
	
		Image image = world.getDataAsImage(); 
		Graphics cg = c.getGraphics();
		
		if (image != null) {
			cg.drawImage(image, 0, 0, this);
		}
		

		
		for (Agent agent : agents) {
			cg.setColor(Color.GREEN);
			cg.fillOval(agent.getX() - 1,agent.getY() - 1,2,2);
			cg.drawPolygon(agent.getNeighbourhood());
			cg.setColor(Color.BLACK);	
		}
			
		if (protector != null) {		
			cg.setColor(Color.YELLOW);
			cg.fillOval(protector.getX() - 1,protector.getY() - 1,2,2);
			cg.drawPolygon(protector.getNeighbourhood());
			cg.setColor(Color.BLACK);
		}
		
	}

	
	
	
	
	/**
	* Listens to menus.
	*/
	public void actionPerformed(ActionEvent e) {
	
		MenuItem clickedMenuItem = (MenuItem)e.getSource();
	
		if (clickedMenuItem.getLabel().equals("Open...")) {
			FileDialog fd = new FileDialog(this, "Open File", FileDialog.LOAD);
			fd.setVisible(true);
			File f = null;
			if((fd.getDirectory() != null)||( fd.getFile() != null)) {
				f = new File(fd.getDirectory() + fd.getFile());
				world.setData(io.readData(f));
				setSize(world.getWidth() +  getInsets().left +  getInsets().right, world.getHeight() +  getInsets().top +  getInsets().bottom);
				buildAgents();
			}
			runMenuItem.setEnabled(true);
		}
		
		if (clickedMenuItem.getLabel().equals("Save Results...")) {
			FileDialog fd = new FileDialog(this, "Save File", FileDialog.SAVE);
			fd.setVisible(true);
			File f = null;
			if((fd.getDirectory() != null)||( fd.getFile() != null)) {
				f = new File(fd.getDirectory() + fd.getFile());
				io.writeData(world.getData(), f);
			}
		}	
		
		if (clickedMenuItem.getLabel().equals("Run")) {
			Thread tt = new Thread(new Runnable() {
				public void run() {
		
					runAgents();

			}});
			tt.start();
			saveMenuItem.setEnabled(true);
		}
		
		repaint();
		
	}
	
	
	
	
	/**
	* Makes the world with initial values.
	*/
	private void buildWorld() {
	
		for (int i = 0; i < world.getHeight(); i++) {
			for (int j = 0; j < world.getWidth(); j++) {
				world.setDataValue(j, i, 255.0);
			}
		}
		
	}	
	
	
	
	
	/**
	* Makes the agents.
	*/
	private void buildAgents() {
	
		for (int i = 0; i < numberOfAgents; i++) {

			agents.add(new Nibbler(world,agents,eatingRate,fullUp));

		}

		// Make agent controlled by user, and add appropriate key listener.
		
		protector = new Protector(world,agents);

		agents.add(protector);
		
		addKeyListener(new KeyListener() {
		
			public void keyPressed(KeyEvent e) {

				int keyCode = e.getKeyCode();
				String keyString = KeyEvent.getKeyText(keyCode);
				
				switch(keyString) {
					case ("Up") : 
						protector.setY(protector.getY() - 1);
						break;
					case ("Down") : 
						protector.setY(protector.getY() + 1);
						break;
					case ("Left") : 
						protector.setX(protector.getX() - 1);
						break;
					case ("Right") : 
						protector.setX(protector.getX() + 1);
						break;

				}
							
			}
			
			public void keyReleased(KeyEvent e) {
			
			}
			
			public void keyTyped(KeyEvent e) {
			
			}
		
		});
		
	
	}	
	


	
	/**
	* Runs the agents.
	* Iterates through numberOfIterations. Randomises the agent order.
	*/	
	private void runAgents() {
		

		long startTime = System.currentTimeMillis();
		
		for (int i = 0; i < numberOfIterations; i++) {
		
			Collections.shuffle(agents);
			
			for (int j = 0; j < agents.size(); j++) {
				agents.get(j).run();
				// System.out.println(i + " " + agents.get(j).getX() + " " + agents.get(j).getY() + " " + world.getDataValue(agents.get(j).getX(), agents.get(j).getY()));
			}
			update(getGraphics());

			if (stoppingCriteriaMet() == true) break;
			
		} 
		
		System.out.println("Time taken = " + (((double)((System.currentTimeMillis() - startTime))) / 1000.0) + " seconds");
		
	}
	
	
	
	
	/**
	* Checks stopping criteria.
	* Returns true if stopping criteria met, false otherwise, including where the 
	* criteria can't be checked.
	* Here the stopping criterion is that there is nothing left in the environment.
	* @return whether the stopping criteria have been met
	*/
	private boolean stoppingCriteriaMet() {
	
		for (int i = 0; i < world.getHeight(); i++) {
			for (int j = 0; j < world.getWidth(); j++) {
				if (world.getDataValue(j, i) > 0.0) return false;
			}
		}
		return true;
	
	}
	
	
	
	
	/**
	* Just calls the constructor.
	* The arguments, if used, should be:
	* java Model numberOfAgents:int numberOfIterations:int eatingRate:double fullUp:double fileIn:String fileOut:String 
	* @param args String sequence. 
	*/
	public static void main (String args[]) {

		new Model(args);
		
	}


}