/*******************************************************************************
 * Copyright (c) 2007, 2008 IBM Corporation
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 ******************************************************************************/
package org.eclipse.cosmos.me.internal.deployment.sdd.tooling.btg.util;
 
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.cosmos.me.internal.deployment.sdd.resources.tooling.btg.Messages;
import org.eclipse.cosmos.me.provisional.deployment.sdd.tooling.btg.BTGProperty;
import org.eclipse.cosmos.me.provisional.deployment.sdd.tooling.btg.util.BTGLogUtil;

/**
 * Parses properties from a BTG properties file
 */
public class PropertiesReader {
	// File containing properties
	File propertiesFile = null;
	// Reserved property names
	public static final String ENABLED_PROPERTY = "Enabled"; //$NON-NLS-1$
	public static final String PLUGINID_PROPERTY = "PluginID"; //$NON-NLS-1$
	public static final String BTG_PLUGINID = "org.eclipse.cosmos.me.deployment.sdd.tooling.btg"; //$NON-NLS-1$
	private static Logger logger = Logger.getLogger("org.eclipse.cosmos.me.deployment.sdd.tooling.btg");//$NON-NLS-1$
    private static Logger consoleLogger = Logger.getLogger("org.eclipse.cosmos.me.deployment.sdd.tooling.btg.stdout");//$NON-NLS-1$
 
	/**
	 * Constructor
	 * 
	 * @param propertiesFileName Name of properties file from which to read
	 */
	public PropertiesReader(String propertiesFileName) {
		propertiesFile = new File(propertiesFileName);
	}
	
	/**
	 * Get the properties for each enabled plug-in in the properties file
	 * 
	 * @return A Collection of Properties objects, one for each enabled plug-in
	 * @throws FileNotFoundException If the properties file doesn't exist
	 * @throws IOException If there's an error reading from the properties file
	 * @throws ParseException If there's an error parsing the properties file
	 */
	public Collection<Properties> getPropertiesList() throws FileNotFoundException, IOException, ParseException {
		BufferedReader propsFileReader = new BufferedReader(new FileReader(propertiesFile));
		Collection<Properties> propertiesList = new ArrayList<Properties>();
		
		// Keep track of if BTG section has been read (must appear only once)
		boolean readBtgSection = false;
		
		Properties properties = readNextSection(propsFileReader);
		while (properties != null) {
			// Only store properties for enabled plug-ins and the BTG
			if ("true".equalsIgnoreCase(properties.getProperty(ENABLED_PROPERTY)) //$NON-NLS-1$
					|| BTG_PLUGINID.equals(properties.getProperty(PLUGINID_PROPERTY))) {
				propertiesList.add(properties);
			}
			
			// Make sure BTG section appears only once
			if (BTG_PLUGINID.equals(properties.getProperty(PLUGINID_PROPERTY))) {
				if (true == readBtgSection) {
					ParseException e = new ParseException(Messages.getString("PropertiesReader.4"), -1);
					consoleLogger.severe(e.getMessage());
                    consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
                    logger.log(Level.SEVERE, "ExceptionStack", e);
					// BTG section appears twice, throw an exception
					throw e; //$NON-NLS-1$ //$NON-NLS-2$
				}
				
				readBtgSection = true;
			}
			
			properties = readNextSection(propsFileReader);
		}
		
		// Check for BTG properties
		boolean hasBtgProps = false;
		for (Properties props : propertiesList) {
			if (PropertiesReader.BTG_PLUGINID.equals(props.get(PropertiesReader.PLUGINID_PROPERTY))) {
				hasBtgProps = true;
			}
		}
			
		// Throw an error if the properties file doesn't contain the BTG section
		if (!hasBtgProps) {
			ParseException e = new ParseException(Messages.getString("PropertiesReader.6", BTG_PLUGINID), -1); //$NON-NLS-1$
			consoleLogger.severe(e.getMessage());
            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
            logger.log(Level.SEVERE, "ExceptionStack", e);
			throw e; //$NON-NLS-1$ //$NON-NLS-2$
		}
		
		return propertiesList;
	}
	
