/*
Copyright (C) 2000-2010  Ministere de la culture et de la communication (France), AJLSM
See LICENCE file
*/
package fr.gouv.culture.oai;

import fr.gouv.culture.oai.util.OAIUtilities;
import fr.gouv.culture.sdx.utils.Utilities;

import org.apache.cocoon.ProcessingException;
import org.apache.excalibur.source.impl.FileSource;
import org.apache.cocoon.environment.Request;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.apache.cocoon.components.source.SourceUtil;

import java.util.*;
import java.io.IOException;

/**This class represents an OAI repository.
 */
public abstract class AbstractOAIRepository extends OAIObjectImpl implements OAIRepository {

    /**Email address of the administrator of this OAI Repository*/
    protected String[] adminEmails = null;

    /**String value of the version number of the oai protocol supported in this */
    protected String protocolVersion = "";

    /**A human readable name for this repository*/
    protected String repositoryName = "";

    /**The base url of this repository*/
    protected String baseURL = "";

    /**The granularity of datestamps supported by this repository*/
    protected String granularity = "";

    /**String value of yes/no indicating if deleted records are supported
     *
     *deletedRecord : the manner in which the repository supports the notion
     * of deleted records . Legitimate values are no ; transient ; persistent
     * with meanings defined in the section on deletion (from oai spec) .
     *
     */
    protected String deletedRecord = "";

    /**String value indicating the earliest _datestamp of this repository*/
    protected String earliestDatestamp = "";

    /**Compression supported by this OAI repository - optional as per OAI spec*/
    protected String compression = "";

    /**Description of this repository  - optional as per OAI spec*/
    protected FileSource description = null;

    /**Hashtable containing supported OAIMetadataFormat objects*/
    protected Hashtable metadataFormats = null;


    /**Sends SAX events for the OAI-PMH "identify" request
     *
     * @param request    The request
     * @throws SAXException
     */
    public void identify(OAIRequest request) throws SAXException {
        //Arguments accepted : None
        this.startVerbEvent(request);

        String oai20xmlns = OAIObject.Node.Xmlns.OAI_2_0;
        String locQualName = "";


        //elements in sequence for identify (see oai schema)

        if (OAIUtilities.checkString(this.repositoryName)) {
            locQualName = OAIObject.Node.Name.REPOSITORY_NAME;
            sendElement(oai20xmlns, locQualName, locQualName, null, this.repositoryName);
        }

        if (OAIUtilities.checkString(this.baseURL)) {
            locQualName = OAIObject.Node.Name.BASE_URL;
            sendElement(oai20xmlns, locQualName, locQualName, null, this.baseURL);
        }

        if (OAIUtilities.checkString(this.protocolVersion)) {
            locQualName = OAIObject.Node.Name.PROTOCOL_VERSION;
            sendElement(oai20xmlns, locQualName, locQualName, null, this.protocolVersion);
        }

        for (int i = 0; i < adminEmails.length; i++) {
            String s = adminEmails[i];
            if (OAIUtilities.checkString(s)) {
                locQualName = OAIObject.Node.Name.ADMIN_EMAIL;
                sendElement(oai20xmlns, locQualName, locQualName, null, s);
            }
        }

        String eds = getEarliestDatestamp();
        if (OAIUtilities.checkString(eds)) {
            locQualName = OAIObject.Node.Name.EARLIEST_DATESTAMP;
            sendElement(oai20xmlns, locQualName, locQualName, null, eds);
        }

        if (OAIUtilities.checkString(this.deletedRecord)) {
            locQualName = OAIObject.Node.Name.DELETED_RECORD;
            sendElement(oai20xmlns, locQualName, locQualName, null, this.deletedRecord);
        }

        //A repository that supports YYYY-MM-DDThh:mm:ssZ should indicate so in the Identify response
        if (OAIUtilities.checkString(this.granularity)) {
            locQualName = OAIObject.Node.Name.GRANULARITY;
            sendElement(oai20xmlns, locQualName, locQualName, null, this.granularity);
        }

        if (OAIUtilities.checkString(this.compression)) {
            locQualName = OAIObject.Node.Name.COMPRESSION;
            sendElement(oai20xmlns, locQualName, locQualName, null, this.compression);
        }

        if (this.description != null) {
            locQualName = OAIObject.Node.Name.DESCRIPTION;
            startElement(oai20xmlns, locQualName, locQualName, new AttributesImpl());
            try {
				SourceUtil.toSAX(this.description, super.xmlConsumer);
                //this.description.toSAX(super.xmlConsumer);
            } catch (ProcessingException e) {
                throw new SAXException(e.getMessage(), e);
			}
			catch (IOException e) {
				throw new SAXException(e.getMessage(), e);
            }
            endElement(oai20xmlns, locQualName, locQualName);

        }


        this.endVerbEvent(request);

    }

