package org.greenstone.gsdl3.action;

import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;

import org.apache.log4j.Logger;
import org.greenstone.gsdl3.util.GSParams;
import org.greenstone.gsdl3.util.GSPath;
import org.greenstone.gsdl3.util.GSXML;
import org.greenstone.gsdl3.util.GSXSLT;
import org.greenstone.gsdl3.util.OID;
import org.greenstone.gsdl3.util.UserContext;
import org.greenstone.gsdl3.util.XMLConverter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/** action for GS2 style classifier browsing */
public class GS2BrowseAction extends Action
{

	public static final String CLASSIFIER_ARG = "cl";

	public static final String DESCENDANTS_ARG = "descendants";

	static Logger logger = Logger.getLogger(org.greenstone.gsdl3.action.GS2BrowseAction.class.getName());

	/** process the request */
	public Node process(Node message_node)
	{
		Element message = GSXML.nodeToElement(message_node);
	    Document doc = message.getOwnerDocument();
	    
		// get the request - assume only one
		Element request = (Element) GSXML.getChildByTagName(message, GSXML.REQUEST_ELEM);

		// the result
		Element result = doc.createElement(GSXML.MESSAGE_ELEM);
		Element response = classifierBrowse(request);
		result.appendChild(response);
		return result;
	}

	protected Element classifierBrowse(Element request)
	{
		Document doc = request.getOwnerDocument();
		Element page_response = doc.createElement(GSXML.RESPONSE_ELEM);

		// extract the params from the cgi-request, and check that we have a coll specified
		Element cgi_paramList = (Element) GSXML.getChildByTagName(request, GSXML.PARAM_ELEM + GSXML.LIST_MODIFIER);
		HashMap<String, Serializable> params = GSXML.extractParams(cgi_paramList, false);

		String service_name = (String) params.get(GSParams.SERVICE);
		String collection = (String) params.get(GSParams.COLLECTION);
		if (collection == null || collection.equals(""))
		{
			logger.error("classifierBrowse, need to specify a collection!");
			return page_response;
		}

		boolean get_descendants = false;
		String descendants_str = (String) params.get(DESCENDANTS_ARG);
		if (descendants_str != null && descendants_str.equals("1"))
		{
			get_descendants = true;
		}

		UserContext userContext = new UserContext(request);
		String to = GSPath.appendLink(collection, service_name);

		// the first part of the response is the service description 
		// for now get this again from the service. 
		// this should be cached somehow later on. 

		Element info_message = doc.createElement(GSXML.MESSAGE_ELEM);
		Element info_request = GSXML.createBasicRequest(doc, GSXML.REQUEST_TYPE_DESCRIBE, to, userContext);
		info_message.appendChild(info_request);

		// also get the format stuff now if there is some
		Element format_request = GSXML.createBasicRequest(doc, GSXML.REQUEST_TYPE_FORMAT, to, userContext);
		info_message.appendChild(format_request);
		// process the requests

		Element info_response = (Element) this.mr.process(info_message);

		// the two responses
		NodeList responses = info_response.getElementsByTagName(GSXML.RESPONSE_ELEM);
		Element service_response = (Element) responses.item(0);
		Element format_response = (Element) responses.item(1);

		Element service_description = (Element) GSXML.getChildByTagName(service_response, GSXML.SERVICE_ELEM);
		page_response.appendChild(doc.importNode(service_description, true));

		//append site metadata
		addSiteMetadata(page_response, userContext);
		addInterfaceOptions(page_response);

		// if rt=d, then we are just displaying the service
		String request_type = (String) params.get(GSParams.REQUEST_TYPE);
		if (request_type.equals("d"))
		{
			//return the page that we have so far
			return page_response;
		}

		// get the node that the user has clicked on
		String classifier_node = (String) params.get(CLASSIFIER_ARG);

		// if the node is not defined, return the page that we have so far
		if (classifier_node == null || classifier_node.equals(""))
		{
			return page_response;
		}

		// the id of the classifier is the top id of the selected node
		String top_id = OID.getTop(classifier_node);
		HashSet<String> doc_meta_names = new HashSet<String>();
		HashSet<String> class_meta_names = new HashSet<String>();
		// add in the defaults
		doc_meta_names.add("Title");
		doc_meta_names.add("root_assocfilepath");
		class_meta_names.add("Title");

		// add the format info into the response
		Element format_elem = (Element) GSXML.getChildByTagName(format_response, GSXML.FORMAT_ELEM);
                Element this_format = null;
		if (format_elem != null)
		{
                  // find the one for the classifier we are in
                  this_format = GSXML.getNamedElement(format_elem, GSXML.CLASSIFIER_ELEM, GSXML.NAME_ATT, top_id);
                  if (this_format == null)
                  {
                    this_format = (Element) GSXML.getChildByTagName(format_elem, GSXML.DEFAULT_ELEM);
                  }

                  if (this_format != null)
                  {
                    Element global_format_elem = (Element) GSXML.getChildByTagName(format_response, GSXML.GLOBAL_FORMAT_ELEM);
                    if (global_format_elem != null)
                    {
                      GSXSLT.mergeFormatElements(this_format, global_format_elem, false);
                    }
                    
                    Element new_format = GSXML.duplicateWithNewName(doc, this_format, GSXML.FORMAT_ELEM, false);
                    // set the format type
                    new_format.setAttribute(GSXML.TYPE_ATT, "browse");
                    
                    page_response.appendChild(new_format);
                    
                  }
				
			
                }
                extractMetadataNames(doc_meta_names, class_meta_names, this_format, request);

                // find out if this classifier is horizontal at top
		Element class_list = (Element) GSXML.getChildByTagName(service_description, GSXML.CLASSIFIER_ELEM + GSXML.LIST_MODIFIER);
		Element this_classifier = GSXML.getNamedElement(class_list, GSXML.CLASSIFIER_ELEM, GSXML.NAME_ATT, top_id);

		// get the browse structure for the selected node
		Element classify_message = doc.createElement(GSXML.MESSAGE_ELEM);
		Element classify_request = GSXML.createBasicRequest(doc, GSXML.REQUEST_TYPE_PROCESS, to, userContext);
		classify_message.appendChild(classify_request);

		//Create a parameter list to specify the required structure information
		// for now, always get ancestors and children
		Element param_list = doc.createElement(GSXML.PARAM_ELEM + GSXML.LIST_MODIFIER);
		classify_request.appendChild(param_list);
		Element param = doc.createElement(GSXML.PARAM_ELEM);
		param_list.appendChild(param);
		param.setAttribute(GSXML.NAME_ATT, "structure");
		param.setAttribute(GSXML.VALUE_ATT, "ancestors");
		param = doc.createElement(GSXML.PARAM_ELEM);
		param_list.appendChild(param);
		param.setAttribute(GSXML.NAME_ATT, "structure");
		param.setAttribute(GSXML.VALUE_ATT, "children");

		if (get_descendants)
		{
			param = doc.createElement(GSXML.PARAM_ELEM);
			param_list.appendChild(param);
			param.setAttribute(GSXML.NAME_ATT, "structure");
			param.setAttribute(GSXML.VALUE_ATT, "descendants");
		}


		// put the classifier node into a classifier node list
		Element classifier_list = doc.createElement(GSXML.CLASS_NODE_ELEM + GSXML.LIST_MODIFIER);
		Element classifier = doc.createElement(GSXML.CLASS_NODE_ELEM);
		classifier.setAttribute(GSXML.NODE_ID_ATT, classifier_node);
		classifier_list.appendChild(classifier);
		classify_request.appendChild(classifier_list);

		// process the request
		Element classify_response = (Element) this.mr.process(classify_message);

		String path = GSPath.appendLink(GSXML.RESPONSE_ELEM, GSXML.CLASS_NODE_ELEM + GSXML.LIST_MODIFIER);
		Element class_node_list = (Element) GSXML.getNodeByPath(classify_response, path);

		path = GSPath.appendLink(GSXML.CLASS_NODE_ELEM, GSXML.NODE_STRUCTURE_ELEM);
		// assume that we always get back the top level CL1 node - this becomes the page_classifier node
		path = GSPath.appendLink(path, GSXML.CLASS_NODE_ELEM);
		Element cl_structure = (Element) GSXML.getNodeByPath(class_node_list, path);
		if (cl_structure == null)
		{
			logger.error("classifier structure request returned no structure");
			return page_response;
		}
		Document cl_struct_doc = cl_structure.getOwnerDocument();

		//If the user is viewing a horizontal classifier then we need to get extra information - siblings along the horizontal

		// cl_structure is the classifier structure containing ancestors and children of current node.

		//If we have a horizontal classifier and we have had the top-level node requested (e.g. CL1, CL2 etc.)
		//then we want to get the children of the first classifier node (e.g. the children of CL2.1) 
		if (OID.isTop(classifier_node) && cl_structure.getAttribute(GSXML.CHILD_TYPE_ATT).equals(GSXML.HLIST)) {
		    boolean firstChildIsClassifierNode = false;
		    NodeList classifierChildrenNodes = GSXML.getChildrenByTagName(cl_structure, GSXML.CLASS_NODE_ELEM);
		    for (int i = 0; i < classifierChildrenNodes.getLength(); i++)
			{
			    Element currentChild = (Element) classifierChildrenNodes.item(i);
			    if (currentChild.getAttribute(GSXML.NODE_ID_ATT).endsWith(".1"))
				{
				    firstChildIsClassifierNode = true;
				}
			}

		    if (firstChildIsClassifierNode)
			{
			    // get the children of the first child
			    Element childStructure = getClassifierChildrenFromID(doc, classifier_node + ".1", request, collection, service_name);

			    // and now insert it into the structure we already have
			    NodeList nodesToSearch = cl_structure.getElementsByTagName(GSXML.CLASS_NODE_ELEM);
			    for (int i = 0; i < nodesToSearch.getLength(); i++)
				{
				    Element currentElem = (Element) nodesToSearch.item(i);
				    if (currentElem.getAttribute(GSXML.NODE_ID_ATT).equals(classifier_node + ".1"))
					{
					    Element parent = (Element) currentElem.getParentNode();
					    parent.insertBefore(cl_struct_doc.importNode(childStructure, true), currentElem); 
					    parent.removeChild(currentElem);
					    break;
					}
				}
			}
		} // if OID.isTop(classifier_node) && we are an HList
		//If we have a horizontal classifier and we have NOT had the top-level node requested then we need to 
		//make sure we get the full list of top-level children to display (e.g. if the user has requested
		//CL2.1.1 we also need to make sure we have CL2.2, CL2.3, CL2.4 etc.)
		else
		    {
			// we need to get siblings of any HList nodes so the HLists remain expanded in the display
			// start at the bottom (the specified element) and work up teh list of parents expanding any that have hlists
			NodeList elems = GSXML.getNamedElements(cl_structure, GSXML.CLASS_NODE_ELEM, GSXML.NODE_ID_ATT, classifier_node);
			Element current_node = null;
			String current_id = classifier_node;
			if (elems != null) {
			    // there should only be one node in the list
			    current_node = (Element)elems.item(0);
			}
			if (current_node != null) {
			    if (current_node.getAttribute(GSXML.CHILD_TYPE_ATT).equals(GSXML.HLIST)) {

		                boolean firstChildIsClassifierNode = false;
				NodeList classifierChildrenNodes = GSXML.getChildrenByTagName(current_node, GSXML.CLASS_NODE_ELEM);
				for (int i = 0; i < classifierChildrenNodes.getLength(); i++)
				    {

					Element currentChild = (Element) classifierChildrenNodes.item(i);
				
					if (currentChild.getAttribute(GSXML.NODE_ID_ATT).endsWith(".1"))
					    {
						firstChildIsClassifierNode = true;
						break;
					    }
				    }

				if (firstChildIsClassifierNode)
				    {

					// get the children of the first child
					Element childStructure = getClassifierChildrenFromID(doc, classifier_node + ".1", request, collection, service_name);

					// and now insert it into the structure we already have
					NodeList nodesToSearch = cl_structure.getElementsByTagName(GSXML.CLASS_NODE_ELEM);
					for (int i = 0; i < nodesToSearch.getLength(); i++)
					    {
						Element currentElem = (Element) nodesToSearch.item(i);
						if (currentElem.getAttribute(GSXML.NODE_ID_ATT).equals(classifier_node + ".1"))
						    {
							Element parent = (Element) currentElem.getParentNode();
							parent.insertBefore(cl_struct_doc.importNode(childStructure, true), currentElem); 
							parent.removeChild(currentElem);
							break;
						    }
					    }
				    }
				//**
			    } // current_node is an HList
			    Element parent_node = (Element)current_node.getParentNode();
			    while(parent_node != null && parent_node.getNodeName().equals(GSXML.CLASS_NODE_ELEM)) {
		
				if (((Element)parent_node).getAttribute(GSXML.CHILD_TYPE_ATT).equals(GSXML.HLIST)) {
				    // get the children of the parent
				    Element new_parent_elem = getClassifierChildrenFromID(cl_struct_doc, parent_node.getAttribute(GSXML.NODE_ID_ATT), request, collection, service_name);

				    // put current node into this new structure
				    Element to_be_replaced = GSXML.getNamedElement(new_parent_elem, GSXML.CLASS_NODE_ELEM, GSXML.NODE_ID_ATT, current_id);
				    if (to_be_replaced != null) {
					new_parent_elem.insertBefore(current_node, to_be_replaced);
					new_parent_elem.removeChild(to_be_replaced);
					
					
					if (OID.isTop(parent_node.getAttribute(GSXML.NODE_ID_ATT))) {
					    
					    // we can't go up any further.
					    cl_structure = new_parent_elem;
					    parent_node = null;
					    
					} else {
					    Element next_parent = (Element)parent_node.getParentNode();
					    next_parent.insertBefore(new_parent_elem, parent_node );
					    next_parent.removeChild(parent_node);
					    current_node = new_parent_elem;
					    current_id = current_node.getAttribute(GSXML.NODE_ID_ATT);
					    parent_node = next_parent;
					}
				    } else {
					// something went wrong, we'll stop this
					parent_node = null;
				    }
				} // end of if HList
				else {
				    current_node = parent_node;
				    current_id = current_node.getAttribute(GSXML.NODE_ID_ATT);
				    parent_node = (Element)current_node.getParentNode();
				}

			    } // while parent != null
			} // if current_node != null
		    } // end of else
	

		  Element page_classifier = null;
		// add the single classifier node as the page classifier 
		page_classifier = GSXML.duplicateWithNewName(doc, cl_structure, GSXML.CLASSIFIER_ELEM, true);
		page_response.appendChild(page_classifier);
		page_classifier.setAttribute(GSXML.NAME_ATT, top_id);

		// get the metadata for each classifier node, 
		// then for each document node

		Element metadata_message = doc.createElement(GSXML.MESSAGE_ELEM);

		boolean did_classifier = false;
		boolean did_documents = false;

		// if there are classifier nodes 
		// create a metadata request for the classifier, and add it to 
		// the the message
		NodeList cl_nodes = page_classifier.getElementsByTagName(GSXML.CLASS_NODE_ELEM);

		if (cl_nodes.getLength() > 0)
		{
			did_classifier = true;
			Element cl_meta_request = GSXML.createBasicRequest(doc, GSXML.REQUEST_TYPE_PROCESS, to + "MetadataRetrieve", userContext);
			metadata_message.appendChild(cl_meta_request);

			Element new_cl_nodes_list = doc.createElement(GSXML.CLASS_NODE_ELEM + GSXML.LIST_MODIFIER);
			cl_meta_request.appendChild(new_cl_nodes_list);

			for (int c = 0; c < cl_nodes.getLength(); c++)
			{

				Element cl = doc.createElement(GSXML.CLASS_NODE_ELEM);
				cl.setAttribute(GSXML.NODE_ID_ATT, ((Element) cl_nodes.item(c)).getAttribute(GSXML.NODE_ID_ATT));
				new_cl_nodes_list.appendChild(cl);
			}

			// create and add in the param list - for now get all the metadata
			// should be based on info sent in from the recept, and the 
			// format stuff
			Element cl_param_list = createMetadataParamList(doc,class_meta_names);
			cl_meta_request.appendChild(cl_param_list);

		}

		// if there are document nodes in the classification (happens 
		// sometimes), create a second request for document metadata and 
		// append to the message
		NodeList doc_nodes = page_classifier.getElementsByTagName(GSXML.DOC_NODE_ELEM);
		if (doc_nodes.getLength() > 0)
		{
			did_documents = true;
			Element doc_meta_request = GSXML.createBasicRequest(doc, GSXML.REQUEST_TYPE_PROCESS, GSPath.appendLink(collection, "DocumentMetadataRetrieve"), userContext);
			metadata_message.appendChild(doc_meta_request);

			Element doc_list = doc.createElement(GSXML.DOC_NODE_ELEM + GSXML.LIST_MODIFIER);
			doc_meta_request.appendChild(doc_list);

			for (int c = 0; c < doc_nodes.getLength(); c++)
			{

				Element d = doc.createElement(GSXML.DOC_NODE_ELEM);
				d.setAttribute(GSXML.NODE_ID_ATT, ((Element) doc_nodes.item(c)).getAttribute(GSXML.NODE_ID_ATT));
				doc_list.appendChild(d);
			}

			// create and add in the param list - add all for now
			Element doc_param_list = createMetadataParamList(doc,doc_meta_names);
			doc_meta_request.appendChild(doc_param_list);

		}

		// process the metadata requests
		Element metadata_response = (Element) this.mr.process(metadata_message);
		if (did_classifier)
		{
			// the classifier one will be the first response
			// add the metadata lists for each node back into the 
			// page_classifier nodes
			path = GSPath.appendLink(GSXML.RESPONSE_ELEM, GSXML.CLASS_NODE_ELEM + GSXML.LIST_MODIFIER);
			Node pathNode = GSXML.getNodeByPath(metadata_response, path);
			if (pathNode == null)
			{
				return page_response;
			}
			//NodeList meta_response_cls = (Element)pathNode.getChildNodes(); // can't handle empty elements from converting formatted strings (with empty newlines) into XML
			NodeList meta_response_cls = ((Element) pathNode).getElementsByTagName(GSXML.CLASS_NODE_ELEM);
			for (int i = 0; i < cl_nodes.getLength(); i++)
			{
				GSXML.mergeMetadataLists(cl_nodes.item(i), meta_response_cls.item(i));
			}
		}

		if (did_documents)
		{
			NodeList meta_response_docs = null;
			if (!did_classifier)
			{
				// its the first response
				path = GSPath.appendLink(GSXML.RESPONSE_ELEM, GSXML.DOC_NODE_ELEM + GSXML.LIST_MODIFIER);
				Node pathNode = GSXML.getNodeByPath(metadata_response, path);
				if (pathNode == null)
				{
					return page_response;
				}

				meta_response_docs = pathNode.getChildNodes();

			}
			else
			{ // its the second response
				Node nodes = GSXML.getChildByTagName(metadata_response.getElementsByTagName(GSXML.RESPONSE_ELEM).item(1), GSXML.DOC_NODE_ELEM + GSXML.LIST_MODIFIER);
				if (nodes == null)
				{
					return page_response;
				}
				meta_response_docs = nodes.getChildNodes();
			}

			for (int i = 0; i < doc_nodes.getLength(); i++)
			{
				GSXML.mergeMetadataLists(doc_nodes.item(i), meta_response_docs.item(i));
			}
		}

		//logger.debug("(GS2BrowseAction) Page:\n" + this.converter.getPrettyString(page_response));
		return page_response;
		    }

