package de.tu_darmstadt.tk.SmartHomeNetworkSim.control;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.LinkedList;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.Link;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.Model;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.Connection;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.ProbabilityDistributionHandler;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.Protocol;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.SmartDevice;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.configuration.ImportConfiguration;

/**
 * Controller which manages Imported Classes of the framework
 *
 * @author Andreas T. Meyer-Berg
 */
public class ImportController {
	Model model;
	Controller controller;
	ImportConfiguration importConf;

	/**
	 * Creates a new ImportController
	 * 
	 * @param model
	 *            model to be edited
	 * @param controller
	 *            parent controller
	 */
	public ImportController(Model model, Controller controller) {
		this.model = model;
		this.controller = controller;
		this.importConf = controller.getSettingsController().getConfigurationManager().getImportConfiguration();
	}

	/**
	 * Adds new Link to the model
	 * 
	 * @param Link
	 *            Link to be added
	 * @return true if it was added
	 */
	public boolean addLink(Class<? extends Link> Link) {
		if (isValidLink(Link))
			importConf.addLinkClass(Link);
		else
			return false;
		return true;

	}

	/**
	 * Removes Link from the model
	 * 
	 * @param Link
	 *            Link to be removed
	 */
	public void removeLink(Class<? extends Link> Link) {
		importConf.removeLinkClass(Link);
	}
	
	/**
	 * Returns the available Links of the model
	 * 
	 * @return available links
	 */
	public LinkedList<Class<? extends Link>> getLinks() {
		return importConf.getLinkClasses();
	}
	
	/**
	 * Returns true if it is a Valid Link, false if not
	 * 
	 * @param link
	 *            Link to be checked
	 * @return true if it is a valid Link
	 */
	public boolean isValidLink(Class<? extends Link> link) {
		try {
			/**
			 * Link to be tested
			 */
			Link l = link.newInstance();
			// Empty constructor required, to create new instance
			if (l == null)
				throw new Exception("Link required an empty constructor");
			// Name shall not be null or empty string
			if (l.getName() == null || l.getName() == "")
				throw new Exception(
						"Link name shall not be null or empty string.");
		} catch (Exception e) {
			return false;
		}
		return true;
	}
	
	/**
	 * Adds new Protocol to the model
	 * 
	 * @param protocol
	 *            protocol to be added
	 * @return true if it was added
	 */
	public boolean addProtocol(Class<? extends Protocol> protocol) {
		if (isValidProtocol(protocol))
			importConf.addProtocolClass(protocol);
		else
			return false;
		return true;
		
	}
	
	/**
	 * Removes protocol from the model
	 * 
	 * @param protocol
	 *            protocol to be removed
	 */
	public void removeProtocol(Class<? extends Protocol> protocol) {
		importConf.removeProtocolClass(protocol);
	}
	
	/**
	 * Returns the available Protocols of the model
	 * 
	 * @return available protocols
	 */
	public LinkedList<Class<? extends Protocol>> getProtocols() {
		return importConf.getProtocolClasses();
	}
	
	/**
	 * Returns true if it is a Valid Protocol, false if not
	 * 
	 * @param protocol
	 *            protocol to be checked
	 * @return true if it is a valid protocol
	 */
	public boolean isValidProtocol(Class<? extends Protocol> protocol) {
		try {
			/**
			 * Protocol to be tested
			 */
			Protocol p = protocol.newInstance();
			// Empty constructor required, to create new instance
			if (p == null)
				throw new Exception("Protocol required an empty constructor");
			// Name shall not be null or empty string
			if (p.getName() == null || p.getName() == "")
				throw new Exception(
						"Protocol name shall not be null or empty string.");
			if (p.getRoles() == null || p.getRoles().length == 0)
				throw new Exception("Roles shall not be null or empty");
			// Number of roles have to match
			if (p.getNumberOfRoles() != p.getRoles().length)
				throw new Exception(
						"getNumberOfRoles() does not match getRoles().length");
		} catch (Exception e) {
			return false;
		}
		return true;
	}

	/**
	 * Adds new Connection to the model
	 * 
	 * @param connection
	 *            Connection to be added
	 * @return true if it was added
	 */
	public boolean addConnection(Class<? extends Connection> connection) {
		if (isValidConnection(connection))
			importConf.addConnectionClass(connection);
		else
			return false;
		return true;
		
	}
	