    /*One of the below is required by the OAI protocol when querying a repository*/
    public String getDeletedRecord() {
        return deletedRecord;
    }

    public String getGranularity() {
        return granularity;
    }

    public String getRepositoryName() {
        return repositoryName;
    }

    public String getEarliestDatestamp() {
        return earliestDatestamp;
    }

    public String getProtocolVersion() {
        return protocolVersion;
    }

    public String getBaseURL() {
        return baseURL;
    }

    public String[] getAdminEmails() {
        return adminEmails;
    }

    /*The below are optional and repeatable when responding to a repository query*/
    public String getCompression() {
        return compression;
    }

    /**Send's the description file to the current consumer
     *
     * @param handler
     * @throws SAXException
     */
    public void getDescription(ContentHandler handler) throws SAXException {
        try {
            if (this.description != null && handler != null)
				SourceUtil.toSAX(this.description, handler);
                //this.description.toSAX(handler);
        } catch (ProcessingException e) {
            throw new SAXException(e.getMessage(), e);
		}
		catch (IOException e) {
			throw new SAXException(e);
        }
    }


    /**Verifies the request parameters.
     *
     * <p>Verifies the request parameters respective of the
     * verb provided in the request and the repository's
     * level of support for certain optional features
     * like "resumptionToken"s and "set"s.</p>
     *
     * <p>Note this method should return SAX events to any
     * XMLConsumer set if errors exist within the request parameters.</p>
     *
     * @param request The request object
     * @return	Return <CODE>true</CODE> or <CODE>false</CODE>.
     * @throws SAXException
     */
    public boolean verifyParameters(OAIRequest request) throws SAXException {

    	boolean ret = true;

    	//TODO: Refactor allow this method to call overridable methods for it's valid parameter lists

    	if (request == null) return false;
    	Request httpRequest = request.getRequest();
    	if (httpRequest == null) return false;

    	int verb = request.getVerb();
    	HashSet requiredParamNames = new HashSet();
    	requiredParamNames.add(OAIRequest.URL_PARAM_NAME_VERB);
    	HashSet optionalParamNames = new HashSet();

    	// Check the verb and list the required argument(s)
    	switch (verb) {
    	case OAIRequest.VERB_IDENTIFY:
    		break;
    	case OAIRequest.VERB_LIST_SETS:
    		optionalParamNames.add(OAIRequest.URL_PARAM_NAME_RESUMPTION_TOKEN);
    		break;
    	case OAIRequest.VERB_LIST_METADATA_FORMATS:
    		optionalParamNames.add(OAIRequest.URL_PARAM_NAME_IDENTIFIER);
    		break;
    	case OAIRequest.VERB_LIST_IDENTIFIERS:
    		if (OAIUtilities.checkString(request.getResumptionToken()))//if resumptionToken then no other params are allowed
    			requiredParamNames.add(OAIRequest.URL_PARAM_NAME_RESUMPTION_TOKEN);
    		else {
    			requiredParamNames.add(OAIRequest.URL_PARAM_NAME_METADATA_PREFIX);
    			optionalParamNames.add(OAIRequest.URL_PARAM_NAME_SET);
    			optionalParamNames.add(OAIRequest.URL_PARAM_NAME_FROM);
    			optionalParamNames.add(OAIRequest.URL_PARAM_NAME_UNTIL);
    		}
    		break;
    	case OAIRequest.VERB_LIST_RECORDS:
    		if (OAIUtilities.checkString(request.getResumptionToken()))//if resumptionToken then no other params are allowed
    			requiredParamNames.add(OAIRequest.URL_PARAM_NAME_RESUMPTION_TOKEN);
    		else {
    			requiredParamNames.add(OAIRequest.URL_PARAM_NAME_METADATA_PREFIX);
    			optionalParamNames.add(OAIRequest.URL_PARAM_NAME_SET);
    			optionalParamNames.add(OAIRequest.URL_PARAM_NAME_FROM);
    			optionalParamNames.add(OAIRequest.URL_PARAM_NAME_UNTIL);
    		}
    		break;
    	case OAIRequest.VERB_GET_RECORD:
    		requiredParamNames.add(OAIRequest.URL_PARAM_NAME_IDENTIFIER);
    		requiredParamNames.add(OAIRequest.URL_PARAM_NAME_METADATA_PREFIX);
    		break;
    	default:
    	case OAIRequest.VERB_UNKNOWN:
    		break;
    	}


    	Enumeration paramsNamesEnum = httpRequest.getParameterNames();
    	ArrayList paramNames = null;
    	if (paramsNamesEnum != null) {
    		if (paramNames == null) paramNames = new ArrayList();
    		while (paramsNamesEnum.hasMoreElements()) {
    			String paramName = (String) paramsNamesEnum.nextElement();
    			paramName = OAIUtilities.normalizeHttpRequestParameterName(paramName);
    			paramNames.add(paramName);
    		}
    	}

    	// Check for required arguments
    	Iterator iter = requiredParamNames.iterator();
    	while (iter.hasNext()) {
    		String requiredParamName = (String) iter.next();
    		if (!paramNames.contains(requiredParamName)) {
    			if (requiredParamName.equals(OAIRequest.URL_PARAM_NAME_VERB)) {
    				new OAIError(OAIError.ERROR_BAD_VERB, "The request is missing a verb.").toSAX(this);
    			}
    			else {
    				new OAIError(OAIError.ERROR_BAD_ARGUMENT, "The request is missing the required argument, " + requiredParamName + ".").toSAX(this);
    			}
    		}
    	}
    	// Verifies that the parameter name/value pairs provided are valid for each type of request
    	String paramName = "", paramVal = "", mdPrefix = "", fromParam = "", untilParam = "";
    	String[] paramValues = null;
    	for (int i = 0; i < paramNames.size(); i++) {
    		paramName = (String) paramNames.get(i);
    		if (OAIUtilities.checkString(paramName)) {
    			if (!requiredParamNames.contains(paramName) & !optionalParamNames.contains(paramName)) {
					// The request contains argument that is not allowed for the current verb
    				new OAIError(OAIError.ERROR_BAD_ARGUMENT, "The request includes the illegal argument, " + paramName + ".").toSAX(this);
    				ret = false;
    			} else {
					//we have parameters arguments and want to ensure they have good values
    				paramValues = httpRequest.getParameterValues(paramName);
    				if (paramValues != null) {
    					if (paramValues.length > 0) {
    						if (paramValues.length > 1) {
    							// Duplicate argument
    							new OAIError(OAIError.ERROR_BAD_ARGUMENT, "The request includes the duplicate argument, " + paramName + ", only the first value provided, " + paramValues[i] + ", was used in processing this request.").toSAX(this);
    							ret = false;
    						}
    						paramVal = paramValues[0];
    						//verifying the actual value
    						if (!OAIUtilities.checkString(paramVal)) {
    							new OAIError(OAIError.ERROR_BAD_ARGUMENT, "The value of the parameter with the name, " + paramName + ", wall null or an empty String").toSAX(this);
    							ret = false;
    						}
    						//verifying that the metadataPrefix supplied is one of registered formats
    						else if (OAIRequest.URL_PARAM_NAME_METADATA_PREFIX.equals(paramName)) {
    							mdPrefix = paramVal;
    							if (!metadataFormats.containsKey(mdPrefix) && !OAIUtilities.checkString(request.getResumptionToken())) {
    								new OAIError(OAIError.ERROR_CANNOT_DISSEMINATE_FORMAT, "The metadata format identified by the value given for the metadataPrefix argument, " + mdPrefix + ", is not supported by the repository.").toSAX(this);
    								ret = false;
    							}
    						}
    						// check for dates
    						else if ( OAIRequest.URL_PARAM_NAME_FROM.equals(paramName) || OAIRequest.URL_PARAM_NAME_UNTIL.equals(paramName) ){
    							if ( OAIRequest.URL_PARAM_NAME_FROM.equals(paramName)
								)	fromParam = paramVal;
    							else if ( OAIRequest.URL_PARAM_NAME_UNTIL.equals(paramName)
								)	untilParam = paramVal;
    							 // check for granularity
    							if (!verifyGranularity(paramVal)) {
									// dates must be formated according ISO-8601 format : YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DD
    								new OAIError(OAIError.ERROR_BAD_ARGUMENT, "The value of the \"" + paramName + "\" argument, " + paramVal + ", is invalid. ").toSAX(this);
    								ret = false;
	    						}
    							if ( Utilities.checkString(fromParam) && Utilities.checkString(untilParam) ) {
    								// from and until params must have the same granularity : YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DD
    								if ( AbstractOAIRepository.getGranularity( fromParam ) != AbstractOAIRepository.getGranularity( untilParam ) ) {
    									new OAIError(OAIError.ERROR_BAD_ARGUMENT, "Both arguments \"from\" and \"until\" must have the same granularity.").toSAX(this);
    									ret = false;
    								}
    							}
    						}

    					}
    				}
    			}


    		}

    	}

    	return ret;
    }

//    /**Verifies values for the <code>from</code> and <code>until</code> request params*/
//    public abstract boolean verifyGranularity(String paramVal);
    /**	Verifies the granularity of a date in a OAI request.
	 * <p>
	 * The date must be formated according the <a href="http://www.w3.org/TR/NOTE-datetime">ISO-8601</a>
	 * format : YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DD.
	 *
	 * @param	paramVal	The date as a String.
	 * @return	Return <CODE>true</CODE> if the date is well formated as a complete date or complete date plus hours, minutes and seconds.
	 * @see #getGranularity(java.lang.String)
	 */
    public boolean verifyGranularity(String paramVal) {

    	if ( getGranularity( paramVal ) == null ) return false;
    	else return true;

    	// Parse the date was not sufficient : 2006-11-28T23 was correct in that case.
//        Date date = fr.gouv.culture.sdx.utils.Date.parseUtcISO8601DateDayOrSecond(paramVal);
//        if (date == null) return false;
//        return true;

    }

