/*
 * Delivered to the U.S. Government in Accordance with Contract FA8650-10-C-7080.
 *
 * EXPORT CONTROLLED
 * ECCN#   EAR99
 * Warning: This document contains technical data which is controlled under the
 * Bureau of Industry & Security's Export Administration Regulations and may not be
 * exported or re-exported without authorization by the Bureau of Industry & Security.
 * Diversion contrary to US law is prohibited.
 *
 * (c) 2011 United Technologies Corp.
 * Developed with the sponsorship of the Defense Advanced Research Projects Agency (DARPA).
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this data,
 * including any software or models in source or binary form, as well as any drawings, 
 * specifications, and documentation (collectively "the Data"), to deal in the Data without 
 * restriction, including without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Data, and to permit persons to whom the Data
 * is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Data.
 *
 * THE DATA IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS, SPONSORS, DEVELOPERS, CONTRIBUTORS, OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE DATA OR THE
 * USE OR OTHER DEALINGS IN THE DATA.
*/

package meta.aee.service;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import meta.aee.service.excel.Command;
import meta.aee.service.excel.CommandResult;
import meta.aee.service.excel.ExcelProxy;
import meta.aee.service.excel.GetCellCommand;
import meta.aee.service.excel.GetCellValue;
import meta.aee.service.excel.GetSheetCommand;
import meta.aee.service.excel.IsExcelRunning;
import meta.aee.service.excel.RunMacroCommand;
import meta.aee.service.excel.SetCellValue;
import meta.aee.service.excel.StartExcelCommand;
import meta.aee.service.excel.StopExcelCommand;
import meta.rmi.Argument;
import meta.rmi.ServiceImpl;
import meta.rmi.ServiceRequest;
import meta.rmi.ServiceResponse;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;

import aee.csv.CsvWriter;
import aee.csv.DeviceRulesCsvWriter;
import aee.csv.DeviceTableCsvWriter;
import aee.csv.FlowRulesCsvWriter;
import aee.csv.FlowTableCsvWriter;
import aee.csv.InOutTableCsvWriter;
import aee.engine.DeviceTable;
import aee.engine.FlowTable;
import aee.engine.InOutTable;
import aee.engine.RuleTable;
import aee.engine.TypedObject;
import aee.excel.Cell;
import aee.excel.Worksheet;
import aee.exception.AeeException;
import aee.util.FileUtils;
import aee.util.StringUtils;

/**
 * The AEE service handles the calls  (see aee.properties file)
 *     NewConfiguration     -- define a new architecture configuration
 *     GetNextArchitecture  -- fetch the next architecture
 *     Shutdown             -- shut the service down
 */
public class AeeService extends ServiceImpl {
	private static final long	serialVersionUID	= 1L;
	/**
	 * The service description
	 */
	public static final String ServiceDesc = "AEE Service";
	/**
	 * The service name. This is the RMI bind property
	 */
	public static final String ServiceName = "AEE";
	
	private Properties properties;
	private BlockingQueue<Command>        toXq;
	private BlockingQueue<CommandResult>  fromXq;

	/**
	 * Constructor
	 *
	 * @param description  Service description
	 * @throws RemoteException
	 */
	public AeeService(String description) throws RemoteException {
		super(description);
		loadServiceProperties();
		
		// Communicate with the ExcelProxy with two Blocking queues. The to queue
		// is the command to execute, while the from queue gets the command result
		// See the execXxxCmd() suite of methods for executing Excel commands
		toXq   = new ArrayBlockingQueue<Command>(1);
		fromXq = new ArrayBlockingQueue<CommandResult>(1);
		new ExcelProxy(toXq, fromXq, properties).start();
	}

	/* (non-Javadoc)
	 * @see meta.rmi.ServiceImpl#execute()
	 */
	@Override
	protected void execute() {
		
		// What operation should be performed?
		String oper = getServiceRequest()
		             .getRequestName()
		             .trim();
		if (oper == null)
			setServiceStatus(500, "Missing request name");
		
		else if (oper.equalsIgnoreCase(properties.getProperty("NewConfig")))
			executeNewConfig();
			
		else if (oper.equalsIgnoreCase(properties.getProperty("GetNextArch")))
			executeGetNextArch();
		
		else if (oper.equalsIgnoreCase(properties.getProperty("Shutdown")))
			executeShutdown();
		
		else
			setServiceStatus(500, 
					            "Unrecognized service request name \"%s\"", oper);
	}
	
