/**
 * ParameterReferenceHelper.java
 * Created on May 19, 2008
 * (C) Copyright TANDBERG Television Ltd.
 */
package com.tandbergtv.watchpoint.pmm.job.conf;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.tandbergtv.watchpoint.pmm.entities.EntityType;
import com.tandbergtv.watchpoint.pmm.job.ui.IMenuOptionProvider;
import com.tandbergtv.watchpoint.pmm.job.util.ParameterReferenceHelper;

/**
 * @author spuranik
 * 
 * This class parses the config file which indicates the list of items
 * that should appear in the drop down menu for a given entity.
 * It also serves as a provider for the callback class for a job rule. 
 */
public class ParameterReferenceFileParser {

	// map of entityType->groups available in the job rule drop down
	private Map<EntityType, List<ParameterReferenceGroup>> rules;

	// map of entityType->groups available in the job parameter drop down
	private Map<EntityType, List<ParameterReferenceGroup>> parameters;

	private static final Logger logger = Logger.getLogger(ParameterReferenceFileParser.class);

	// config file name
	private static String PARAMETER_REFERENCE_FILE = "jobParameterReference.xml";
	// entity element
	private static String ENTITY_ELEMENT = "entity";
	// entity type attribute of entity
	private static String ENTITY_TYPE_ELEMENT = "type";
	// rule menu options
	private static String RULE_MENU_ELEMENT = "ruleMenu";
	// parameter menu options
	private static String PARAMETER_MENU_ELEMENT = "paramMenu";
	// item group name attribute in itemgroup element
	// itemgroup element
	private static String ITEMGROUP_ELEMENT = "itemGroup";
	private static String ITEM_GROUPNAME_ATTR = "name";
	// class attribute of itemgroup 
	private static String ITEM_GROUP_PROVIDERCLASS = "class";
	// attribute which indicates if this group is associated with titles
	private static String ITEMGROUP_TYPE_ATTR = "type";
	// callback element under itemgroup
	private static String ITEMGROUP_CALLBACK_ELEMENT = "callback";
	// callback element's class attribute
	private static String CALLBACK_CLASS_ATTR = "class";
	// item element under itemgroup
	private static String ITEMGROUP_ITEM_ELEMENT = "item";
	// name attribute for item
	private static String ITEM_NAME_ATTR = "name";
	// spec by which job and rule parameters are filtered
	private String spec;

	/**
	 * @return the rules
	 */
	public Map<EntityType, List<ParameterReferenceGroup>> getRules() {
		return rules;
	}

	/**
	 * @param rules the rules to set
	 */
	public void setRules(Map<EntityType, List<ParameterReferenceGroup>> rules) {
		this.rules = rules;
	}

	/**
	 * @return the parameters
	 */
	public Map<EntityType, List<ParameterReferenceGroup>> getParameters() {
		return parameters;
	}

	/**
	 * @param parameters the parameters to set
	 */
	public void setParameters(Map<EntityType, List<ParameterReferenceGroup>> parameters) {
		this.parameters = parameters;
	}

	/**
	 * Default ctor
	 */
	public ParameterReferenceFileParser(String spec) {
		rules = new HashMap<EntityType, List<ParameterReferenceGroup>>();
		parameters = new HashMap<EntityType, List<ParameterReferenceGroup>>();
		this.spec = spec;
		readConfig();
	}