    /**	Return the granularity of a date in a OAI request.
	 * <p>
	 * The date must be formated according the <a href="http://www.w3.org/TR/NOTE-datetime">ISO-8601</a>
	 * format : YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DD.
	 *
	 * @param	paramVal	The date as a String
	 * @return	Return <CODE>YYYY-MM-DD</CODE> or
	 * <CODE>YYYY-MM-DDThh:mm:ssZ</CODE> if the date is well
	 * formated as a complete date or complete date plus hours, minutes and seconds.
	 * @see OAIObject.Node.Value#STRING_GRANULARITY_DAY
	 * @see OAIObject.Node.Value#STRING_GRANULARITY_SECOND
	 */
    public static String getGranularity(String paramVal) {

    	// Regular expressions to perform the control
    	String	dateDay		= "^[\\d]{4}-[\\d]{2}-[\\d]{2}$",
    			dateSecond	= "^[\\d]{4}-[\\d]{2}-[\\d]{2}T[\\d]{2}:[\\d]{2}:[\\d]{2}Z$";

    	if ( paramVal==null ) return null;
    	else if ( paramVal.matches( dateDay ) ) return OAIObject.Node.Value.STRING_GRANULARITY_DAY;
    	else if ( paramVal.matches( dateSecond ) ) return OAIObject.Node.Value.STRING_GRANULARITY_SECOND;
    	else return null;

    }