	/**
	 * Load the AEE Service property file
	 * @throws RemoteException if the properties cannot be read
	 */
	private void loadServiceProperties() throws RemoteException {
		
		// If the property -Daee.properties is set, then this is the pathname
		// to the AEE property file. If missing, then the default path of
		// "aee.properties" is used.  Note that if the "datadir" property is
		// set, propagate it to the new properties
		final String DATADIR = "datadir";
		String datadir       = System.getProperty(DATADIR);
		String props         = System.getProperty("aee.properties", "aee.properties");
		properties           = new Properties();
		try {
			properties.load(new FileInputStream(props));
			if (datadir != null)
				properties.setProperty(DATADIR, datadir);
			
		} catch (IOException ioex) {
			throw new RemoteException(String.format(
					"Error loading property file \"%s\": %s", props, ioex.getMessage()));
		}
	}

	/**
	 * Perform a new configuration request
	 */
	private void executeNewConfig() {
		try {
			
			// Get the level argument
			ServiceRequest req = getServiceRequest();
			Argument       larg = req.getArgument("level", "int");
			if (larg == null)
				throw new AeeException("Missing the level argument in request");
			int level           = larg.getIntValue();
			
			// Read and parse all XML input from the service request
			TypedObject[] args = parseNewConfigArgs(level);
			
			// Build a set of CSV files containing data from the service request
			String base     = System.getProperty("java.io.tmpdir", "T:\\Temp");
			base            = StringUtils.trimTrailingDlm(base, null);
			String folder   = "AeeTemp";
			File   basePath = new File(base,folder);
			basePath.mkdir();
			
			CsvWriter[] csv = null;
			try {
				csv = writeCsvFiles(args, basePath.getPath());
				
				// Update the master Excel spreadsheet
				doNewConfig(level, base, folder, csv);
				
			} finally {
				
				// Blow away all CSV files when done
				if (csv != null) {
					for (CsvWriter w : csv) {
						if (w != null)
							new File(w.getFn()).delete();
					}
				}
			}
	
		} catch (AeeException aex) {
			setServiceStatus(500, aex.getMessage());
		}
	}

	/**
	 * Perform a get next architecture request
	 */
	private void executeGetNextArch() {
		try {
			if (! execBooleanCmd(new IsExcelRunning()))
				throw new AeeException("You must call %s before calling %s",
							                   properties.getProperty("NewConfig"),
							                   properties.getProperty("GetNextArch"));

			// Run a macro to get the next architecture
			execCmd(new RunMacroCommand(properties.getProperty("nextarch.macro")));
			extractArchitecture();
			
		} catch (Exception aex) {
			setServiceStatus(500, aex.getMessage());
		}
	}
	
	/**
	 * Extract the next architecture from Excel. The architecture is stuffed
	 * into the service response
	 * @throws Exception if any problem getting the architecture
	 */
	private void extractArchitecture() throws Exception {
		Worksheet worksheet = null;
		Cell      cell      = null;
		try {
	
			// Get the cell with the next architecture in it
			String shtName  = properties.getProperty("nextarch.sheet");
			String cellName = properties.getProperty("nextarch.cell");
			worksheet       = (Worksheet) execCmd(new GetSheetCommand(shtName));	
			cell            = (Cell)      execCmd(new GetCellCommand(worksheet, cellName));
			String nextarch = execStringCmd(new GetCellValue(cell));
			if (nextarch == null) {
				throw new AeeException("No value in cell %s.%s", shtName, cellName);

			} else {
		    SAXBuilder builder  = new SAXBuilder();
		    Document   document = builder.build(new ByteArrayInputStream(nextarch.getBytes()));
		    Element    arch     = document.getRootElement();
		    normalizeArchitecture(arch);
		    getServiceResponse().addContent((Element) arch.clone());
			}

		} finally {
			if (worksheet != null) worksheet.dispose();
			if (cell      != null) cell.dispose();
		}
	}
	
	/**
	 * "Normalize" the architecture by making all device (and flow id's)
	 * consecutive starting at 1
	 * @param arch The root architecture element
	 */
	private void normalizeArchitecture(Element arch) {
		Map<String, Integer> devices = buildNormalMap(arch.getChildren("device"));
		//Map<String, Integer> flows   = buildNormalMap(arch.getChildren("flow"));
		
		// Update all links with the new mapping
		for (Object child : arch.getChildren("link")) {
			Element element = (Element) child;
			normalizeReference(element, "source", devices);
			normalizeReference(element, "sink",   devices);
			//normalizeReference(element, "flow",   flows);			
		}
	}

	/**
	 * Build a normalization mapping table
	 * @param children   The children elementst
	 * @return           The normalization table
	 */
	private Map<String, Integer> buildNormalMap(List<?> children) {
		Map<String, Integer> mapping = new HashMap<String, Integer>();
		int                  index   = 1;
		
		for (Object child : children) {
			Element element = (Element) child;
			String  id      = element.getAttributeValue("id");
			if (id != null) {
				int newref = index++;
				mapping.put(id, newref);
				element.setAttribute("id", String.format("%d", newref));
			}
		}
		return mapping;
	}
	
