/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.app.xmlui.cocoon;

import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
import org.apache.avalon.framework.parameters.ParameterException;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.environment.*;
import org.apache.cocoon.reading.ResourceReader;
import org.apache.excalibur.source.Source;
import org.apache.excalibur.source.SourceValidity;
import org.apache.excalibur.source.impl.validity.TimeStampValidity;
import org.apache.log4j.Logger;
import org.dspace.core.ConfigurationManager;
import org.mozilla.javascript.EvaluatorException;
import org.xml.sax.SAXException;

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

/**
 * Concatenates and Minifies CSS, JS and JSON files
 *
 * The URL of the resource can contain references to multiple
 * files: e.g."themes/Mirage/lib/css/reset,base,helper,style,print.css"
 * The Reader will concatenate all these files, and output them as
 * a single resource.
 *
 * If "xmlui.theme.enableMinification" is set to true, the
 * output will also be minified prior to returning the resource.
 *
 * Validity is determined based upon last modified date of
 * the most recently edited file.
 *
 * @author Roel Van Reeth (roel at atmire dot com)
 * @author Art Lowel (art dot lowel at atmire dot com)
 * @author Ben Bosman (ben at atmire dot com)
 */

public class ConcatenationReader extends ResourceReader {

    private static final int MINIFY_LINEBREAKPOS = 8000;
    protected List<Source> inputSources;
    private String key;
    private StreamEnumeration streamEnumeration;
    private static Logger log = Logger.getLogger(ConcatenationReader.class);
    private boolean doMinify = true;

    /**
     * Setup the reader.
     * The resource is opened to get an <code>InputStream</code>,
     * the length and the last modification date
     */
    public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par)
            throws ProcessingException, SAXException, IOException {

        // save key
        this.key = src;

        // don't support byte ranges (resumed downloads)
        this.setByteRanges(false);

        // setup list of sources, get relevant parts of path
        this.inputSources = new ArrayList<Source>();
        String path = src.substring(0, src.lastIndexOf('/'));
        String file = src.substring(src.lastIndexOf('/')+1);

        // now build own list of inputsources
        String[] files = file.split(",");
        for (String f : files) {
            if (file.endsWith(".json") && !f.endsWith(".json")) {
                f += ".json";
            }
            if (file.endsWith(".js") && !f.endsWith(".js")) {
                f += ".js";
            }
            if (file.endsWith(".css") && !f.endsWith(".css")) {
                f += ".css";
            }

            String fullPath = path + "/" + f;
            this.inputSources.add(resolver.resolveURI(fullPath));
        }

        // do super stuff
        super.setup(resolver, objectModel, path+"/"+files[files.length-1], par);

        // add stream enumerator
        this.streamEnumeration = new StreamEnumeration();

        // check minify parameter
        try {
            if("nominify".equals(par.getParameter("requestQueryString"))) {
                this.doMinify = false;
            } else {
                // modify key!
                this.key += "?minify";
            }
        } catch (ParameterException e) {
            log.error("ParameterException in setup when retrieving parameter requestQueryString", e);
        }
    }

    /**
     * Recyclable
     */
    public void recycle() {
        if (this.inputSources != null) {
            for(Source s : this.inputSources) {
                super.resolver.release(s);
            }
            this.inputSources = null;
            this.streamEnumeration = null;
            this.key = null;
        }
        super.recycle();
    }

    /**
     * Generate the unique key.
     * This key must be unique inside the space of this component.
     *
     * @return The generated key hashes the src
     */
    public Serializable getKey() {
        return key;
    }

    /**
     * Generate the validity object.
     *
     * @return The generated validity object or <code>null</code> if the
     *         component is currently not cacheable.
     */
    public SourceValidity getValidity() {
        final long lm = getLastModified();
        if(lm > 0) {
            return new TimeStampValidity(lm);
        }
        return null;
    }

    /**
     * @return the time the read source was last modified or 0 if it is not
     *         possible to detect
     */
    public long getLastModified() {
        // get latest modified value
        long modified = 0;

        for(Source s : this.inputSources) {
            if(s.getLastModified() > modified) {
                modified = s.getLastModified();
            }
        }

        return modified;
    }

    /**
     * Generates the requested resource.
     */
    public void generate() throws IOException, ProcessingException {
        InputStream inputStream;

        // create one single inputstream from all files
        inputStream = new SequenceInputStream(streamEnumeration);

        try {
            if (ConfigurationManager.getBooleanProperty("xmlui.theme.enableMinification",false) && this.doMinify) {
                compressedOutput(inputStream);
            } else {
                normalOutput(inputStream);
            }
            // Bugzilla Bug #25069: Close inputStream in finally block.
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }

        out.flush();
    }

    private void compressedOutput(InputStream inputStream) throws IOException {
        // prepare streams
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        Writer outWriter = new OutputStreamWriter(bytes);

        // do compression
        Reader in = new BufferedReader(new InputStreamReader(inputStream));
        if (this.key.endsWith(".js?minify") || this.key.endsWith(".json?minify")) {
            try {

                JavaScriptCompressor compressor = new JavaScriptCompressor(in, null);

                // boolean options: munge, verbose, preserveAllSemiColons, disableOptimizations
                compressor.compress(outWriter, MINIFY_LINEBREAKPOS, true, false, false, false);

            } catch (EvaluatorException e) {
                // fail gracefully on malformed javascript: send it without compressing
                normalOutput(inputStream);
                return;
            }
        } else if (this.key.endsWith(".css?minify")) {
            CssCompressor compressor = new CssCompressor(in);
            compressor.compress(outWriter, MINIFY_LINEBREAKPOS);
        } else {
            // or not if not right type
            normalOutput(inputStream);
            return;
        }

        // first send content-length header
        outWriter.flush();
        response.setHeader("Content-Length", Long.toString(bytes.size()));

        // then send output and clean up
        bytes.writeTo(out);
        in.close();
    }

    private void normalOutput(InputStream inputStream) throws IOException {
        boolean validContentLength = true;
        byte[] buffer = new byte[bufferSize];
        int length;
        long contentLength = 0;

        // calculate content length
        for (Source s : this.inputSources) {
            if(s.getContentLength() < 0) {
                validContentLength = false;
            }
            contentLength += s.getContentLength();
        }
        if(validContentLength) {
            response.setHeader("Content-Length", Long.toString(contentLength));
        }

        // send contents
        while ((length = inputStream.read(buffer)) > -1) {
            out.write(buffer, 0, length);
        }
    }

    private final class StreamEnumeration implements Enumeration {
        private int index;

        private StreamEnumeration() {
            this.index = 0;
        }

        public boolean hasMoreElements() {
            return index < inputSources.size();
        }

        public InputStream nextElement() {
            try {
                InputStream elem = inputSources.get(index).getInputStream();
                index++;
                return elem;
            } catch (IOException e) {
                log.error("IOException in StreamEnumeration.nextElement when retrieving InputStream of a Source; index = "
                        + index + ", inputSources.size = " + inputSources.size(), e);
                return null;
            }
        }
    }
}