	private Element getClassifierChildrenFromID(Document doc, String id, Element request, String collection, String service_name)
	{
		UserContext userContext = new UserContext(request);
		String to = GSPath.appendLink(collection, service_name);

		Document msg_doc = XMLConverter.newDOM();
		Element firstClassifierNodeChildrenMessage = msg_doc.createElement(GSXML.MESSAGE_ELEM);
		Element firstClassifierNodeChildrenRequest = GSXML.createBasicRequest(msg_doc, GSXML.REQUEST_TYPE_PROCESS, to, userContext);
		firstClassifierNodeChildrenMessage.appendChild(firstClassifierNodeChildrenRequest);

		Element paramList = msg_doc.createElement(GSXML.PARAM_ELEM + GSXML.LIST_MODIFIER);
		firstClassifierNodeChildrenRequest.appendChild(paramList);

		// Element ancestorParam = doc.createElement(GSXML.PARAM_ELEM);
		// paramList.appendChild(ancestorParam);
		// ancestorParam.setAttribute(GSXML.NAME_ATT, "structure");
		// ancestorParam.setAttribute(GSXML.VALUE_ATT, "ancestors");

		Element childrenParam = msg_doc.createElement(GSXML.PARAM_ELEM);
		paramList.appendChild(childrenParam);
		childrenParam.setAttribute(GSXML.NAME_ATT, "structure");
		childrenParam.setAttribute(GSXML.VALUE_ATT, "children");

		Element classifierToGetList = msg_doc.createElement(GSXML.CLASS_NODE_ELEM + GSXML.LIST_MODIFIER);
		Element classifierToGet = msg_doc.createElement(GSXML.CLASS_NODE_ELEM);
		classifierToGet.setAttribute(GSXML.NODE_ID_ATT, id);
		classifierToGetList.appendChild(classifierToGet);
		firstClassifierNodeChildrenRequest.appendChild(classifierToGetList);

		Element firstClassifierNodeChildrenResponse = (Element) this.mr.process(firstClassifierNodeChildrenMessage);

		String nsPath = GSPath.appendLink(GSXML.RESPONSE_ELEM, GSXML.CLASS_NODE_ELEM + GSXML.LIST_MODIFIER);
		Element topClassifierNode = (Element) GSXML.getNodeByPath(firstClassifierNodeChildrenResponse, nsPath);
		nsPath = GSPath.appendLink(GSXML.CLASS_NODE_ELEM, GSXML.NODE_STRUCTURE_ELEM);
		nsPath = GSPath.appendLink(nsPath, GSXML.CLASS_NODE_ELEM);
		Element childStructure = (Element) GSXML.getNodeByPath(topClassifierNode, nsPath);
		return (Element)doc.importNode(childStructure, true);
	}

  protected void extractMetadataNames(HashSet<String> doc_meta_names, HashSet<String> class_meta_names, Element new_format, Element request)
	{
          // for doc names, add the ones from extraMetadata
          getRequiredMetadataNames(doc_meta_names, null, request);
	if (new_format == null) return;          
	NodeList templates = new_format.getElementsByTagNameNS(GSXML.GSF_NAMESPACE, "template");
		for (int i = 0; i < templates.getLength(); i++)
		{
			Element template = (Element) templates.item(i);
			String match = template.getAttribute(GSXML.MATCH_ATT);
			String name = template.getAttribute(GSXML.NAME_ATT);
			if (match != null && match.length() > 0)
			{
				if (match.startsWith("documentNode"))
				{
                                  getRequiredMetadataNames(doc_meta_names, template, null);
				}
				else if (match.startsWith("classifierNode")) // not match.equals, as we want to match nodes like: classifierNode[@classifierStyle = 'VList']
				{
                                  getRequiredMetadataNames(class_meta_names, template, null);
				}
			}
			else
			{
                          getRequiredMetadataNames(doc_meta_names, template, null);
                          getRequiredMetadataNames(class_meta_names, template, null);
			}
		}
	}

}