	/**
	 * Normalize an architecture reference. Convert an old reference to a
	 * new value
	 * @param element  The element with a reference
	 * @param attrName The element attribute that references an id
	 * @param mapping  The id mapping table
	 */
	private void normalizeReference(Element element, String attrName,
			                            Map<String, Integer> mapping) {
		String attr = element.getAttributeValue(attrName);
		if (attr != null  &&  mapping.containsKey(attr)) {
			int newref = mapping.get(attr);
			element.setAttribute(attrName, String.format("%d", newref));
		}
	}

	/**
	 * Handle a service shutdown
	 */
	private void executeShutdown() {
		try {
			execCmd(new StopExcelCommand());
		} catch (Exception ex) {
		}
	}
	
	/**
	 * Convert input service request into a set of parsed objects
	 * @param level  Current execution level
	 * @return Array of parsed objects:
	 * 	          DeviceTable FlowTable InOutTable(input)
	 *            InOutTable(output) DeviceRules FlowRules
	 * @throws AeeException
	 */
	private TypedObject[] parseNewConfigArgs(int level) throws AeeException {
		Element     root    = getValidateArg("devicetable");
		DeviceTable devices = new DeviceTable(level, root);

		root                = getValidateArg("flowtable");
		FlowTable   flows   = new FlowTable(level, root);

		root                = getValidateArg("inputtable");
		InOutTable  input   = new InOutTable(root, "inputtable");

		root                = getValidateArg("outputtable");
		InOutTable  output  = new InOutTable(root, "outputtable");

		root                = getValidateArg("devicerules");
		RuleTable   drules  = new RuleTable(level, root, "devicerules");
		
		// The flow rules are not in level 0
		RuleTable   frules  = null;
		try {
			root   = getValidateArg("flowrules");
			frules = new RuleTable(level, root, "flowrules");
		} catch (Exception ex) {
			frules = null;
		}

		return new TypedObject[]{devices, flows, input, output, drules, frules};
	}
	
	/**
	 * Get and validate a composite argument. There can only be 1 such arg
	 * @param type  The argument type (its name attribute)
	 * @return      The composite arg root element
	 * @throws AeeException if invalid number of composite args of type
	 */
	private Element getValidateArg(String type) throws AeeException {
		final String   argName = properties.getProperty("RequestName");
		ServiceRequest req     = getServiceRequest();
		List<?>        roots   = req.getCompositeArg(argName, type);
		int            n       = roots.size();
		if (n == 0)
			throw new AeeException("AEE %s request is missing a \"%s\" " +
					                   "composite argument",
					                   req.getRequestName(), type);
		else if (n > 1)
			throw new AeeException("AEE %s request has %d %s " +
					                   "composite arguments. Only 1 is allowed",
					                   req.getRequestName(), n, type);
		
		// So the child of roots is a single composite_argument. This composite arg
		// itself must have a single child
		Element compArg = (Element) roots.get(0);
		roots           = compArg.getChildren();
		n               = roots.size();
		if (n == 0)
			throw new AeeException("AEE %s request has a %s " +
					                   "composite argument with no data",
					                   req.getRequestName(), type);
		else if (n > 1)
			throw new AeeException("AEE %s request has a %s " +
					                   "composite argument with %d children. Only 1 is allowed",
					                   req.getRequestName(), n, type);
		
		return (Element) roots.get(0);
	}

	/**
	 * Convert service request arguments into CSV files
	 * @param args      Parsed service request objects from
	 *                  parseNewConfigArgs()
	 * @param basePath  Base path for all CSV files
	 * @return          Array of csv writers corresponding to the
	 *                  written csv files
	 * @throws AeeException if any error writing the CSV files
	 */
	private CsvWriter[] writeCsvFiles(TypedObject[] args, String basePath)
	throws AeeException {
		CsvWriter[]  csv = new CsvWriter[args.length];
		final String ext = "csv";
		
		File file = FileUtils.getUniquelyNamedFile(basePath, "devicetable", ext);
		csv[0]    = new DeviceTableCsvWriter(file.getPath());
		try {
			((DeviceTableCsvWriter) csv[0]).write((DeviceTable) args[0]);
		} finally {
			csv[0].close();
		}
		
		file = FileUtils.getUniquelyNamedFile(basePath, "flowtable", ext);
		csv[1] = new FlowTableCsvWriter(file.getPath());
		try {
			((FlowTableCsvWriter) csv[1]).write((FlowTable) args[1]);
		} finally {
			csv[1].close();
		}
		
		file = FileUtils.getUniquelyNamedFile(basePath, "inputtable", ext);
		csv[2] = new InOutTableCsvWriter(file.getPath());
		try {
			((InOutTableCsvWriter) csv[2]).write((InOutTable)  args[2], 
						                               (DeviceTable) args[0],
						                               (FlowTable)   args[1]);
		} finally {
			csv[2].close();
		}
		
		file = FileUtils.getUniquelyNamedFile(basePath, "outputtable", ext);
		csv[3] = new InOutTableCsvWriter(file.getPath());
		try {
			((InOutTableCsvWriter) csv[3]).write((InOutTable)  args[3], 
                                           (DeviceTable) args[0],
                                           (FlowTable)   args[1]);
		} finally {
			csv[3].close();
		}
		
		file = FileUtils.getUniquelyNamedFile(basePath, "devicerules", ext);
		csv[4] = new DeviceRulesCsvWriter(file.getPath());
		try {
			((DeviceRulesCsvWriter) csv[4]).write((RuleTable) args[4]);
		} finally {
			csv[4].close();
		}
		
		csv[5] = null;
		if (args[5] != null) {
			file = FileUtils.getUniquelyNamedFile(basePath, "flowrules", ext);
			csv[5] = new FlowRulesCsvWriter(file.getPath());
			try {
				((FlowRulesCsvWriter) csv[5]).write((RuleTable) args[5]);
			} finally {
				csv[5].close();
			}
		}

		return csv;
	}
	
