package controller;

import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.BufferedWriter;
import java.io.FileWriter;

import java.util.Queue;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.LinkedHashMap;

import java.lang.reflect.Method;
import java.lang.reflect.Field;

import com.ms4systems.devs.core.model.impl.AtomicModelImpl;
import com.ms4systems.devs.core.model.impl.CoupledModelImpl;
import com.ms4systems.devs.core.simulation.Simulation;
import com.ms4systems.devs.core.model.AtomicModel;
import com.ms4systems.devs.core.message.Coupling;
import com.ms4systems.devs.core.message.Port;

import events.Connection;
import events.ConnectionRef;
import events.Done;
import events.Event;
import events.NewLink;
import events.NewNode;
import events.RendezVous;

import responses.Events;
import responses.Init;
import responses.TraceStart;

import requests.Request;

import values.Value;

import manager.OperatorManager;

import exceptions.ParseException;

import elements.Addition;
import elements.Operator;
import elements.Removal;
import elements.Reorganization;
import elements.RemovalType;

public class MS4MEController {
	private ServerSocket serverSocket;
	private Socket clientSocket;

	private Queue<Event> events = new LinkedList<Event>();
	private LinkedHashMap<String, String> models;

	private BufferedWriter logger;
	private BufferedWriter loggerOp;

	private ObjectOutputStream oOut;
	private ObjectInputStream oIn;

	private AtomicModelImpl controller;

	private OperatorManager operatorManager;

	private int traceNumber;
	private int currentStep;

	public MS4MEController(AtomicModelImpl controller){
		super();
		this.controller = controller;
		this.models = new LinkedHashMap<String, String>();
		this.traceNumber = 0;
		this.currentStep = 0;
		try {
			this.logger = new BufferedWriter(new FileWriter(getModelsDirectory().getAbsolutePath() + "/txt/trace.txt"));
			this.loggerOp = new BufferedWriter(
					new FileWriter(getModelsDirectory().getAbsolutePath() + "/txt/executedOperators.txt"));
		} catch (Exception e) {
		}

		File f = getDataFile("operators.txt");
		if (f.exists()) {
			try {
				this.operatorManager = new OperatorManager(f.getAbsolutePath());
			} catch (ParseException e) {
				try {
					loggerOp.write(e.getMessage() + "\n");
					loggerOp.flush();
				} catch (Exception e2) {
				}
			}
		}
	}

	public void start() {
		new Thread(new Runnable() {
			public void run() {
				MS4MEController.this.run();
			}
		}).start();
	}

	private void run() {
		Request request = null;
		try {
			open();
			while (true) {
				clientSocket = serverSocket.accept();
				System.out.println("Connection established");
				oOut = new ObjectOutputStream(clientSocket.getOutputStream());
				oIn = new ObjectInputStream(clientSocket.getInputStream());
				while (true) {
					try {
						request = (Request) oIn.readObject();

						switch (request) {
						case END:
							close();
							return;
						case INIT:
							init();
							oOut.writeObject(new Init());
							break;
						case NEW_STATE:
							oOut.writeObject(simulationEvents());
							this.currentStep++;
							break;
						case NEW_TRACE:
							this.currentStep = 0;
							models = new LinkedHashMap<String, String>();
							newTrace();
							executeOperators();
							oOut.writeObject(new TraceStart());
							traceNumber++;
							break;
						}
					} catch (EOFException e2) {
						break;
					} catch (ClassNotFoundException e) {
						System.out.println("Unexpected message from the simulator.");
					}
				}
				System.out.println("Connection disconnected");
				close();
				return;
			}

		} catch (IOException e) {
			close();
		}
	}