	/**
	 * Removes Connection from the model
	 * 
	 * @param connection
	 *            Connection to be removed
	 */
	public void removeConnection(Class<? extends Connection> connection) {
		importConf.removeConnectionClass(connection);
	}
	
	/**
	 * Returns the available Connections of the model
	 * 
	 * @return available Connections
	 */
	public LinkedList<Class<? extends Connection>> getConnections() {
		return importConf.getConnectionClasses();
	}
	
	/**
	 * Returns true if it is a Valid Connection, false if not
	 * 
	 * @param connection
	 *            Connection to be checked
	 * @return true if it is a valid Connection
	 */
	public boolean isValidConnection(Class<? extends Connection> connection) {
		try {
			/**
			 * Connection to be tested
			 */
			Connection p = connection.newInstance();
			// Empty constructor required, to create new instance
			if (p == null)
				throw new Exception("Connection required an empty constructor");
			// Name shall not be null or empty string
			if (p.getName() == null || p.getName() == "")
				throw new Exception(
						"Connection name shall not be null or empty string.");
		} catch (Exception e) {
			return false;
		}
		return true;
	}
	
	/**
	 * Adds new SmartDevice to the model
	 * 
	 * @param smartDevice
	 *            SmartDevice to be added
	 * @return true if it was added
	 */
	public boolean addSmartDevice(Class<? extends SmartDevice> smartDevice) {
		if (isValidSmartDevice(smartDevice))
			importConf.addSmartDeviceClass(smartDevice);
		else
			return false;
		return true;
		
	}
	
	/**
	 * Removes SmartDevice from the model
	 * 
	 * @param smartDevice
	 *            SmartDevice to be removed
	 */
	public void removeSmartDevice(Class<? extends SmartDevice> smartDevice) {
		importConf.removeSmartDeviceClass(smartDevice);
	}
	
	/**
	 * Returns the available SmartDevices of the model
	 * 
	 * @return available SmartDevices
	 */
	public LinkedList<Class<? extends SmartDevice>> getSmartDevices() {
		return importConf.getSmartDeviceClasses();
	}
	
	/**
	 * Returns true if it is a Valid SmartDevice, false if not
	 * 
	 * @param smartDevice
	 *            SmartDevice to be checked
	 * @return true if it is a valid SmartDevice
	 */
	public boolean isValidSmartDevice(Class<? extends SmartDevice> smartDevice) {
		try {
			/**
			 * SmartDevice to be tested
			 */
			SmartDevice p = smartDevice.newInstance();
			// Empty constructor required, to create new instance
			if (p == null)
				throw new Exception("SmartDevice required an empty constructor");
			// Name shall not be null or empty string
			if (p.getName() == null || p.getName() == "")
				throw new Exception(
						"SmartDevice name shall not be null or empty string.");
		} catch (Exception e) {
			return false;
		}
		return true;
	}
	
	/**
	 * Adds new DistributionHandler to the model
	 * 
	 * @param distributionHandler DistributionHandler to be added
	 * @return true if it was added
	 */
	public boolean addDistributionHandler(Class<? extends ProbabilityDistributionHandler> distributionHandler) {
		if (isValidDistributionHandler(distributionHandler))
			importConf.addDistributionHandlerClass(distributionHandler);
		else
			return false;
		return true;
		
	}
	
	/**
	 * Removes DistributionHandler from the model
	 * 
	 * @param distributionHandler
	 *            DistributionHandler to be removed
	 */
	public void removeDistributionHandler(Class<? extends ProbabilityDistributionHandler> distributionHandler) {
		importConf.removeDistributionHandlerClass(distributionHandler);
	}
	
	/**
	 * Returns the available DistributionHandler of the model
	 * 
	 * @return available DistributionHandler
	 */
	public LinkedList<Class<? extends ProbabilityDistributionHandler>> getDistributionHandlers() {
		return importConf.getDistributionHandlerClasses();
	}
	
	/**
	 * Returns true if it is a Valid DistributionHandler, false if not
	 * 
	 * @param distributionHandler
	 *            DistributionHandler to be checked
	 * @return true if it is a valid DistributionHandler
	 */
	public boolean isValidDistributionHandler(Class<? extends ProbabilityDistributionHandler> distributionHandler) {
		try {
			/**
			 * SmartDevice to be tested
			 */
			ProbabilityDistributionHandler p = distributionHandler.newInstance();
			// Empty constructor required, to create new instance
			if (p == null)
				throw new Exception("DistributionHandler required an empty constructor");
			// Name shall not be null or empty string
			if (p.getSimpleDescription() == null || p.getSimpleDescription() == "")
				throw new Exception(
						"DistributionHandler name shall not be null or empty string.");
		} catch (Exception e) {
			return false;
		}
		return true;
	}
	
