package Connection.socket;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import classes.HolonElement;
import classes.HolonObject;
import ui.controller.Control;

import java.io.*;

public class Server implements Runnable{
	private ServerSocket serverSocket;
    private Socket clientSocket;
    private DataOutputStream out;
    private DataInputStream in;
    private boolean stopped = false;		
    private boolean connection = false;
    private ui.view.component.Console console;
    
    
    private HolonObject observed;
	private HolonObjectModel modelField;
    private HolonObjectModel newModelField;
	private Control control;
    
    public Server(int port, ui.view.component.Console console, HolonObject observed, Control control) throws IOException {
    	this.observed = observed;
    	this.console = console;
    	this.control = control;
    	//Bind Port
    	serverSocket = new ServerSocket(port);
        //Wait for Connection
    	Thread serverThread = new Thread(this);
        serverThread.start();
        this.modelField = createModel();
        this.newModelField = createModel();
        
        
    }
 
    public void stopServer() throws IOException {
    	stopped = true;
        stopConnection();
        if(serverSocket != null)serverSocket.close();
		console.println("Server Closed");
    }

	private void stopConnection() throws IOException {
		connection = false;
		if(in != null)in.close();
		if(out != null)out.close();
		if(clientSocket != null)clientSocket.close();
	}

	@Override
	public void run() {
		while(!stopped) {
			try {
			//Wait for new Connection
			console.println("Wait for Connection..");
			clientSocket = serverSocket.accept();
			console.println("Connection from " + clientSocket.getInetAddress() + ":" + clientSocket.getPort());
			connection = true;
			out = new DataOutputStream(clientSocket.getOutputStream());
			in = new DataInputStream(new BufferedInputStream(clientSocket.getInputStream()));
			if(observed == null) stopConnection();	
			this.modelField = createModel();
			Thread updateThread = new Thread(new UpdateLoop());
			updateThread.start();
			//InputLoop
			try {
				inputLoop();
			}catch(EOFException e){
				console.println("Connection Closed");
			}
			stopConnection();	
			}
			catch(SocketException e){
				//ServerSocket is closed
				stopped = true;
			}
			catch (IOException e) {
				e.printStackTrace();
			}
			connection = false;
		}
	}

	private void inputLoop() throws IOException {
		while (connection) {
			//WaitForInput
			byte inputByte = in.readByte();
			//UpdateHoleg
			if (inputByte == Command.SetAmount) {
				if(observed == null) stopConnection();
				console.println("Res: [" + inputByte + "] -> SetAmount");
				int index = in.readInt();
				int amount = in.readInt();
				modelField.getElements().get(index).amount = amount;
				observed.getElements().get(index).setAmount(amount);
				
			}else if (inputByte == Command.SetEnable) {
				if(observed == null) stopConnection();
				console.println("Res: [" + inputByte + "] -> SetEnable");
				int index = in.readInt();
				boolean enabled = in.readBoolean();
				modelField.getElements().get(index).enabled = enabled;
				observed.getElements().get(index).setActive(enabled);
			}else if (inputByte == Command.IncreaseAmount) {
				if(observed == null) stopConnection();
				console.println("Res: [" + inputByte + "] -> IncreaseAmount");
				int index = in.readInt();
				modelField.getElements().get(index).amount++;
				HolonElement ele = observed.getElements().get(index);
				ele.setAmount(ele.getAmount()+1);
				
			}else if (inputByte == Command.DecreaseAmount) {
				if(observed == null) stopConnection();
				console.println("Res: [" + inputByte + "] -> DecreaseAmount");
				int index = in.readInt();
				modelField.getElements().get(index).amount--;
				HolonElement ele = observed.getElements().get(index);
				ele.setAmount(ele.getAmount()-1);
				
			} else{
				console.println("Res: [" + inputByte + "] -> unknown");
			}
			control.calculateStateAndVisualForCurrentTimeStep();
			control.updateCanvas();
			control.getGui().triggerUpdateController(null);
		}
	}
	
	
	private HolonObjectModel createModel() {
		HolonObjectModel model = new HolonObjectModel();
		int index = 0;
		for(HolonElement ele :observed.getElements()) {
			model.add(ele, index++);
		}
		return model;
	}
	
	
	
	
	