	private Events simulationEvents() throws IOException {
		List<Event> le = new ArrayList<Event>();
		Events ev = new Events(le);
		Simulation s = controller.getParent().getCoordinator().getSimulation().getRootSimulator().getSimulation();
		if (this.currentStep != 0) {
			while (events.isEmpty()) {
				try {
					s.simulateIterations(1);
				} catch (ConcurrentModificationException e) {
				}
			}
		}
		while (!events.isEmpty()) {
			if (events.peek() == null) {
				events.poll();
			} else {
				try {
					logger.write(events.peek().toString() + "\n");
					logger.flush();
				} catch (Exception u) {
				}
				le.add(events.poll());
			}
		}
		return ev;
	}

	private void newTrace() {
		Simulation s = controller.getParent().getCoordinator().getSimulation().getRootSimulator().getSimulation();
		events.clear();
		s.restartSimulation(0);

		for (Coupling e : controller.getParent().getCouplings()) {
			Port<?> a, b;
			a = e.getSourcePort();
			b = e.getDestinationPort();
			this.newLink(a, b);
		}
	}

	private void init() {
		CoupledModelImpl p = (CoupledModelImpl) controller.getParent();
		while (p.getChildren().size() > 0) {
			p.removeChildModel(p.getChildren().get(0));
		}
		while (p.getCouplings().size() > 0) {
			p.removeCoupling(p.getCouplings().get(0));
		}
		try {
			Method m = p.getClass().getMethod("make");
			m.invoke(p);
		} catch (Exception ex) {
		}
	}

	private void executeOperators() {
		AtomicModel drc = null;
		if (operatorManager == null) {
			try {
				loggerOp.write(controller.getClass().getResource(".").toURI().toString());
				loggerOp.flush();
				return;
			} catch (IOException | URISyntaxException e1) {
			}
		}
		List<Operator> l = operatorManager.getOperator(traceNumber);
		if (l.size() == 0) {
			return;
		}
		for (AtomicModel e : controller.getParent().getChildren()) {
			if (e.getName().equals("DRC" + controller.getParent().getName())) {
				drc = e;
				break;
			}
		}

		for (Operator o : l) {
			if (o instanceof Addition) {
				Addition a = (Addition) o;
				Method m = null;
				try {
					m = drc.getClass().getMethod(toFirstLower(a.getName()) + "AdditionOperator");
					m.invoke(drc);
					loggerOp.write("Addition Operator executed in " + a.getName() + "\n");
					loggerOp.flush();
				} catch (Exception ex) {
				}
			} else if (o instanceof Removal) {
				Removal r = (Removal) o;
				AtomicModelImpl am = null;
				int countType = 0;
				for (AtomicModel e : controller.getParent().getChildren()) {
					if (r.getType() == RemovalType.ConstituentType) {
						Field f = null;
						try {
							f = e.getClass().getField("typeCons");
							String name = (String) f.get(e);
							if (name.equalsIgnoreCase(r.getName())) {
								am = (AtomicModelImpl) e;
								for (Entry<String, String> entry : models.entrySet()) {
									if (entry.getValue().equals(name))
										countType++;
								}
								break;
							}
						} catch (Exception e1) {
							e1.printStackTrace();
						}
					} else {
						if (e.getName().equalsIgnoreCase(r.getName())) {
							am = (AtomicModelImpl) e;
							String name = models.get(e.getName());
							for (Entry<String, String> entry : models.entrySet()) {
								if (entry.getValue().equals(name))
									countType++;
							}
							break;
						}
					}
				}
				if (am != null) {
					Method m = null;
					try {
						if (countType > 1) {
							m = drc.getClass().getMethod("deletionOperator", AtomicModelImpl.class);
							m.invoke(drc, am);
							loggerOp.write("Deletion Operator executed in " + am.getName() + "." + "\n");
							loggerOp.flush();
						} else {
							loggerOp.write("Deletion Operator not executed!" + "because dont have "
									+ models.get(am.getName()) + " enough." + "\n");
							loggerOp.flush();
						}
					} catch (Exception e1) {
						try {
							loggerOp.write("Deletion Operator not executed " + e1.getMessage() + "\n");
							loggerOp.flush();
						} catch (Exception e2) {
						}
						e1.printStackTrace();
					}
				}
			} else if (o instanceof Reorganization) {
				Method m = null;
				try {
					m = drc.getClass().getMethod("reorganizationOperator");
					m.invoke(drc);
					loggerOp.write("Reorganization Operator executed\n");
					loggerOp.flush();
				} catch (Exception e1) {
					e1.printStackTrace();
				}
			}
		}
	}