	/*
	 * Get the properties from the next section in the properties file
	 */
	private Properties readNextSection(BufferedReader propertiesReader) throws IOException, ParseException {
		Properties properties = new Properties();
		
		// Read the first line starting at the current position
		String line = propertiesReader.readLine();
		if (line != null) {
			line = line.trim();
		}
		else {
			// No more lines to read so no more sections
			return (null);
		}
		
		// Ignore comments and blank lines until the section header or the end of the file
		while (!isSectionHeader(line) && (isComment(line) || isBlankLine(line))) {
			line = propertiesReader.readLine();
			if (line != null) {
				line = line.trim();
			}
			else {
				// No more lines to read so no more sections
				return (null);				
			}
		}
		
		// Line should be a section header
		if (isSectionHeader(line)) {
			properties.put(PLUGINID_PROPERTY, getPluginId(line));
		}
		else {
			// Line is not a section header, so there was an invalid line in the file
			ParseException e = new ParseException(Messages.getString("PropertiesReader.7", line), -1); //$NON-NLS-1$
			consoleLogger.severe(e.getMessage());
            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
            logger.log(Level.SEVERE, "ExceptionStack", e);
			throw e;
			
		}
		
		// Keep track of how many properties there are before we start reading the section
		int propsCount = properties.size();
		
		// Line is a section header - read until the next section header or end of file
		propertiesReader.mark(8192);
		line = propertiesReader.readLine();
		if (line != null) {
			line = line.trim();
		}
		else {
			// Hit end of file before reading any properties
			ParseException e = new ParseException(Messages.getString("PropertiesReader.8", properties.getProperty(PLUGINID_PROPERTY)), -1); //$NON-NLS-1$
			consoleLogger.severe(e.getMessage());
            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
            logger.log(Level.SEVERE, "ExceptionStack", e);
			throw e;
			 
		}
		while (line != null && !isSectionHeader(line)) {
			// Ignore comments and blank lines
			if (!isComment(line) && !isBlankLine(line)) {
				if (getPropertyName(line) == null) {
					// Invalid syntax on this line, throw an exception
					String [] inserts = new String [2];
                	inserts[0] = line;
                	inserts[1] = properties.getProperty(PLUGINID_PROPERTY);
					ParseException e = new ParseException(Messages.getString("PropertiesReader.10",inserts), -1); //$NON-NLS-1$ //$NON-NLS-2$
					consoleLogger.severe(e.getMessage());
		            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
		            logger.log(Level.SEVERE, "ExceptionStack", e);
					throw e;
					 
				}
				else {
					String propertyName = getPropertyName(line);
					// Check for duplicate property
					if (isDuplicateProperty(propertyName, properties)) {
						String [] inserts = new String [2];
	                	inserts[0] = propertyName;
	                	inserts[1] = properties.getProperty(PLUGINID_PROPERTY);
						ParseException e = new ParseException(Messages.getString("PropertiesReader.12", inserts), -1);
						consoleLogger.severe(e.getMessage());
			            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
			            logger.log(Level.SEVERE, "ExceptionStack", e);
						throw e;
						 
					}
					
					// Store the property
					String propertyValue = getPropertyValue(line);
					if (propertyValue != null) {
						properties.put(propertyName, propertyValue);
					}
					else {
						String [] inserts = new String [2];
	                	inserts[0] = line;
	                	inserts[1] = properties.getProperty(PLUGINID_PROPERTY);
						ParseException e = new ParseException(Messages.getString("PropertiesReader.14", inserts), -1);
						consoleLogger.severe(e.getMessage());
			            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
			            logger.log(Level.SEVERE, "ExceptionStack", e);
						throw e;
						 						
					}
				}
			}
			
			// Mark the current position and read the next line
			propertiesReader.mark(8192);
			line = propertiesReader.readLine();
			if (line != null) {
				line = line.trim();
			}
		}
		
		// If the last line read is a section header, back the reader up one line
		if (isSectionHeader(line)) {
			propertiesReader.reset();
		}
		
		// Make sure the section just read contained some properties
		if (properties.size() <= propsCount) {
			ParseException e = new ParseException(Messages.getString("PropertiesReader.16", properties.getProperty(PLUGINID_PROPERTY)), -1);  //$NON-NLS-1$
			consoleLogger.severe(e.getMessage());
            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
            logger.log(Level.SEVERE, "ExceptionStack", e);
			throw e;
			 
		}
		
		// Make sure the section just read contained the Enabled property or is the BTG section
		boolean hasEnabled = false;
		for (Object propertyNameObj : properties.keySet()) {
			String propertyName = (String)propertyNameObj;
			if (ENABLED_PROPERTY.equals(propertyName)) {
				hasEnabled = true;
			}
		}
		if (!hasEnabled && !BTG_PLUGINID.equals(properties.getProperty(PLUGINID_PROPERTY))) {
			String[] inserts = new String[2];
        	inserts[0] = properties.getProperty(PLUGINID_PROPERTY);
        	inserts[1] = ENABLED_PROPERTY;
			ParseException e = new ParseException(Messages.getString("PropertiesReader.18", inserts), -1); //$NON-NLS-1$ //$NON-NLS-2$
			consoleLogger.severe(e.getMessage());
            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
            logger.log(Level.SEVERE, "ExceptionStack", e);
			throw e;
		}
		
		return (properties);
	}
	