	/**
	 * For each entity type in the config, parse the rule and parameter options
	 * and then add it to the respective map.
	 */
	private void readConfig() {
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder docBuilder = factory.newDocumentBuilder();
			Document doc = docBuilder.parse(this.getClass().getResourceAsStream(
					PARAMETER_REFERENCE_FILE));

			// get all the entity elements
			NodeList entities = doc.getElementsByTagName(ENTITY_ELEMENT);

			for (int i = 0; i < entities.getLength(); i++) {
				Node currEntity = entities.item(i);
				// entity type
				String type = ((Element) currEntity).getAttribute(ENTITY_TYPE_ELEMENT);
				NodeList children = currEntity.getChildNodes();
				for (int j = 0; j < children.getLength(); j++) {
					Node currChild = children.item(j);
					if (currChild.getNodeName().equals(RULE_MENU_ELEMENT)) {
						// parse the rule menu
						List<ParameterReferenceGroup> groups = getGroupsFromMenu(currChild);
						logger.debug("Adding rule group entry for type: " + type);
						rules.put(EntityType.toType(type), groups);
					} else if (currChild.getNodeName().equals(PARAMETER_MENU_ELEMENT)) {
						// parse the parameter menu
						List<ParameterReferenceGroup> groups = getGroupsFromMenu(currChild);
						logger.debug("Adding parameter group entry for type: " + type);
						parameters.put(EntityType.toType(type), groups);
					}
				}
			}
			logger.debug("Final Rules map count: " + rules.size());
			logger.debug("Final Parameters map count: " + parameters.size());
		} catch (ParserConfigurationException e) {
			logger
					.error("Error while parsing the file: " + PARAMETER_REFERENCE_FILE
							+ e.toString());
		} catch (SAXException e) {
			logger.error("SAX exception while parsing the file: " + PARAMETER_REFERENCE_FILE
					+ e.toString());
		} catch (IOException e) {
			logger.error("SAX exception while parsing the file: " + PARAMETER_REFERENCE_FILE
					+ e.toString());
			e.printStackTrace();
		}
	}

	/**
	 * @param ruleMenu
	 * the 'entity/ruleMenu' element which needs to be parsed 
	 * @return
	 * list of groups found in the ruleMenu element passed.
	 */
	private List<ParameterReferenceGroup> getGroupsFromMenu(Node menu) {
		List<ParameterReferenceGroup> groupList = new ArrayList<ParameterReferenceGroup>();

		try {
			NodeList itemGroups = ((Element) menu).getElementsByTagName(ITEMGROUP_ELEMENT);
			for (int i = 0; i < itemGroups.getLength(); i++) {
				Node currItemGroup = itemGroups.item(i);
				Element itemGroupElement = ((Element) currItemGroup);
				// create a group for every itemgroup element
				ParameterReferenceGroup group = new ParameterReferenceGroup();

				// group name
				String groupName = itemGroupElement.getAttribute(ITEM_GROUPNAME_ATTR);
				group.setName(groupName);

				// get the type for this group e.g. whether it is strictly associated with titles
				// or available irrespective of that.
				ParameterReferenceGroupType type = ParameterReferenceGroupType.INDEPENDENT;
				if (itemGroupElement.hasAttribute(ITEMGROUP_TYPE_ATTR)) {
					type = ParameterReferenceGroupType.getType(itemGroupElement
							.getAttribute(ITEMGROUP_TYPE_ATTR));
				}
				group.setType(type);

				// callback for this group. callback is not given in parameter menus.				
				NodeList callbacks = itemGroupElement
						.getElementsByTagName(ITEMGROUP_CALLBACK_ELEMENT);
				if (callbacks.getLength() > 0) {
					String callbackClass = ((Element) callbacks.item(0))
							.getAttribute(CALLBACK_CLASS_ATTR);
					group.setCallback(callbackClass);
				}

				// if the items for this group are provided by a class, call the class
				if (itemGroupElement.hasAttribute(ITEM_GROUP_PROVIDERCLASS)) {
					String groupProviderClass = itemGroupElement
							.getAttribute(ITEM_GROUP_PROVIDERCLASS);
					if (groupProviderClass.trim().length() > 0) {
						Class<?> groupProvider = Class.forName(groupProviderClass);
						IMenuOptionProvider provider = (IMenuOptionProvider) groupProvider
								.newInstance();

						// get the appropriate menu list based on whether its a ruleMenu or a paramMenu element
						List<ParameterReferenceItem> providerItems = new ArrayList<ParameterReferenceItem>();
						if (menu.getNodeName().equalsIgnoreCase(RULE_MENU_ELEMENT)) {
							providerItems = provider.getJobRuleMenuOptions(spec);
						} else if (menu.getNodeName().equalsIgnoreCase(PARAMETER_MENU_ELEMENT)) {
							providerItems = provider.getJobParameterMenuOptions(spec);
						}

						// set these as items for the outer group
						group.setItems(providerItems);

						// iterate thru the item list given by the provider and 
						// set their parent. NOTE: an item could be a group again.						
						for (int j = 0; j < providerItems.size(); j++) {
							ParameterReferenceItem currItem = providerItems.get(j);
							currItem.setParent(group);
							if (currItem.getClass().equals(ParameterReferenceGroup.class)) {
								((ParameterReferenceGroup) currItem).setType(group.getType());
							}							
						}
					}
				} else {
					// if there are any items that need to be added to the outer group add them
					NodeList items = itemGroupElement.getElementsByTagName(ITEMGROUP_ITEM_ELEMENT);
					for (int j = 0; j < items.getLength(); j++) {
						Element currItem = (Element) items.item(j);
						ParameterReferenceItem item = new ParameterReferenceItem();
						String itemName = currItem.getAttribute(ITEM_NAME_ATTR);
						item.setName(itemName);
						item.setParent(group);

						group.addItem(item);
					}
				}
				// add the newly created group to the item list
				groupList.add(group);
			}
		} catch (ClassNotFoundException e) {
			logger.error("Error while loading class: " + e.toString());
		} catch (InstantiationException e) {
			logger.error("Error : " + e.toString());
		} catch (IllegalAccessException e) {
			logger.error("Error : " + e.toString());
		}

		return groupList;
	}

	/**
	 * @param type
	 * 		the entity type which has the group
	 * @param groupName
	 * 		the group name for which the callback is requested
	 * @return
	 * 		fully qualified name of the callback class for this groupname
	 */
	public String getCallback(EntityType type, String groupName) {
		// only rules have a callback. so look in this map only.
		if (rules.containsKey(type)) {
			List<ParameterReferenceGroup> groups = rules.get(type);
			for (ParameterReferenceGroup group : groups) {
				if (group.getName().equalsIgnoreCase(groupName)) {
					return group.getCallback();
				}
			}
		}
		return new String();
	}

	/**
	 * @param type
	 * entity type e.g. partner, service, self for which job rules are requested
	 * @return
	 * the groups that are available for selection for this entity type 
	 */
	public List<ParameterReferenceGroup> getRules(EntityType type) {
		List<ParameterReferenceGroup> groups = new ArrayList<ParameterReferenceGroup>();

		if (rules.containsKey(type))
			return rules.get(type);

		return groups;
	}

	/**
	 * @param type
	 * entity type e.g. partner, service, self for which job parameter refs are requested
	 * @return
	 * the groups that are available for selection for this entity type
	 */
	public List<ParameterReferenceGroup> getParameters(EntityType type) {
		List<ParameterReferenceGroup> groups = new ArrayList<ParameterReferenceGroup>();

		if (parameters.containsKey(type))
			return parameters.get(type);

		return groups;
	}

	/**
	 * For the given entity type get all groups that are available for the parameter ref selection
	 * based on whether or not the group is associated with titles.
	 * INDEPENDENT groups are also included in title associated groups.
	 *  
	 * @param type
	 * entity type. e.g. PARTNER, SELF, SERVICE
	 * @param associatedWithTitles
	 * true/false indicating if this group is associated with titles	 * 
	 * @return
	 * list of all groups for the given entity that satisfy the match.
	 */
	public List<ParameterReferenceGroup> getParameters(EntityType type,
			boolean associatedWithTitles) {
		List<ParameterReferenceGroup> paramGroups = new ArrayList<ParameterReferenceGroup>();

		if (parameters.containsKey(type)) {
			List<ParameterReferenceGroup> groups = parameters.get(type);

			// if groups associated with titles are requested, return all
			// including independent groups
			if (associatedWithTitles) {
				return groups;
			} else {
				// else only return those groups which are INDEPENDENT type
				for (ParameterReferenceGroup currGroup : groups) {
					if (currGroup.getType() == ParameterReferenceGroupType.INDEPENDENT)
						paramGroups.add(currGroup);
				}
			}
		}
		return paramGroups;
	}

	/**
	 * Returns the value of the job parameter matching the given item name in the given
	 * group of the given entity type.
	 * 
	 * @param type
	 * @param parentGroupName
	 * @param itemName
	 * @return
	 */
	public String getJobParameterValue(EntityType type, String itemName) {
		List<ParameterReferenceGroup> groups = this.getParameters().get(type);
		for (ParameterReferenceGroup g : groups) {
				String internalName = getInternalName(g, itemName);
				if (internalName != null) {
					return internalName;
			}
		}
		return null;
	}
	
	/**
	 * Returns the value of the job parameter matching the given item name in the given
	 * group of the given entity type.
	 * 
	 * @param type
	 * @param parentGroupName
	 * @param itemName
	 * @return
	 */
	public String getRuleParameterValue(EntityType type, String itemName) {
		List<ParameterReferenceGroup> groups = this.getRules(type);
		for (ParameterReferenceGroup g : groups) {
				String internalName = getInternalName(g, itemName);
				if (internalName != null) {
					return internalName;
			}
		}
		return null;
	}
	
	private String getInternalName(ParameterReferenceGroup group, String itemName) {
		String parentGroup = ParameterReferenceHelper.getParentGroup(itemName);
		// only if the group name matches the root parent in the display name go further.
		if(!group.getName().equals(parentGroup))
			return null;

		// drop this as the parent and go to the next level.
		itemName = ParameterReferenceHelper.removeParentGroup(itemName);
		
		for (ParameterReferenceItem item : group.getItems()) {
			if (item instanceof ParameterReferenceGroup) {
				String internalName = getInternalName((ParameterReferenceGroup) item, itemName);
				if (internalName != null) {
					return internalName;
				}
			} else {
				if (item.getName().equals(itemName)) {
					return item.getValue();
				}
			}
		}
		return null;
	}

	/**
	 * Gets the job parameter for the internal name provided. The entity type is
	 * used to lookup the correct list of job parameters which will be matched.
	 * 
	 * @param type
	 * @param internalName
	 * @return
	 */
	public ParameterReferenceItem getJobParameter(EntityType type, String internalName) {
		List<ParameterReferenceGroup> groups = getParameters().get(type);
		for (ParameterReferenceGroup g : groups) {
			ParameterReferenceItem displayItem = getItem(g, internalName);
			if (displayItem != null) {
				return displayItem;
			}
		}
		return null;
	}
	
	/**
	 * Gets the job rule parameter for the internal name provided. The entity type is
	 * used to lookup the correct list of job rule parameters which will be matched.
	 * 
	 * @param type
	 * @param internalName
	 * @return
	 */
	public ParameterReferenceItem getJobRuleParameter(EntityType type, String internalName) {
		List<ParameterReferenceGroup> groups = getRules(type);
		for (ParameterReferenceGroup g : groups) {
			ParameterReferenceItem displayItem = getItem(g, internalName);
			if (displayItem != null) {
				return displayItem;
			}
		}
		return null;
	}
	
	private ParameterReferenceItem getItem(ParameterReferenceGroup group, String internalName) {
		for (ParameterReferenceItem item : group.getItems()) {
			if (item instanceof ParameterReferenceGroup) {
				ParameterReferenceItem displayItem = getItem(
						(ParameterReferenceGroup) item, internalName);
				if (displayItem != null) {
					return displayItem;
				}
			} else {
				if (item.getValue() != null && item.getValue().equals(internalName)) {
					return item;
				}
			}
		}
		return null;
	}	
}
