/*
Copyright (C) 2000-2010  Ministere de la culture et de la communication (France), AJLSM
See LICENCE file
*/

package fr.gouv.culture.sdx.pipeline;

import fr.gouv.culture.sdx.documentbase.DefaultIDGenerator;
import fr.gouv.culture.sdx.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.utils.Identifiable;
import fr.gouv.culture.sdx.utils.SdxObject;
import fr.gouv.culture.sdx.utils.Utilities;
import fr.gouv.culture.sdx.utils.configuration.ConfigurationUtils;
import fr.gouv.culture.sdx.utils.xml.AbstractSdxXMLPipe;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.configuration.DefaultConfiguration;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.Constants;
import org.apache.cocoon.serialization.XMLSerializer;
import org.apache.cocoon.xml.XMLConsumer;
import org.apache.cocoon.xml.XMLMulticaster;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Vector;

/**An abstract implementation of a Pipeline for manipulation of SAX events
 *
 */
public abstract class AbstractPipeline extends AbstractSdxXMLPipe implements Pipeline {

    /**The parameters for this pipeline*/
    private Parameters params;

    /**A list of the transformations that make up this pipeline. */
    protected LinkedList transformations = new LinkedList();

    /**A list of the transformations that make up this pipeline. */
    protected Vector transformsList = new Vector();

    /*The transformation step of which the results should be stored*/
    protected String keepStep = null;

    /**A output stream for the event results of a transformation*/
    private ByteArrayOutputStream transformedBytes = null;

    /**A output stream for the event results of a transformation*/
    private File transformedFile = null;

    FileOutputStream fOs = null;

    private XMLSerializer xsFile = null;

    private XMLSerializer xsBytes = null;

    /**prefix for our tempfile created for the fileOuputStream*/
    private String TEMPFILE_PREFIX = "tempTransDoc";

    /*suffix for our tempfile created for the fileOuputStream*/
    private String TEMPFILE_SUFFIX = ".sdx";

    /**
     * Configures the pipeline.
     *
     * @param configuration  The configuration object provided from a document base object.
     *
     *<p>Retrieves the children elements named "transformation" and builds a
     * transformation object for each element. Currently the only supported
     * transformation type is "xslt".
     *
     * <p>Sample configuration snippet:
     * <p>&lt;sdx:pipeline sdx:id = "sdxIndexationPipeline">
     *      <p>&lt;sdx:transformation src = "path to stylesheet, can be absolute or relative to the directory containing this file" sdx:id = "step2" sdx:type = "xslt"/>
     *      <p>&lt;sdx:transformation src = "path to stylesheet, can be absolute or relative to the directory containing this file" sdx:id = "step3" sdx:type = "xslt" keep = "true"/>
     * <p>&lt;/sdx:pipeline>
     * @throws org.apache.avalon.framework.configuration.ConfigurationException
     */
    public void configure(Configuration configuration) throws ConfigurationException {
        //Utilities.checkConfiguration(configuration);
        loadBaseConfiguration(configuration);
        super.configure(configuration);

        try {
            configureTransformations(configuration);
        } catch (SDXException sdxE) {
            throw new ConfigurationException(sdxE.getMessage(), sdxE);
        }
        if (configuration != null) {
            this.params = Parameters.fromConfiguration(configuration);
            verifyConfigurationParameters(this.params);
        }

    }

    protected void verifyConfigurationParameters(Parameters params) {
        if (params != null) {
            String[] paramNames = params.getNames();
            for (int i = 0; i < paramNames.length; i++) {
                String paramName = paramNames[i];
                if (Utilities.checkString(paramName)) {
                    String paramValue = params.getParameter(paramName, null);
                    if (!Utilities.checkString(paramValue) || paramValue.equals("null"))
                        params.removeParameter(paramName);
                }


            }
        }

    }

    private void loadBaseConfiguration(Configuration configuration) throws ConfigurationException {
        //if we don't already have a good id we find one fromt the configuration
        if (!Utilities.checkString(getId())) {
            if (configuration != null)
                try {
                    super.setId(configuration.getAttribute(Identifiable.ConfigurationNode.ID, new DefaultIDGenerator().generate()));
                } catch (SDXException e) {
                    throw new ConfigurationException(e.getMessage(), e);
                }
        }
    }