	/**
	 * Actually perform the new configuration. This invokes an excel macro
	 * @param level   Current execution level
	 * @param base    Base folder for all csv files
	 * @param folder  The folder within base of the csv files
	 * @param csv     Array of all csv files
	 * @throws AeeException if any problem running the macro or setting up
	 *                      excel to use this csv data
	 */
	private void doNewConfig(int level, String base, String folder, CsvWriter[] csv) 
	throws AeeException {
		Worksheet worksheet = null;
		try {
			execCmd(new StartExcelCommand(level));
			
			// On sheet FileIO...
			// ...set the base folder...			
			// ...set the csv folder...			
			// ...set the fn (minus extension -- must be csv) of each csv file
			worksheet = (Worksheet) execCmd(new GetSheetCommand(properties.getProperty("File.sheet")));
			setCellValue(worksheet, "Base",   base);
			setCellValue(worksheet, "Folder", folder);
			
			String[] cellnames = new String[]{"devicetable", "flowtable",   "inputtable",
					                              "outputtable", "devicerules", "flowrules"};
			for (int i = 0; i < cellnames.length; i++) {
				if (csv[i] != null) {
					File f = new File(csv[i].getFn());
					setCellValue(worksheet, cellnames[i], StringUtils.removeExt(f.getName()));
				}
			}
	
			// Establish the new configuration
			execCmd(new RunMacroCommand(properties.getProperty("newconfig.macro")));
			
			// Setting the new configuration reports the first architecture
			extractArchitecture();
			
		} catch (Exception iex) {
			throw new AeeException(iex);
			
		} finally {
			if (worksheet != null) worksheet.dispose();
		}
	}
		
	/**
	 * Set a cell's value
	 * @param worksheet The excel worksheet with the cell
	 * @param prop   Cell property name
	 * @param value  Value for the cell
	 * @throws Exception if any error setting the value
	 */
	private void setCellValue(Worksheet worksheet, String prop, String value) 
	throws Exception {
		Cell cell = null;
		try {
			cell = (Cell) execCmd(new GetCellCommand(worksheet, 
					                                     properties
					                                    .getProperty(String.format("%s.cell", prop))));
			execCmd(new SetCellValue(cell, value));
			
		} finally {
			if (cell != null) cell.dispose();
		}
	}

	/**
	 * Set a service response
	 * @param status The response ststus
	 * @param format The response message as a String.format
	 * @param args   Arguments for the response message
	 */
	private void setServiceStatus(int status, String format, Object... args) {
		ServiceResponse response = getServiceResponse();
		response.setStatus(status);
		response.setStatusMsg(String.format(format, args));
	}

	/**
	 * Utility to execute a synchronized command with the ExcelProxy
	 * @param cmd The command to execute
	 * @return    The command result
	 * @throws Exception if any command exception
	 */
	private Object execCmd(Command cmd) throws Exception {
		toXq.add(cmd);
		CommandResult cmdResult = fromXq.take();
		Object        result    = cmdResult.getResult();
		return result;
	}
	
	/**
	 * Execute an Excel command that returns a string
	 * @param cmd The command to execute
	 * @return    The command result as a boolean
	 * @throws Exception if any command exception
	 */
	private String execStringCmd(Command cmd) throws Exception {
		return execCmd(cmd).toString();
	}
	
	/**
	 * Execute an Excel command that returns a boolean
	 * @param cmd The command to execute
	 * @return    The command result as a boolean
	 * @throws Exception if any command exception
	 */
	private boolean execBooleanCmd(Command cmd) throws Exception {
		return Boolean.parseBoolean(execStringCmd(cmd));
	}
}