	/*
	 * Returns true if the given line is a section header
	 */
	private boolean isSectionHeader(String line) {
		// Avoid a NullPointerException
		if (null == line) {
			return (false);
		}
		
		// A section header must start with [, end with ] and have one or more characters in between
		if (line.startsWith("[") && line.endsWith("]")) { //$NON-NLS-1$ //$NON-NLS-2$
			if (line.indexOf("[") < (line.lastIndexOf("]") - 1)) { //$NON-NLS-1$ //$NON-NLS-2$
				return (true);
			}
		}
		
		return (false);
	}
	
	/*
	 * Returns true if the given line is a comment
	 */
	private boolean isComment(String line) {
		// Avoid a NullPointerException
		if (null == line) {
			return (false);
		}

		// A comment starts with #
		if (line.startsWith("#")) { //$NON-NLS-1$
			return (true);
		}
		
		return (false);
	}
	
	/*
	 * Returns true if the given line is blank
	 */
	private boolean isBlankLine(String line) {
		// Avoid a NullPointerException
		if (null == line) {
			return (false);
		}

		// A blank line contains no characters
		if (line.length() == 0) {
			return (true);
		}
		
		return (false);
	}
	
	/*
	 * Extract the plug-in id from the given line (which must be a section header)
	 */
	private String getPluginId(String line) {
		int openingBracket = line.indexOf("["); //$NON-NLS-1$
		int closingBracket = line.lastIndexOf("]"); //$NON-NLS-1$
		
		return (line.substring(openingBracket + 1, closingBracket));
	}
	
	/*
	 * Extract the property name from the given line
	 */
	private String getPropertyName(String line) {
		int equalIndex = line.indexOf("="); //$NON-NLS-1$
		
		if (equalIndex == -1) {
			// The syntax of this line is invalid
			return (null);
		}
		
		String propertyName = line.substring(0, equalIndex).trim();
		
		if (propertyName.length() == 0) {
			// The syntax of this line is invalid
			return (null);			
		}
		
		return (propertyName);
	}
	