	/**
	 * Imports the given .java File, compiles it and returns the compiled class
	 * 
	 * @param javaFile File(path)
	 * @return Class which was compiled
	 * @throws ClassImportException if an problem occurred during import
	 */
	public static Class<?> importJavaClass(File javaFile) throws ClassImportException{
		/**
		 * Package name
		 */
		String packageName = getJavaPackageName(javaFile);
		if (packageName == null) {
			// if package null - try default package
			packageName = "";
		}
		
		/**
		 * Name of the Class. File name "ClassName.java"
		 */
		String className = javaFile.getName().substring(0,
				javaFile.getName().lastIndexOf('.'));
		
		/**
		 * Compiler, to compile the File
		 */
		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		compiler.run(null, null, null, javaFile.getPath());
		
		/**
		 * Root File of the imported Protocol (if the Java File is inside a package)
		 */
		File root = javaFile.getParentFile();
		if(!packageName.isEmpty()){
			/**
			 * Array of all parent package names e.g. "pack1.pack2.pack3" -> {"pack1","pack2","pack3"}
			 */
			String[] packageFolders = packageName.split("\\.");
			if(packageFolders.length<2){
				//A single level
				if(root.getName().compareTo(packageName)!=0){
					throw new ClassImportException("Invalid ParentFolderName: Expected \""+packageName+"\" but was \""+root.getName()+"\"");
				}
				root = root.getParentFile();
			}else{
				for(int i = packageFolders.length-1;i>=0;i--){
					if(root.getName().compareTo(packageFolders[i])!=0){
						throw new ClassImportException("Invalid ParentFolderName at level "+i+": Expected \""+packageFolders[i]+"\" but was \""+root.getName()+"\"");
					}
					root = root.getParentFile();					
				}
			}
		}
		
		/**
		 * ClassLoader to load the compiled class, given it's root
		 */
		URLClassLoader classLoader;
		try {
			classLoader = URLClassLoader
					.newInstance(new URL[] { root.toURI().toURL() });
		} catch (MalformedURLException e1) {
			throw new ClassImportException("Invalid URL/File at the root of the imported Class");
		}

		// If packageName is not empty, and dot is required: e.g. "packagePath.ClassName"
		if (!packageName.isEmpty())
			packageName += ".";
		
		/**
		 * Imported Class
		 */
		Class<?> cls = null;
		try{
			cls = Class.forName(packageName + className, true, classLoader);
		}catch(ClassNotFoundException e){
			try{
				//Second try to load class
				cls = classLoader.loadClass(packageName + className);
			}catch(ClassNotFoundException e2){
				throw new ClassImportException("Class not found: "+packageName+className);
			}
		}
		return cls;
	}
	
	/**
	 * Returns the package path of a given Java File. Returns null if the
	 * exception occurred and returns an empty string if no package declaration
	 * was found. Package declaration should be in a single line.
	 * 
	 * @param javaFile
	 *            File to be searched for the package path
	 * @return PackagePath of the JavaFile, EmptyString, if no declaration was
	 *         found, null on errors
	 */
	public static String getJavaPackageName(File javaFile) {
		/**
		 * If javaFile null / non existent -> return null
		 */
		if(javaFile==null||!javaFile.exists())
			return null;
		
		/**
		 * Package name
		 */
		String packageName = "";
		
		/**
		 * Reader to read the java File
		 */
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new FileReader(javaFile.getPath()));
			/**
			 * Current Line which is investigated
			 */
			String currentLine = reader.readLine();

			// Search each line until a valid package is found
			while (currentLine != null) {
				currentLine = currentLine.trim();
				if (!currentLine.isEmpty()) {
					//Check if line begins with package
					if (currentLine.length() >= 7 && currentLine.startsWith("package")) {
						packageName = currentLine.substring(8, currentLine.length() - 1);
					}
				}
				if (packageName.isEmpty()) {
					currentLine = reader.readLine();
				} else {
					currentLine = null;
				}
			}
		} catch (Exception e) {
			return null;
		} finally {
			try {
				if(reader != null)
					reader.close();
			} catch (IOException e) {
			}
		}
		// Remove whitespace in cases like "import   test.package  ;"
		packageName = packageName.trim();
		return packageName;
	}

	
}
