package org.inria.bmajwatcher.server;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;

import org.apache.log4j.Logger;
import org.inria.biomaj.singleton.BiomajInformation;
import org.inria.biomaj.utils.BiomajException;
import org.inria.bmajwatcher.client.services.BankEditingService;
import org.inria.bmajwatcher.server.data.RemoteBankRetriever;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * Servlet that retrieves and updates the banks properties.
 * 
 * @author rsabas
 *
 */
public class BankEditingServiceImpl extends RemoteServiceServlet implements BankEditingService {

	private static final long serialVersionUID = -1432322262693308493L;

	private Properties properties = new Properties();
	private Properties categorizedProps = new Properties();
	private String serverPath;
	private RemoteBankRetriever remoteRetriever = new RemoteBankRetriever();
	public static final String TYPE_LOCAL = "local";
	public static final String TYPE_REMOTE = "remote";
	public static final String TYPE_EMPTY = "empty";
	
	public static final String CREATE_NEW_INCLUDE = "include/Create new include file...";
	
	public static final String PROP_SEP = "__";
	
	private int inc = 1;
	
	private static Logger log = Logger.getLogger(BankEditingServiceImpl.class);
	
	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		
		serverPath = config.getServletContext().getRealPath("/");
		File fProps = new File(serverPath + "/bankProperties.properties");
		File fCat = new File(serverPath + "/propertyList.properties");
		try {
			properties.load(new FileReader(fProps));
			categorizedProps.load(new FileReader(fCat));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public Collection<String> getRemoteBankList(String url) {
		return remoteRetriever.getRemoteBankList(url);
	}
	
	@Override
	public Map<String, String> getBankProperties(String login, String bankName, String type, Collection<String> included) {
		
		Map<String, String> res = new HashMap<String, String>();
		if (type.equals(TYPE_LOCAL)) {
			return getBankProperties(login, bankName, included);
		} else if (type.equals(TYPE_REMOTE)) {
			Properties bankOnlyProperties = remoteRetriever.getBankOnlyProperties(bankName);
			Properties allProps = remoteRetriever.getAllBankProperties(bankName);
			if (bankOnlyProperties != null) {
				fixProcesses(bankOnlyProperties);
				fixProcesses(allProps);
				for (Object key : allProps.keySet()) {
					res.put(prefixProperty(key.toString(), bankOnlyProperties), allProps.get(key).toString());
				}
			}
			return res;
		}
		
		return null;
	}
	
	@Override
	public Collection<String> getIncludePropertiesList(String user) {
		List<String> users = null;
		if (user.equals(UserManagerServiceImpl.adminLogin()))
			users = UserManagerServiceImpl.getUserList();
		else
			users = UserManagerServiceImpl.getGroupUserFromLogin(user);
		
		Set<String> res = new TreeSet<String>();
		try {
			String root = BiomajInformation.getInstance().getProperty(BiomajInformation.WORKFLOWSDIR);
			for (String u : users) {
				File dir = new File(root + "/" + u + "/include");
				if (!dir.exists() || !dir.isDirectory()) {
					dir.mkdir();
				}
				for (File file : dir.listFiles()) {
					if (file.getName().endsWith(".properties") && !file.isDirectory()) {
						res.add(file.getName());
					}
				}
			}
		} catch (BiomajException e) {
			log.error(e);
		}
		
		return res;
	}

	@Override
	public Collection<String> getBankList(String login) {
		
		List<String> users = null;
		Set<String> res = new TreeSet<String>();
		if (UserManagerServiceImpl.adminLogin().equals(login)) {
			users = UserManagerServiceImpl.getUserList();
			res.add("global");
		}
		else
			users = UserManagerServiceImpl.getGroupUserFromLogin(login);
		
		try {
			String root = new File(BiomajInformation.getInstance().getProperty(BiomajInformation.WORKFLOWSDIR)).getAbsolutePath();
			for (String user : users) {
				File dir = new File(root + "/" + user);
				if (dir.exists()) {
					for (File file : dir.listFiles()) {
						if (file.getName().endsWith(".properties") && !file.isDirectory()) {
							res.add(file.getName().substring(0, file.getName().lastIndexOf('.')));
						}
					}
				} else {
					dir.mkdir();
				}
			}
			return res;
			
		} catch (BiomajException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public Map<String, Map<String, String>> getIncludedProperties(String login, String bankName, Collection<String> included) {
		Map<String, Map<String,String>> res = new HashMap<String, Map<String,String>>();
		if (!bankName.equals("global")) {
			res.put("global.properties", getGlobalPropertiesAsPrefixedMap());
			Map<String, String> props = getFileProperties(login, bankName + ".properties", null);
			if (props != null && props.containsKey("misc__include.properties")) {
				String[] includedProps = props.get("misc__include.properties").split(",");
				for (String file : includedProps) {
					if (!file.trim().isEmpty()) {
						String nameWithoutUser = file.substring(file.indexOf('/') + 1);
						res.put(nameWithoutUser, getFileProperties(login, nameWithoutUser, null));
					}
				}
			}
			for (String name : included) {
				if (name.equals(CREATE_NEW_INCLUDE)) {
					String test = "include/include_file" + inc++ + ".properties";
					while (propertiesExist(login, test))
						test = "include/include_file" + inc++ + ".properties";
//					res.put(test.substring(test.indexOf('/') + 1), new HashMap<String, String>());
					res.put(test, new HashMap<String, String>());
				} else if (!propertiesExist(login, name)) // New include file
					res.put(name, new HashMap<String, String>());
				else
					res.put(name, getFileProperties(login, name, null));
			}
		}
		return res;
	}
	
	@Override
	public int save(String login, String type, String bankName, String newName, Map<String, String> newProps, boolean runUpdate) {
		
		Properties finalProps = new Properties();
		
		String userRoot = canEditProperties(login, newName);
		if (userRoot == null) // Try another name
			return 0;
		
		
		if (type.equals(TYPE_LOCAL)) {
			finalProps = getBankOnlyProperties(login, bankName);
			Map<String, String> tmp = null;
			if (UserManagerServiceImpl.adminLogin().equals(login) && bankName.endsWith("global"))
				tmp = getGlobalPropertiesAsPrefixedMap();
			else
				tmp = getFileProperties(login, bankName + ".properties", null);
			
			/*
			 * Remove previous processes.
			 */
			for (String key : tmp.keySet()) {
				String[] split = key.split("__");
				if (split[0].equals("misc") && !split[1].equals("include.properties")
						&& !split[1].equals("database.login")
						&& !split[1].equals("database.password")
						&& !split[1].equals("database.driver")
						&& !split[1].equals("database.url")
						&& !split[1].equals("database.type")) {
					finalProps.remove(split[1]);
				}
			}
		}
		
		if (newProps != null) {
			
			for (String key : newProps.keySet()) {
				
				String strippedKey = key.split(PROP_SEP)[1];
				
				String value = newProps.get(key);
				if (value == null)
					value = "";
				
				/*
				 * Handle PRE/REMOVE_PROCESS.db.pre.process special case.
				 * Definition of pre and remove processes cannot be done via BLOCKS.
				 */
				if (strippedKey.equals("BLOCKS") && (newProps.get(key).equals("PRE_PROCESS") || newProps.get(key).equals("REMOVE_PROCESS")))
					continue;
				
				if (strippedKey.contains("db.pre.process"))
					strippedKey = "db.pre.process";
				else if (strippedKey.contains("db.remove.process"))
					strippedKey = "db.remove.process";
				else if (strippedKey.equals("include.properties") && !value.equals("__deleted")) {
					String[] includes = value.split(",");
					String newIncl = "";
					for (String incl : includes) {
						String path = canEditProperties(login, incl);
						if (path != null) {
							newIncl += path.substring(getConfDir().length() + 1) + "/" + incl + ",";
						}
					}
					value = newIncl.substring(0, newIncl.length() - 1);
				}
				
				if (value.equals("__deleted"))
					finalProps.remove(strippedKey);
				else {
					finalProps.put(strippedKey, value);
				}
			}
		}
		
		try {
			
			String fileName = userRoot + "/" + newName + ".properties";
			
			PrintWriter pw = new PrintWriter(new File(fileName));
			finalProps.store(pw, "Generated by BmajWatcher");
			pw.close();
			
			if (runUpdate && !newName.equals("global"))
				new BiomajLauncherServiceImpl().startUpdate(newName);
			
			return 1;
		} catch (FileNotFoundException e) {
			log.error(e);
		} catch (IOException e) {
			log.error(e);
		}
		
		return -1;
	}
	
	
	/**
	 * Gets a bank's properties with no inheritance.
	 * 
	 * @param login identifies directories to look into
	 * @param name file name
	 * @param bankProperties don't remember
	 * 
	 * @return
	 */
	private Map<String, String> getFileProperties(String login, String name, Properties bankProperties) {

		Map<String, String> res = new HashMap<String, String>();
		File bank = getFileForUserGroup(login, name);
		if (bank != null) {
			Properties props = new Properties();
			try {
				FileReader fr = new FileReader(bank);
				props.load(fr);
				fr.close();
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
			for (Object o : props.keySet()) {
				res.put(prefixProperty(o.toString(), bankProperties), props.getProperty(o.toString()));
			}
		}
		
		return res;
	}
	
	/**
	 * If exists, returns file corresponding to given file name for the given
	 * user.
	 * 
	 * @param login
	 * @param fileName
	 * @return null 
	 */
	private File getFileForUserGroup(String login, String fileName) {
		List<String> users = null;
		if (login.equals(UserManagerServiceImpl.adminLogin()))
			users = UserManagerServiceImpl.getUserList();
		else
			users = UserManagerServiceImpl.getGroupUserFromLogin(login);
		
		String newName = fileName.endsWith(".properties") ? fileName : fileName + ".properties";
		String root = getConfDir();
		File bank = null;
		for (String user : users) {
			if ((bank = new File(root + "/" + user + "/" + newName)).exists())
				return bank;
		}
		
		return null;
	}
	
	private Map<String, String> getGlobalPropertiesAsPrefixedMap() {
		Map<String, String> res = new HashMap<String, String>();
		
		File global = new File(getConfDir() + "/global.properties");
		if (global.exists()) {
			Properties props = new Properties();
			try {
				FileReader fr = new FileReader(global);
				props.load(fr);
				fr.close();
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
			for (Object o : props.keySet()) {
				res.put(prefixProperty(o.toString(), null), props.getProperty(o.toString()));
			}
		}
		
		return res;
	}
	
	public static Properties getGlobalProperties() {
		Properties global = new Properties();
		try {
			FileReader fr = new FileReader(getConfDir() + "/global.properties");
			global.load(fr);
			fr.close();
		} catch (FileNotFoundException e) {
			log.error(e);
		} catch (IOException e) {
			log.error(e);
		}
		
		return global;
	}
	
	/**
	 * Prefixes a property by its category as defined in bankProperties.properties
	 * and by its inheritance.
	 * 
	 * @param prop
	 * @param bankProperties bank only properties
	 * @return
	 */
	private String prefixProperty(String prop, Properties bankProperties) {
		String res = "";
		
		if (bankProperties != null && !bankProperties.containsKey(prop))
			res += "inherited:";

		if (properties.containsKey(prop))
			res += properties.getProperty(prop) + PROP_SEP + prop;
		else
			res += "misc__" + prop;
		
		return res;
	}
	
	
	private Properties getBankOnlyProperties(String login, String bankName) {
		String newBankName = bankName.endsWith(".properties") ? bankName : bankName + ".properties";
		
		if (newBankName.equals("global.properties") && login.equals(UserManagerServiceImpl.adminLogin()))
			return getGlobalProperties();
		
		Properties bankOnlyProperties = new Properties();
		
		String parent = canEditProperties(login, newBankName);
		
		if (parent != null) {
			File file = new File(parent + "/" + newBankName);
			try {
				if (file.exists()) {
					FileReader fr = new FileReader(file);
					bankOnlyProperties.load(fr);
					fr.close();
				}
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		return bankOnlyProperties;
	}
	
	/**
	 * Modifies pre/post processing properties according to these rules :
	 * - Pre-processes cannot be declared via BLOCKS (only db.pre.process)
	 * - If BLOCKS property is present, any db.post.process property will not be taken
	 * into account.
	 * 
	 * @param props
	 */
	private void fixProcesses(Properties props) {
		
		if (!props.containsKey("BLOCKS")) {
			if (props.containsKey("db.post.process")) {
				if (props.getProperty("db.post.process").trim().isEmpty())
					props.remove("db.post.process");
				else {
					props.put("BLOCKS", "B1");
					String metas = props.getProperty("db.post.process");
					props.remove("db.post.process");
					props.put("B1.db.post.process", metas);
				}
			}
		}
		String pre = props.getProperty("db.pre.process");
		if (pre != null && pre.trim().isEmpty())
			props.remove("db.pre.process");
		String remove = props.getProperty("db.remove.process");
		if (remove != null && remove.trim().isEmpty())
			props.remove("db.remove.process");
	}
	

	@Override
	public Map<String,Map<String, String>> getEmptyBank(String pattern) {
		
		int ref = 0;
		if (pattern.equalsIgnoreCase("common"))
			ref = 1;
		else if (pattern.equalsIgnoreCase("exhaustive"))
			ref = 2;
		
		Map<String, String> emptyBank = new HashMap<String, String>();
		Properties global = getGlobalProperties();
		
		for (Object key : categorizedProps.keySet()) {
			String keyString = key.toString();
			int category = Integer.valueOf(categorizedProps.getProperty(keyString));
			String value = "";
			if (category <= ref) {
				if ((value = global.getProperty(keyString)) != null)
					emptyBank.put("inherited:" + prefixProperty(keyString, null), value);
				else
					emptyBank.put(prefixProperty(keyString, null), "");
			} else if ((value = global.getProperty(keyString)) != null) {
				emptyBank.put("inherited:" + prefixProperty(keyString, null), value);
			}
		}
		
		Map<String, Map<String, String>> res = new HashMap<String, Map<String,String>>();
		res.put("global.properties", getGlobalPropertiesAsPrefixedMap());
		res.put(pattern + ".properties", emptyBank);
		
		return res;
	}

	@Override
	public Map<String, String> getPropertyHelp() {
		Map<String, String> res = new HashMap<String, String>();
		
		try {
			Properties propertyHelp = new Properties();
			propertyHelp.load(new FileReader(new File(serverPath + "/propertyHelp.properties")));
			for (Object key : propertyHelp.keySet()) {
				String type = properties.getProperty(key.toString());
				if (type.contains(">"))
					type = type.substring(0, type.indexOf('>'));
				
				res.put(key.toString(), type + ":" + propertyHelp.getProperty(key.toString()));
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		return res;
	}
	
	@Override
	public Collection<String> getMandatoryProperties() {
		Set<String> res = new TreeSet<String>();
		for (Object key : categorizedProps.keySet()) {
			String value = categorizedProps.getProperty(key.toString());
			if (Integer.valueOf(value) == 0) {
				res.add(key.toString());
			}
		}
		return res;
	}
	
	@Override
	public String getPrefixedKey(String key) {
		return prefixProperty(key, null);
	}
	
	/**
	 * Return all the properties for a given bank, including properties
	 * inherited from parent files.
	 * 
	 * @param bankName file name
	 * @param included name of the included properties files
	 * @return
	 */
	private Map<String, String> getBankProperties(String login, String bankName, Collection<String> included) {
		Map<String, String> res = new HashMap<String, String>();
		Properties bankProps = getBankOnlyProperties(login, bankName);
		fixProcesses(bankProps);
		Properties globalProps = getGlobalProperties();
		fixProcesses(globalProps);
		List<Properties> includedProps = new ArrayList<Properties>();
		
		for (String includedName : included) {
			if (!includedName.equals(CREATE_NEW_INCLUDE) && propertiesExist(login, includedName)) {
				Properties p = getBankOnlyProperties(login, includedName);
				fixProcesses(p);
				includedProps.add(p);
			}
		}
		if (bankProps.containsKey("include.properties")) {
			String[] includedFiles = bankProps.getProperty("include.properties").split(",");
			for (String file : includedFiles) {
				if (!file.trim().isEmpty() && !included.contains(file)) {
					Properties p = getBankOnlyProperties(login, file);
					fixProcesses(p);
					includedProps.add(p);
				}
			}
		}
		
		for (Object key : bankProps.keySet()) {			
			globalProps.remove(key);
			for (Properties p : includedProps)
				p.remove(key);
			res.put(prefixProperty(key.toString(), null), bankProps.getProperty(key.toString()));
		}
		
		for (Object key : globalProps.keySet()) {
			res.put("inherited:" + prefixProperty(key.toString(), null), globalProps.getProperty(key.toString()));
		}
		
		for (Properties p : includedProps) {
			for (Object key : p.keySet()) {
				res.put("inherited:" + prefixProperty(key.toString(), null), p.getProperty(key.toString()));
			}
		}
		
		return res;
	}

	@Override
	public String getProcessDescription(String process) {
		String res = "";
		try {
			Properties props = new Properties();
			props.load(new FileReader(new File(serverPath + "/processDescription.properties")));
			res = props.getProperty(process);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		if (res == null)
			return "";
		return res;
	}

	@Override
	public Collection<String> getProcessList() {
		Collection<String> processes = new TreeSet<String>();
		try {
			File dir = new File(BiomajInformation.getInstance().getProperty(BiomajInformation.PROCESSDIR));
			if (dir.exists() && dir.isDirectory()) {
				for (File f : dir.listFiles()) {
					if (!f.getName().endsWith(".txt"))
						processes.add(f.getName());
				}
			}
			return processes;
			
		} catch (BiomajException e1) {
			e1.printStackTrace();
			return null;
		}
	}
	
	@Override
	public boolean propertiesFileExists(String login, String fileName) {
		return propertiesExist(login, fileName);
	}
	
	private boolean propertiesExist(String login, String fileName) {
		return getFileForUserGroup(login, fileName) != null;
	}

	/**
	 * Returns parent dir path if given file can be created or overriden
	 * by given user, i.e. file with same name doesn't exist
	 * for another group.
	 * 
	 * @param login
	 * @param fileName
	 * @return
	 */
	private String canEditProperties(String login, String fileName) {
		
		String newName = fileName.endsWith(".properties") ? fileName : fileName + ".properties";
		
		if (newName.equals("global.properties")) {
			return getConfDir();
		}
		
		File bank = null;
		String root = getConfDir();
		File dir = new File(root);
		if (dir.exists() && dir.isDirectory()) {
			for (File file : dir.listFiles()) {
				if (file.isDirectory() && new File(file.getAbsolutePath() + "/" + newName).exists()) {
					bank = file;
					break;
				}
			}
		}
		
		if (bank == null) // File was not found, can create under login
			return root + "/" + login;
		else if (login.equals(UserManagerServiceImpl.adminLogin()))
			return bank.getAbsolutePath();
		
		List<String> usersOfGroup = UserManagerServiceImpl.getGroupUserFromLogin(login);
		String path = bank.getAbsolutePath();

		for (String user : usersOfGroup) {
			if (path.endsWith("/" + user)) // File exist within user group, Ok to edit
				return path;
		}
		
		// File exist for another group, cannot create it
		return null;
	}
	
	private static String getConfDir() {
		try {
			return BiomajInformation.getInstance().getProperty(BiomajInformation.WORKFLOWSDIR);
		} catch (BiomajException ex) {
			log.error(ex);
			return null;
		}
	}

}