	/*
	 * Extract the property value from the given line
	 */
	private String getPropertyValue(String line) {
		int equalIndex = line.indexOf("="); //$NON-NLS-1$
		
		if (equalIndex == -1) {
			// The syntax of this line is invalid
			return (null);
		}
		
		String propertyValue = line.substring(equalIndex + 1).trim();

		if (propertyValue.length() == 0) {
			// The syntax of this line is invalid
			return (null);			
		}

		return (propertyValue);
	}
	
	/*
	 * Determine if the given property name already exists in the given properties
	 */
	private boolean isDuplicateProperty(String propertyName, Properties props) {
		for (Object nextNameObj : props.keySet()) {
			String nextName = (String)nextNameObj;
			
			if (propertyName.equals(nextName)) {
				return (true);
			}
		}
		
		return (false);
	}
	
	/**
	 * Check the given properties to make sure no required properties are missing and no unsupported property is present
	 * 
	 * @param props Properties to check for correctness
	 * @param supportedProps Array of supported properties to check against
	 * @throws ParseException If a required property is missing or an unsupported property is present
	 */
	public void checkSupportedProperties(Properties props, Collection<BTGProperty> supportedProps) throws ParseException {
		// if it's BTG plug-in, then do not allow Enabled as a property
		if (BTG_PLUGINID.equals(props.getProperty(PLUGINID_PROPERTY))){
			if (props.getProperty(ENABLED_PROPERTY)!=null){
				String [] inserts = new String [2];
	        	inserts[0] = props.getProperty(PLUGINID_PROPERTY);
	        	inserts[1] = ENABLED_PROPERTY;
				ParseException e = new ParseException(Messages.getString("PropertiesReader.30",inserts), -1);					 //$NON-NLS-1$
				consoleLogger.severe(e.getMessage());
	            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
	            logger.log(Level.SEVERE, "ExceptionStack", e);
				throw e;
			}
		}
		
		// Make sure no unsupported properties are present
		for (Object propertyNameObj : props.keySet()) {
			String propertyName = (String)propertyNameObj;
			
			// Ignore reserved properties
			if (!PLUGINID_PROPERTY.equals(propertyName) && !ENABLED_PROPERTY.equals(propertyName)) {
				boolean supported = false;

				// Check if this property is supported
				for (BTGProperty supportedProp : supportedProps) {
					if (propertyName.equals(supportedProp.getName())) {
						supported = true;
					}
				}
				
				// Throw an exception if the property isn't supported
				if (!supported) {
					String [] inserts = new String [2];
		        	inserts[0] = props.getProperty(PLUGINID_PROPERTY);
		        	inserts[1] = propertyName;
					ParseException e = new ParseException(Messages.getString("PropertiesReader.30",inserts), -1);					 //$NON-NLS-1$
					consoleLogger.severe(e.getMessage());
		            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
		            logger.log(Level.SEVERE, "ExceptionStack", e);
					throw e;
				}
			}
		}
		
		// Make sure no required properties are missing
		for (BTGProperty supportedProp : supportedProps) {
			// Check only required properties
			if (supportedProp.isRequired()) {
				boolean requiredPresent = false;
				
				// Check if this property is present
				for (Object propertyObj : props.keySet()) {
					String property = (String)propertyObj;
					
					if (property.equals(supportedProp.getName())) {
						requiredPresent = true;
					}
				}
				
				// Throw an exception if the required property is missing
				if (!requiredPresent) {
					String [] inserts = new String [2];
		        	inserts[0] = props.getProperty(PLUGINID_PROPERTY);
		        	inserts[1] = supportedProp.getName();
					ParseException e = new ParseException(Messages.getString("PropertiesReader.32", inserts), -1); //$NON-NLS-1$ //$NON-NLS-2$
					consoleLogger.severe(e.getMessage());
		            consoleLogger.log(Level.INFO, Messages.getString("BTGApplication.30"), BTGLogUtil.getMsgLogName());
		            logger.log(Level.SEVERE, "ExceptionStack", e);
					throw e;	 
				}
			}
		}
	}	
}