	public class UpdateLoop implements Runnable{
		public UpdateLoop(){
		}
		@Override
		public void run() {
			sendUpdate();
			while(!stopped && connection) {
				if(checkForUpdates()){
					sendUpdate();
				}else {
					waitOneSecond();	
				}
			}
			
		}

		private void waitOneSecond() {
			//wait one second
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
			}
		}

		private boolean checkForUpdates() {
			//TODO Delete And CheckforUpdatesReal
			newModelField = createModel();
			if(compareModels(modelField,newModelField)) {
				return false;
			}
			modelField = newModelField;
			return true;
			
		}
		/**
		 * Returns true if both are the same.
		 * @param modelField
		 * @param newModelField
		 * @return
		 */
		private void sendUpdate() {
			try {
				if(observed == null) stopConnection();
				console.println("Send: [" + Command.Update + "] -> Update");
				out.writeByte(Command.Update);
				out.writeInt(modelField.size());
				for(HolonElementWrapper wrapper : modelField.getElements()) {
					out.writeUTF(wrapper.name);
					out.writeInt(wrapper.amount);
					out.writeFloat(wrapper.energy);
					out.writeBoolean(wrapper.enabled);
				}
				
				
				
			} catch (IOException e) {
				if(connection)console.println(e.getMessage());
			}
		}
	}
	
	private boolean compareModels(HolonObjectModel model, HolonObjectModel newModel) {
		if(model.size() != newModel.size()) {
			return false;
		}
		for(int i = 0; i < model.size(); i++) {
			if(!model.getElements().get(i).equals(newModel.getElements().get(i))) {				
				return false;
			}
		}
		return true;
	}
	public void setObserved(HolonObject observed) {
		this.observed = observed;
	}
	
	public class Command {
		//InputCommands
		public static final int SetAmount = 10;
		public static final int SetEnable = 11;
		public static final int IncreaseAmount = 12;
		public static final int DecreaseAmount = 13;
		//UpdateResiveCommands
		public static final int Update = 20;
	}
	
	
	
	public class HolonObjectModel {
		//Field
		private List<HolonElementWrapper> elements;
		private String holonObjectName;
		
		//constructor
		public HolonObjectModel(){
			elements = new ArrayList<HolonElementWrapper>();
		}
		
		
		public void add(HolonElement ele, int index ) {
			String name = ele.getEleName();
			int amount =ele.getAmount();
			float energy =  ele.getEnergyPerElement();
			boolean enabled = ele.isActive();
			elements.add(new HolonElementWrapper(name, amount, energy, enabled, index));
		}
		
		public int size() {
			return elements.size();
		}

		//Getter/Setter
		public List<HolonElementWrapper> getElements() {
			return elements;
		}
		
		public String getHolonObjectName() {
			return holonObjectName;
		}
		public void setHolonObjectName(String holonObjectName) {
			this.holonObjectName = holonObjectName;
		}

	}
	public class HolonElementWrapper{
		public int index;
		public String name;
		public int amount;
		public float energy;
		public boolean enabled;
		public HolonElementWrapper(){
			
		}
		public HolonElementWrapper(String name, int amount, float energy, boolean enabled, int index){
			this.name = name;
			this.amount = amount;
			this.energy = energy;
			this.enabled = enabled;
		}
		@Override
		public boolean equals(Object obj) {
			if (obj == this) {
				return true;
			}
			if (!(obj instanceof HolonElementWrapper)) {
			   return false;
			}
			HolonElementWrapper element = (HolonElementWrapper) obj;
			
			
			
			return this.name.compareTo(element.name) == 0 &&
				   this.amount == element.amount &&
				   this.energy == element.energy &&
				   this.enabled == element.enabled;
		}
		
	}
	
	
	
	
}