    private void configureTransformations(Configuration configuration) throws ConfigurationException, SDXException {

        //getting the <sdx:transformation> children elements of the  <sdx:pipeline> element
        Configuration[] transforms = null;
        if (configuration != null)
            transforms = configuration.getChildren(Utilities.getElementName(Transformation.CLASS_NAME_SUFFIX));
        //testing to see if we have something
        /*      if (transforms == null || transforms.length == 0) {
                  String[] args = new String[1];
                  args[0] = configuration.getLocation();
                  SDXException sdxE = new SDXException(super.getLog(), SDXExceptionCode.ERROR_NO_TRANSFORMATIONS_IN_CONFIG, null, null);
                  throw new ConfigurationException(sdxE.getMessage(), sdxE);
              }
         */
        if (transforms != null) {
            /*
            if (transforms.length == 0) {
                String[] args = new String[1];
                args[0] = configuration.getLocation();
                SDXException sdxE = new SDXException(super.getLog(), SDXExceptionCode.ERROR_NO_TRANSFORMATIONS_IN_CONFIG, null, null);
                throw new ConfigurationException(sdxE.getMessage(), sdxE);
            }
            */
            //iterating over the configuration elements to create the necessary transformations and add them to the pipeline
            for (int i = 0; i < transforms.length; i++) {
                Transformation trans = null;
                //getting the "type" attribute
                final String transType = transforms[i].getAttribute(SdxObject.ConfigurationNode.TYPE);
                //verifying the attribute
                ConfigurationUtils.checkConfAttributeValue(SdxObject.ConfigurationNode.TYPE, transType, transforms[i].getLocation());
                Object obj = Utilities.getObjectForClassName(getLog(), transType, Transformation.PACKAGE_QUALNAME, transType, Transformation.CLASS_NAME_SUFFIX);
                Class l_objClass = obj.getClass();
                if (!(obj instanceof Transformation)) {
                    //the object doesn't implement our interface
                    String[] args = new String[3];
                    args[0] = Transformation.CLASS_NAME_SUFFIX;
                    if (l_objClass != null)
                        args[1] = l_objClass.getName();
                    args[2] = transType;
                    throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_CLASS_NOT_INSTANCEOF_SDX_INTERFACE, args, null);
                }
                //casting into a transformation object
                trans = (Transformation) obj;
                trans = (Transformation) super.setUpSdxObject(trans, transforms[i]);
                //adding the transformation to the this pipeline
                //addTransformation(trans);
                String transId = trans.getId();
                if (Utilities.checkString(transId)) {
                    if (trans.shouldKeepResult())
                        keepStep = transId;

                    //adding the transformation to the hashtable
                    transformsList.add(trans);
                }

            }
            //we need to build the pipeline after we have all the transformations and have determined the keep step
            for (int k = 0; k < transformsList.size(); k++) {
                Transformation t = (Transformation) transformsList.get(k);
                if (t != null) addTransformation(t);
            }
        }


    }

    /**
     * Adds a transformation to the pipeline and sets the consumer of the last
     * transformation to be the newly added transformation.
     * @param t     The transformation to be added.
     */
    public void addTransformation(Transformation t) throws SDXException {
        if (transformations.size() > 0) {
            //getting the last transformation
            Transformation lastTrans = ((Transformation) transformations.getLast());
            //if we want to retain the events from this step we bridge two consumers
            XMLConsumer consumer = null;
            if (lastTrans != null && lastTrans.getId() != null && lastTrans.getId().equals(keepStep))
            //creating the special consumer to collect the events
                consumer = this.multiCastEvents(t);
            else
                consumer = t;
            //recycling, the content, lexical, and xmlConsumer
            /*this is a necessary step to ensure that each transformation has a new
            consumer for each time this transformation is executed by the pipeline*/
            /*it is not very evident at this time that this step is useful in this method,
            but if we ever have to REconfigure this pipeline, this will be necessary*/
            lastTrans.recycle();

            //this sets the consumer of the last step in the pipeline
            lastTrans.setConsumer(consumer);
        } else {
            //if not we ONLY have the pipeline and need to set it's consumer to be the new transformation
            //recycling, the content, lexical, and xmlConsumer
            /*this is a necessary step to ensure that the pipeline has a new
            consumer*/
            /*it is not very evident at this time that this step is useful in this method,
            but if we ever have to REconfigure this pipeline, this will be necessary*/
            super.recycle();
            //setting consumer and handlers
            super.xmlConsumer = t;
            super.contentHandler = t;
            super.lexicalHandler = t;
        }
        //adding the new transformation to the data structure
        transformations.add(t);
    }

    /**
     * Sets the consumer of the pipeline.
     *
     * @param xmlConsumer   The XMLConsumer for the LAST step of the pipeline
     */
    public void setConsumer(XMLConsumer xmlConsumer) {
        //verifying the consumer
        try {
            Utilities.checkXmlConsumer(super.getLog(), xmlConsumer);
        } catch (SDXException e) {
            /*doing nothing here as the exception should have been logged,
            as we are governed by superClasses for the exceceptions signatures*/
        }
        if (transformations.size() > 0) {
            //getting the last transformation
            Transformation lastTrans = ((Transformation) transformations.getLast());
            //recycling, the content, lexical, and xmlConsumer
            /*this is a necessary step to ensure that each transformation has a new
            consumer for each time this transformation is executed by the pipeline*/
            lastTrans.recycle();


            if (Utilities.checkString(keepStep)) {

                //if we want to retain the events from the last step we bridge two consumers
                if (lastTrans != null && lastTrans.getId() != null && lastTrans.getId().equals(keepStep))
                //creating the special consumer to collect the events
                    try {
                        xmlConsumer = this.multiCastEvents(xmlConsumer);
                    } catch (SDXException e) {
                        e.printStackTrace();//Can't do much here as we are bound by the interface
                    }

                /*if we have a valid id for a keep step IN THE PIPELINE(not only the last step)
                *we reset the necessary transformed output event collectors, this is necessary for output
                */
                try {
                    resetTransformedDocumentFields();
                } catch (SDXException e) {
                    /*doing nothing here as the exception should have been logged,
                    as we are governed by superClasses for the exceceptions signatures*/
                }
            }

            //this sets the consumer of the last step in the pipeline
            lastTrans.setConsumer(xmlConsumer);

        } else {
            //TODOLogging:better warn message here, i dont think this should ever arrive as we check on configuration
            //Utilities.logWarn(super.getLog(), "no transformations defined", null);
            super.setConsumer(xmlConsumer);
        }


    }

    private void resetTransformedDocumentFields() throws SDXException {
        //resetting the resources output transformation output
        createTempFileResources();
        createTempByteResources();
    }

    /**One should ensure that the createTempFileResources
     *
     */
    //TODO?:should this be in the interface?
    private XMLConsumer multiCastEvents(XMLConsumer xmlConsumer) throws SDXException {

        XMLMulticaster connector = null;
        //buliding the serializers which will supply the output streams
        try {
        if (canBuildTempFile() && this.xsFile == null){
            this.xsFile = new XMLSerializer();
            this.xsFile.enableLogging(super.getLog());
            this.xsFile.configure(new DefaultConfiguration("hack", ""));
        }

        if (this.xsBytes == null){
            this.xsBytes = new XMLSerializer();
            this.xsBytes.enableLogging(super.getLog());
            this.xsBytes.configure(new DefaultConfiguration("hack", ""));
        }

        } catch (ConfigurationException e) {
            //TODOException
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GENERIC, null, e);
        }

        if (this.xsFile != null) {
            XMLMulticaster serializedOutput = new XMLMulticaster(xsBytes, xsFile);
            //building a mutli caster to connect the output serializers and the provided xmlConsumer to the transformation step
            connector = new XMLMulticaster(serializedOutput, xmlConsumer);
        } else {
            connector = new XMLMulticaster(this.xsBytes, xmlConsumer);
        }

        return connector;


    }

    /**Sets the Parameters for each step in the Pipeline
     *
     * @param params    The Parameters to use for each transformation step.
     *
     * <p>Each transformation step in the pipeline can call (getParameters() defined
     * in the Transformation interface) and make use this Parameters object as needed
     * before pipeline processing begins.</p>
     */
    public void setParameters(Parameters params) {
        //setting the class field
        this.params = params;
        setParametersToTransformations(this.params);
    }

    protected void setParametersToTransformations(Parameters parameters) {
        if (transformations != null && !transformations.isEmpty()) {
            Object[] transforms = transformations.toArray();
            for (int i = 0; i < transforms.length; i++) {
                Transformation t = (Transformation) transforms[i];
                t.setParameters(parameters);
            }
        }
    }

    /**Returns a new instance of this object
     *
     * @return A new instance of this object
     */
    public Pipeline newInstance() throws SDXException {
        try {
            Pipeline pipe = (Pipeline) this.getClass().newInstance();
            pipe = (Pipeline) super.setUpSdxObject(pipe, super.getConfiguration());
            return pipe;
        } catch (ConfigurationException e) {
            String[] args = new String[2];
            args[0] = super.getId();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_NEW_INSTANCE, args, null);
        } catch (InstantiationException e) {
            String[] args = new String[2];
            args[0] = super.getId();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_NEW_INSTANCE, args, null);
        } catch (IllegalAccessException e) {
            String[] args = new String[2];
            args[0] = super.getId();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_NEW_INSTANCE, args, null);
        }
    }

    /**Returns the paramters for this pipeline*/
    public Parameters getParameters() {
        return this.params;
    }

    /**Returns a byte array which contains the
     * data of a transformation step. The data will only be present
     * after a pipeline is executed, if no transformation data is retained
     * <code>null</code> will be returned
     *
     * @return
     */
    public byte[] getTransformedBytes() {
        if (this.transformedBytes != null && this.transformedBytes.size() > 0) {
            //TODO:this should be tested with large buffer, ie large xml documents
            return this.transformedBytes.toByteArray();
        } else
            return null;
    }

    /**Returns a file which contains the
     * data of a transformation step. The data will only be present
     * after a pipeline is executed, if no transformation data is retained
     * <code>null</code>  will be returned
     *
     */
    public File getTransformedFile() throws SDXException {
        if (this.fOs != null && this.transformedFile != null && transformedFile.exists() && transformedFile.length() > 0) {
            try {
                this.fOs.flush();
            } catch (IOException e) {
                throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_TRANSFORMED_FILE, null, e);
            }
            return this.transformedFile;
        } else
            return null;
    }

    private void createTempFileResources() throws SDXException {
        try {

            if (canBuildTempFile() && this.xsFile != null) {
                if (this.fOs != null)
                    this.fOs.close();

                //TODO: test this, if we delete the file, does the mulitcaster keep the reference
                if (this.transformedFile != null && transformedFile.exists()) {
                    if (!this.transformedFile.delete())//if we can't delete the file successfully we will try onExit
                        this.transformedFile.deleteOnExit();
                }
                File tempDir = (File) super.getContext().get(Constants.CONTEXT_UPLOAD_DIR);
                if (!tempDir.canWrite())
                    tempDir = Utilities.getSystemTempDir();

                tempDir = new File(tempDir, File.separator + "tempTransformedDocs" + File.separator + getId() + File.separator);

                tempDir = Utilities.createTempDirectory(this.getLog(), this.getId(), Integer.toString(this.hashCode()), tempDir);

                this.transformedFile = File.createTempFile(TEMPFILE_PREFIX, TEMPFILE_SUFFIX, tempDir);

                if (this.transformedFile != null && transformedFile.exists())
                    this.fOs = new FileOutputStream(this.transformedFile);
                if (this.fOs != null) {
                    this.xsFile.recycle();
                    this.xsFile.setOutputStream(this.fOs);
                }
            }
        } catch (IOException e) {
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_BUILD_TRANSFORMED_FILE, null, e);
        } catch (ContextException e) {
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_BUILD_TRANSFORMED_FILE, null, e);
        }
    }

    private void createTempByteResources() {
        //building the output streams which will be populated during the transformation
        if (this.transformedBytes == null)
            this.transformedBytes = new ByteArrayOutputStream();

        //buliding the serializers which will supply the output streams
        if (this.transformedBytes != null) {
            this.transformedBytes.reset();

            if (this.xsBytes != null) {
                this.xsBytes.recycle();
                try {
                	this.xsBytes.setOutputStream(this.transformedBytes);
                } catch (IOException e) {
				}
            }
        }
    }

    private boolean canBuildTempFile() {
        File tempFile = null;
        boolean ret = true;
        try {
            tempFile = File.createTempFile(TEMPFILE_PREFIX, TEMPFILE_SUFFIX);
        } catch (IOException e) {
            ret = false;
        }
        finally {
            if (tempFile != null && tempFile.exists()) tempFile.delete();
        }
        return ret;
    }

    protected String getClassNameSuffix() {
        return Pipeline.CLASS_NAME_SUFFIX;
    }

	protected boolean initToSax() {
		this._xmlizable_objects.put("ID", this.getId());
		this._xmlizable_objects.put("Encoding", this.getEncoding());
		this._xmlizable_objects.put("XML-Lang", this.getXmlLang());
		this._xmlizable_objects.put("Locale", this.getLocale().toString());
//		TODO: Is it useful to send the parameters?
		/*if((this.getParameters()!=null) && (this.getParameters().getNames()!=null)){
			String[] names=this.getParameters().getNames();
			for(int i=0; i< names.length;i++){
				try{
					this._xmlizable_objects.put(names[i], this.getParameters().getParameter(names[i]));
				} catch(ParameterException e){	
				}
			}
		}*/
		for(ListIterator iter=transformations.listIterator(); iter.hasNext();){
			Transformation t=(Transformation)iter.next();
			this._xmlizable_objects.put("Transformation", t);
		}
		
		return true;
	}
	
	/**Init the LinkedHashMap _xmlizable_volatile_objects with the objects in order to describ them in XML
	 * Some objects need to be refresh each time a toSAX is called*/
	protected void initVolatileObjectsToSax() {}

}