  /**Sends the oai error specifying that a set hierarchy is NOT supported by this oai repository*/
    protected void sendNoSetHierarchyError() throws SAXException {
        new OAIError(OAIError.ERROR_NO_SET_HIERARCHY, "This repository does not support set hierarchies").toSAX(this);

    }


    /**Sends the oai error specifying that resumptionTokens are NOT supported by this oai repository*/
    protected void sendResumptionTokensNotSupportedError() throws SAXException {
        new OAIError(OAIError.ERROR_BAD_RESUMPTION_TOKEN, "This repository does not support resumptionTokens").toSAX(this);
    }

    /**Sends the start event corresponding to the verb of the request*/
    protected void startVerbEvent(OAIRequest request) throws SAXException {
        //<GetRecord>
        String verb = request.getVerbString();
        if (OAIUtilities.checkString(verb)) {
            super.startElement(OAIObject.Node.Xmlns.OAI_2_0, verb, verb, null);
        }
    }

    /**Sends the end event corresponding to the verb of the request*/
    protected void endVerbEvent(OAIRequest request) throws SAXException {
        //</GetRecord>
        String verb = request.getVerbString();
        if (OAIUtilities.checkString(verb))
            super.endElement(OAIObject.Node.Xmlns.OAI_2_0, verb, verb);
    }