	public void open() throws IOException {
		serverSocket = new ServerSocket(4242);
		System.out.println("Server started!");
	}

	public void close() {
		try {
			serverSocket.close();
		} catch (IOException e) {
		}
		System.out.println("Server closed");
	}

	public void newNode(String name, String type, List<Connection> l) {
		models.put(name, type);
		events.add(new NewNode(currentStep, name, type, l));
	}

	public void newLink(Port<?> src, Port<?> dest) {
		if (!src.getModel().getName().contains("DRC")
				&& !src.getModel().getName().equals(controller.getParent().getName())
				&& !dest.getModel().getName().contains("DRC")
				&& !dest.getModel().getName().equals(controller.getParent().getName())
				&& !src.getName().contains("Checked") && !src.getName().contains("WriteData")
				&& !dest.getName().contains("Checked") && !dest.getName().contains("WriteData")) {

			if (this.models.containsKey(src.getModel().getName()) && models.containsKey(dest.getModel().getName())) {
				this.events.add(new NewLink(currentStep, new ConnectionRef(src.getModel().getName(), src.getName()),
						new ConnectionRef(dest.getModel().getName(), dest.getName())));
			}
		}
	}

	public void rendezvous(Port<?> src, Port<?> dest, Value value) {
		if (!src.getModel().getName().contains("DRC")
				&& !src.getModel().getName().equals(controller.getParent().getName())
				&& !dest.getModel().getName().contains("DRC")
				&& !dest.getModel().getName().equals(controller.getParent().getName())
				&& !src.getName().contains("Checked") && !src.getName().contains("WriteData")
				&& !dest.getName().contains("Checked") && !dest.getName().contains("WriteData")) {

			if (models.containsKey(src.getModel().getName()) && models.containsKey(dest.getModel().getName())) {
				events.add(new RendezVous(currentStep, new ConnectionRef(src.getModel().getName(), src.getName()),
						new ConnectionRef(dest.getModel().getName(), dest.getName()), value));
			}
		}
	}

	public void finalizeConstituent(AtomicModelImpl a) {
		if (models.containsKey(a.getName())) {
			Done d2 = new Done(currentStep, a.getName());
			events.add(d2);
			models.remove(a.getName());
		}
	}

	public int currentStep() {
		return currentStep;
	}

	public void setController(AtomicModelImpl controller) {
		this.controller = controller;
	}

	private File getModelsDirectory() {
		URI dirUri;
		File dir;
		try {
			dirUri = controller.getClass().getResource(".").toURI();
			dir = new File(dirUri);
		} catch (URISyntaxException e) {
			e.printStackTrace();
			throw new RuntimeException("Could not find Models directory. Invalid model URL: "
					+ controller.getClass().getResource(".").toString());
		}
		while (dir != null && dir.getParentFile() != null) {
			if (dir.getName().equalsIgnoreCase("bin")) {
				return new File(dir.getParentFile(), "src/Models");
			}
			dir = dir.getParentFile();
		}
		return null;
	}

	private File getDataFile(String fileName) {
		File modelDir = getModelsDirectory();
		File dir = new File(modelDir, "txt");
		File dataFile = new File(dir, fileName);
		return dataFile;
	}

	private String toFirstLower(String a) {
		String r = "";
		if (a.equals("")) {
			return "";
		}
		r = a.substring(0, 1).toLowerCase();
		if (a.length() > 1) {
			r += a.substring(1);
		}
		return r;
	}
}