package org.greenstone.gsdl3.action;

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

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.greenstone.gsdl3.util.GSFile;
import org.greenstone.gsdl3.util.GSParams;
import org.greenstone.gsdl3.util.GSPath;
import org.greenstone.gsdl3.util.GSXML;
import org.greenstone.gsdl3.util.UserContext;
import org.greenstone.gsdl3.util.XMLConverter;
import org.greenstone.util.GlobalProperties;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class GeneralDocumentListAction extends Action
{

  /** process a request */
  public Node process(Node message_node)
  {
    Element message = GSXML.nodeToElement(message_node);
    Document doc = message.getOwnerDocument();
	    
    // the result
    Element result = doc.createElement(GSXML.MESSAGE_ELEM);
    Element page_response = doc.createElement(GSXML.RESPONSE_ELEM);
    result.appendChild(page_response);

    // assume only one request
    Element request = (Element) GSXML.getChildByTagName(message, GSXML.REQUEST_ELEM);
    //logger.debug(" request=" + this.converter.getString(request));

    UserContext userContext = new UserContext(request);

    // get the param list
    Element cgi_param_list = (Element) GSXML.getChildByTagName(request, GSXML.PARAM_ELEM + GSXML.LIST_MODIFIER);
    HashMap<String, Serializable> params = GSXML.extractParams(cgi_param_list, false);


    String service_name = (String) params.get(GSParams.SERVICE);
    String cluster_name = (String) params.get(GSParams.CLUSTER);
    String response_only_p = (String) params.get(GSParams.RESPONSE_ONLY);
    boolean response_only = false;
    if (response_only_p != null)
    {
      response_only = (response_only_p.equals("1") ? true : false);
    }
    String request_type = (String) params.get(GSParams.REQUEST_TYPE);
    // what is carried out depends on the request_type
    // if rt=d, then a describe request is done,
    // is rt=r, a request and then a describe request is done

    // if ro=1, then this calls for a process only page - we do the request
    // (rt should be r) and just give the response straight back
    // without any page processing

    // where to send requests
    String to;
    if (cluster_name != null)
    {
      to = GSPath.appendLink(cluster_name, service_name);
    }
    else
    {
      to = service_name;
    }

    // request the service info for the selected service - should be cached
    Element description_elem = getServiceDescription(to, userContext);

    if (request_type.equals(GSParams.REQUEST_TYPE_DESCRIBE))
    {

      // we only want to describe the service
      if (description_elem != null)
      {
        page_response.appendChild((Element) doc.importNode(description_elem, true));
      }
      addSiteMetadata(page_response, userContext);
      addInterfaceOptions(page_response);
      return result;
      // thats all. format info here??
    }
    
    //ok, so now we do the actual request
    Element mr_query_message = doc.createElement(GSXML.MESSAGE_ELEM);
    Element mr_query_request = GSXML.createBasicRequest(doc, GSXML.REQUEST_TYPE_PROCESS, to, userContext);
    
    mr_query_message.appendChild(mr_query_request);
    
    Element param_list = null;
    // add in the service params - except the ones only used by the action
    HashMap service_params = (HashMap) params.get(GSParams.SERVICE_PREFIX);
    if (service_params != null)
    {
      param_list = doc.createElement(GSXML.PARAM_ELEM + GSXML.LIST_MODIFIER);
      GSXML.addParametersToList(param_list, service_params);
      mr_query_request.appendChild(param_list);
    }

    Element mr_query_response = (Element) this.mr.process(mr_query_message);
    Element result_response = (Element) GSXML.getChildByTagName(mr_query_response, GSXML.RESPONSE_ELEM);

    if (result_response == null) {
      // something went wrong
      logger.error("no result from sending request to "+to);
      return result;
    }

    NodeList doc_nodes = result_response.getElementsByTagName(GSXML.DOC_NODE_ELEM);
    if (doc_nodes.getLength() == 0) {
      // no docs in the result
      return result;
    }

    // lets get format info for each collection
    HashSet<String> collections = getCollectionNames(doc_nodes);

    //get the format info for the service
    Element base_format_elem = getFormatInfo(to, userContext);
    // now get any from the collections, and combine into a big if/else block
    Element combined_format = getAndCombineFormats(doc, base_format_elem, collections, service_name);

    if (combined_format != null) {
      // set the format type - what would it be for this??
      //format_elem.setAttribute(GSXML.TYPE_ATT, "");
      // add to the response
      page_response.appendChild(combined_format);
    }

    // we need to get metadata for the documents
    // which metadata elements do we need?
    HashSet<String> doc_meta_names = new HashSet<String>();
    // always want this one
    doc_meta_names.add("root_assocfilepath");
    // we use extraMetadataList, plus anything that shows up in the service's format statement

    // ... and find any metadata names mentioned in the format, or in extraMetadata
    getRequiredMetadataNames(doc_meta_names, combined_format, request);

    // TODO, do we want docType and nodeType attributes for each doc Node??
    getAndAddMetadataToDocNodes(result_response, doc_meta_names);
    
    if (response_only)
    {
      // just send the reponse as is
      addSiteMetadata(result_response, userContext);
      addInterfaceOptions(result_response);
      return result_response;
    }
    
    // else append the contents of the response to the page 
    GSXML.copyAllChildren(page_response, result_response);
    
    // another part of the page is the service description
    if (description_elem != null)
    {
      page_response.appendChild((Element) doc.importNode(description_elem, true));
    }

    addSiteMetadata(page_response, userContext);
    addInterfaceOptions(page_response);

    return result;
  }

  protected HashSet<String> getCollectionNames(NodeList docNodes) {
    HashSet<String> coll_names = new HashSet<String>();
    for (int i=0; i<docNodes.getLength(); i++) {
      String collection = ((Element)docNodes.item(i)).getAttribute(GSXML.COLLECTION_ATT);
      if (!collection.equals("")) {
        coll_names.add(collection);
      }
    }
    return coll_names;
  }

  // TODO - store this so don't need to get it each time.
  protected Element getAndCombineFormats(Document result_doc, Element base_format, HashSet<String> collections, String service_name)
  {
    Element combined_format = result_doc.createElement(GSXML.FORMAT_ELEM);

    // lets see if the collections are returning any local formats
    Document doc = XMLConverter.newDOM();
    Element message = doc.createElement(GSXML.MESSAGE_ELEM);

    // service_name is an extra attribute
    for (String coll : collections) { 
      Element this_coll_request = GSXML.createBasicRequest(doc, GSXML.REQUEST_TYPE_FORMAT, coll, new UserContext());
      this_coll_request.setAttribute(GSXML.SERVICE_ATT, service_name);
      message.appendChild(this_coll_request);
    }

    // template match:mode is the key->collection->template element
    HashMap<String, HashMap<String,Element>> template_map = new HashMap<String, HashMap<String,Element>>();
    if (base_format != null) {
      // to do - process the defaults differently. any non match tempaltes need to be added as is into hte result, and the options.
      addFormatsToMap(template_map, base_format, "default_format", combined_format);
    }

    //send off the request
    boolean has_coll_specific_format = false;
    Element mr_response = (Element)this.mr.process(message);
    // add all the resulting templates into the map
    NodeList colls_responses = mr_response.getElementsByTagName(GSXML.RESPONSE_ELEM);
    for (int i=0; i<colls_responses.getLength(); i++) {
      Element coll_response = (Element)colls_responses.item(i);
      Element format = (Element)GSXML.getChildByTagName(coll_response, GSXML.FORMAT_ELEM);
      if (format == null) continue;
      String this_coll = coll_response.getAttribute(GSXML.FROM_ATT);
      addFormatsToMap(template_map, format, this_coll, null);
    }

    // ok, now we need to go through the map, and join them up into a big <xsl:choose>
    for(Map.Entry<String, HashMap<String, Element>> entry : template_map.entrySet()) {
      String id = entry.getKey();
      HashMap<String, Element> coll_map = entry.getValue();

      Element new_template = result_doc.createElementNS("http://www.w3.org/1999/XSL/Transform", "xsl:template");
      String[] id_parts = id.split(":");
      String match = id_parts[0];
      String mode = "";
      if (id_parts.length==2) {
        mode = id_parts[1];
      }
      new_template.setAttribute("match", match);
      if (!mode.equals("")) {
        new_template.setAttribute("mode", mode);
      }
      if (coll_map.size()==1 && coll_map.containsKey("default_format")) {
        // we only have the default format - can copy it as is
        new_template = (Element)result_doc.importNode(coll_map.get("default_format"), true);

      } else if (coll_map.size()==1) {
        // this is an error
        logger.error("no default entry for xsl template!"); // todo better message
      } else {
        Element choose = result_doc.createElementNS("http://www.w3.org/1999/XSL/Transform", "xsl:choose");
        new_template.appendChild(choose);
        Element otherwise = null;
        for (Map.Entry<String, Element> entry2 : coll_map.entrySet()) {
          String coll = entry2.getKey();
          Element template = entry2.getValue();
          if (coll.equals("default_format")) {
            // this is the otherwsie clause
            otherwise = GSXML.duplicateWithNewNameNS(result_doc, template, "xsl:otherwise","http://www.w3.org/1999/XSL/Transform", false);
            
          } else {
            // this is a when statement
            Element when = GSXML.duplicateWithNewNameNS(result_doc, template, "xsl:when","http://www.w3.org/1999/XSL/Transform", false);
            when.setAttribute("test", "@collection='"+coll+"'");
            choose.appendChild(when);
        }
          if (otherwise != null) {
            // append otherwise last
            choose.appendChild(otherwise);
          }
          combined_format.appendChild(new_template);
        
        }
        
      } // for each entry in template map
    }
    return combined_format;
  }
  protected void addFormatsToMap(HashMap<String, HashMap<String, Element>> template_map, Element format_element, String coll_name, Element combined_format) {
    Document doc = null;
    if (combined_format != null) {
      doc = combined_format.getOwnerDocument();
    }
    NodeList children = format_element.getChildNodes();
    for (int j=0; j<children.getLength(); j++) {
      Node this_child = children.item(j);
      if (this_child instanceof Element) { // lets ignore any other kind of node
        if (this_child.getNodeName().equals("xsl:template")) {
          Element this_template = (Element)this_child;
          String match = this_template.getAttribute("match");
          if (match.equals("")) {
            if (combined_format != null) {
              combined_format.appendChild(doc.importNode(this_template,true));
            }
          } else {
            String mode = this_template.getAttribute("mode");
            String id = match+":"+mode;
            HashMap<String, Element> coll_map = template_map.get(id);
            if (coll_map == null) {
              coll_map = new HashMap<String, Element>();
              template_map.put(id, coll_map);
            }
            coll_map.put(coll_name, this_template);
          }
        } else {
        // the node is not xsl:template, add it to combined format, if that has been supplied.
          if (combined_format != null) {
            combined_format.appendChild(doc.importNode(this_child, true));
          }
        }
      }// if isa Element
    } // for each node
 

  }

  protected void getAndAddMetadataToDocNodes(Element result_elem, HashSet<String> meta_names) {
    
    Document doc = XMLConverter.newDOM();
    Document result_doc = result_elem.getOwnerDocument();
    HashMap<String, Element>coll_requests_map = new HashMap<String, Element>();
    
    Element message = doc.createElement(GSXML.MESSAGE_ELEM);
    
    // the param list - will be the same for all collection requests
    Element dm_param_list = createMetadataParamList(doc,meta_names);
  
    NodeList doc_nodes = result_elem.getElementsByTagName(GSXML.DOC_NODE_ELEM);
    for (int i = 0; i < doc_nodes.getLength(); i++) {
      Element dn = (Element) doc_nodes.item(i);
      String collection = dn.getAttribute(GSXML.COLLECTION_ATT);

      Element this_coll_dn_list = coll_requests_map.get(collection);
      if (this_coll_dn_list == null) {
        Element this_coll_request = GSXML.createBasicRequest(doc, GSXML.REQUEST_TYPE_PROCESS, GSPath.appendLink(collection, "DocumentMetadataRetrieve"), new UserContext());
        this_coll_request.appendChild(dm_param_list.cloneNode(true));
        this_coll_dn_list = doc.createElement(GSXML.DOC_NODE_ELEM+GSXML.LIST_MODIFIER);
        this_coll_request.appendChild(this_coll_dn_list);
        coll_requests_map.put(collection, this_coll_dn_list);
        message.appendChild(this_coll_request);
      }

      this_coll_dn_list.appendChild(doc.importNode(dn, false));
    } // for each node

    // ok, send off the message
    Element dm_result = (Element)this.mr.process(message);
    String [] att_names = {GSXML.COLLECTION_ATT, GSXML.NODE_ID_ATT};
    // go throught he original list of doc nodes
    for (int i = 0; i < doc_nodes.getLength(); i++) {
      Element dn = (Element) doc_nodes.item(i);
      String collection = dn.getAttribute(GSXML.COLLECTION_ATT);
      String node_id = dn.getAttribute(GSXML.NODE_ID_ATT);
      // find the corresponding one in the result
      Element new_dn = GSXML.findMatchingElement(dm_result, GSXML.DOC_NODE_ELEM, att_names, new String[] {collection, node_id});
      if (new_dn != null) {
        Node new_meta = GSXML.getChildByTagName(new_dn, GSXML.METADATA_ELEM+GSXML.LIST_MODIFIER);
        if (new_meta != null) {
          dn.appendChild(result_doc.importNode(new_meta, true));
        }
      }
    }
  }
}