    /**Calls the identify method and sends the SAX flow to the
     * provided content handler
     *
     * @param contentHandler    The event receiver
     * @throws SAXException
     * @see #identify
     */
    public void toSAX(ContentHandler contentHandler) throws SAXException {
        ContentHandler currentHandler = super.contentHandler;
        this.setContentHandler(contentHandler);
        OAIRequest request = new OAIRequestImpl();
        request.setVerb(OAIRequest.VERB_IDENTIFY);
        this.identify(request);
        this.setContentHandler(currentHandler);

    }

    /**Sends the resumption token element to the consumer
    *
    * @param resumptionToken   The resumption token
    * @throws SAXException
    */
   public void sendResumptionToken(String resumptionToken) throws SAXException {
       sendResumptionToken(resumptionToken, null, null);
   }

    /**Sends the resumption token element to the consumer
     *
     * @param resumptionToken   The resumption token
     * @param cursor    The cursor position
     * @throws SAXException
     */
    public void sendResumptionToken(String resumptionToken, String cursor) throws SAXException {
        sendResumptionToken(resumptionToken, cursor, null);
    }

    /**Sends the resumption token element to the consumer
    *
    * @param resumptionToken   The resumption token
    * @param cursor    The cursor position
    * @param completeListSize    The size of the complete list
    * @throws SAXException
    */
   public void sendResumptionToken(String resumptionToken, String cursor, String completeListSize) throws SAXException {
       if (OAIUtilities.checkString(resumptionToken) | OAIUtilities.checkString(cursor) | OAIUtilities.checkString(completeListSize)) {
           AttributesImpl atts = new AttributesImpl();
           if (OAIUtilities.checkString(cursor))
               atts.addAttribute("", "cursor", "cursor", OAIObject.Node.Type.CDATA, cursor);
           if (OAIUtilities.checkString(completeListSize))
               atts.addAttribute("", "completeListSize", "completeListSize", OAIObject.Node.Type.CDATA, completeListSize);
           super.sendElement(OAIObject.Node.Xmlns.OAI_2_0, OAIObject.Node.Name.RESUMPTION_TOKEN, OAIObject.Node.Name.RESUMPTION_TOKEN, atts, resumptionToken);
       }
   }


}





