/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
 */
package org.apache.wiki.ui;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.apache.wiki.WatchDog;
import org.apache.wiki.api.core.Context;
import org.apache.wiki.api.core.Engine;
import org.apache.wiki.event.WikiEventManager;
import org.apache.wiki.event.WikiPageEvent;
import org.apache.wiki.url.URLConstructor;
import org.apache.wiki.util.TextUtil;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;


/**
 * This filter goes through the generated page response prior and places requested resources at the appropriate inclusion markers.
 * This is done to let dynamic content (e.g. plugins, editors) include custom resources, even after the HTML head section is
 * in fact built. This filter is typically the last filter to execute, and it <em>must</em> run after servlet or JSP code that performs
 * redirections or sends error codes (such as access control methods).
 * <p>
 * Inclusion markers are placed by the IncludeResourcesTag; the default content templates (see .../templates/default/commonheader.jsp)
 * are configured to do this. As an example, a JavaScript resource marker is added like this:
 * <pre>
 * &lt;wiki:IncludeResources type="script"/&gt;
 * </pre>
 * Any code that requires special resources must register a resource request with the TemplateManager. For example:
 * <pre>
 * &lt;wiki:RequestResource type="script" path="scripts/custom.js" /&gt;
 * </pre>
 * or programmatically,
 * <pre>
 * TemplateManager.addResourceRequest( context, TemplateManager.RESOURCE_SCRIPT, "scripts/customresource.js" );
 * </pre>
 *
 * @see TemplateManager
 * @see org.apache.wiki.tags.RequestResourceTag
 */
public class WikiJSPFilter extends WikiServletFilter {

    private static final Logger LOG = LogManager.getLogger( WikiJSPFilter.class );
    private String m_wiki_encoding;
    private boolean useEncoding;

    /** {@inheritDoc} */
    @Override
    public void init( final FilterConfig config ) throws ServletException {
        super.init( config );
        m_wiki_encoding = m_engine.getWikiProperties().getProperty( Engine.PROP_ENCODING );

        useEncoding = !Boolean.parseBoolean( m_engine.getWikiProperties().getProperty( Engine.PROP_NO_FILTER_ENCODING, "false" ).trim() );
    }

    @Override
    public void doFilter( final ServletRequest  request, final ServletResponse response, final FilterChain chain ) throws ServletException, IOException {
        final WatchDog w = WatchDog.getCurrentWatchDog( m_engine );
        try {
            ThreadContext.push( m_engine.getApplicationName() + ":" + ( ( HttpServletRequest )request ).getRequestURI() );
            w.enterState("Filtering for URL "+((HttpServletRequest)request).getRequestURI(), 90 );
            final HttpServletResponseWrapper responseWrapper = new JSPWikiServletResponseWrapper( ( HttpServletResponse )response, m_wiki_encoding, useEncoding );

            // fire PAGE_REQUESTED event
            final String pagename = URLConstructor.parsePageFromURL( ( HttpServletRequest )request, Charset.forName( response.getCharacterEncoding() ) );
            fireEvent( WikiPageEvent.PAGE_REQUESTED, pagename );
            super.doFilter( request, responseWrapper, chain );

            // The response is now complete. Lets replace the markers now.

            // WikiContext is only available after doFilter! (That is after interpreting the jsp)

            try {
                w.enterState( "Delivering response", 30 );
                final Context wikiContext = getWikiContext( request );
                final String r = filter( wikiContext, responseWrapper );

                if( useEncoding ) {
                    final OutputStreamWriter out = new OutputStreamWriter( response.getOutputStream(), response.getCharacterEncoding() );
                    out.write( r );
                    out.flush();
                    out.close();
                } else {
                    response.getWriter().write(r);
                }

                // Clean up the UI messages and loggers
                if( wikiContext != null ) {
                    wikiContext.getWikiSession().clearMessages();
                }

                // fire PAGE_DELIVERED event
                fireEvent( WikiPageEvent.PAGE_DELIVERED, pagename );

            } finally {
                w.exitState();
            }
        } finally {
            w.exitState();
            ThreadContext.pop();
            ThreadContext.remove( m_engine.getApplicationName() + ":" + ( ( HttpServletRequest )request ).getRequestURI() );
        }
    }

    /**
     * Goes through all types and writes the appropriate response.
     *
     * @param wikiContext The usual processing context
     * @param response The source string
     * @return The modified string with all the insertions in place.
     */
    private String filter( final Context wikiContext, final HttpServletResponse response ) {
        String string = response.toString();

        if( wikiContext != null ) {
            final String[] resourceTypes = TemplateManager.getResourceTypes( wikiContext );
            for( final String resourceType : resourceTypes ) {
                string = insertResources( wikiContext, string, resourceType );
            }

            //  Add HTTP header Resource Requests
            final String[] headers = TemplateManager.getResourceRequests( wikiContext, TemplateManager.RESOURCE_HTTPHEADER );

            for( final String header : headers ) {
                String key = header;
                String value = "";
                final int split = header.indexOf( ':' );
                if( split > 0 && split < header.length() - 1 ) {
                    key = header.substring( 0, split );
                    value = header.substring( split + 1 );
                }

                response.addHeader( key.trim(), value.trim() );
            }
        }

        return string;
    }

    /**
     *  Inserts whatever resources were requested by any plugins or other components for this particular type.
     *
     *  @param wikiContext The usual processing context
     *  @param string The source string
     *  @param type Type identifier for insertion
     *  @return The filtered string.
     */
    private String insertResources( final Context wikiContext, final String string, final String type ) {
        if( wikiContext == null ) {
            return string;
        }

        final String marker = TemplateManager.getMarker( wikiContext, type );
        final int idx = string.indexOf( marker );
        if( idx == -1 ) {
            return string;
        }

        LOG.debug("...Inserting...");

        final String[] resources = TemplateManager.getResourceRequests( wikiContext, type );
        final StringBuilder concat = new StringBuilder( resources.length * 40 );

        for( final String resource : resources ) {
            LOG.debug( "...:::" + resource );
            concat.append( resource );
        }

        return TextUtil.replaceString( string, idx, idx + marker.length(), concat.toString() );
    }

    /**
     *  Simple response wrapper that just allows us to gobble through the entire
     *  response before it's output.
     */
    private static class JSPWikiServletResponseWrapper extends HttpServletResponseWrapper {

        final ByteArrayOutputStream m_output;
        private final ByteArrayServletOutputStream m_servletOut;
        private final PrintWriter m_writer;
        private final HttpServletResponse m_response;
        private final boolean useEncoding;

        /** How large the initial buffer should be.  This should be tuned to achieve a balance in speed and memory consumption. */
        private static final int INIT_BUFFER_SIZE = 0x8000;

        public JSPWikiServletResponseWrapper( final HttpServletResponse r, final String wikiEncoding, final boolean useEncoding ) throws UnsupportedEncodingException {
            super( r );
            m_output = new ByteArrayOutputStream( INIT_BUFFER_SIZE );
            m_servletOut = new ByteArrayServletOutputStream( m_output );
            m_writer = new PrintWriter( new OutputStreamWriter( m_servletOut, wikiEncoding ), true );
            this.useEncoding = useEncoding;

            m_response = r;
        }

        /** Returns a writer for output; this wraps the internal buffer into a PrintWriter. */
        @Override
        public PrintWriter getWriter() {
            return m_writer;
        }

        @Override
        public ServletOutputStream getOutputStream() {
            return m_servletOut;
        }

        @Override
        public void flushBuffer() throws IOException {
            m_writer.flush();
            super.flushBuffer();
        }

        class ByteArrayServletOutputStream extends ServletOutputStream {

            final ByteArrayOutputStream m_buffer;

            public ByteArrayServletOutputStream( final ByteArrayOutputStream byteArrayOutputStream ) {
                super();
                m_buffer = byteArrayOutputStream;
            }

            //
            /**{@inheritDoc} */
            @Override
            public void write( final int aInt ) {
                m_buffer.write( aInt );
            }

            /**{@inheritDoc} */
            @Override
			public boolean isReady() {
				return false;
			}

            /**{@inheritDoc} */
            @Override
			public void setWriteListener( final WriteListener writeListener ) {
			}
			
        }

        /** Returns whatever was written so far into the Writer. */
        @Override
        public String toString() {
            try {
				flushBuffer();
			} catch( final IOException e ) {
                LOG.error( e );
                return StringUtils.EMPTY;
			}

            try {
				if( useEncoding ) {
					return m_output.toString( m_response.getCharacterEncoding() );
				}

				return m_output.toString();
			} catch( final UnsupportedEncodingException e ) {
                LOG.error( e );
                return StringUtils.EMPTY;
             }
        }

    }

    // events processing .......................................................

    /**
     *  Fires a WikiPageEvent of the provided type and page name
     *  to all registered listeners of the current Engine.
     *
     * @see org.apache.wiki.event.WikiPageEvent
     * @param type       the event type to be fired
     * @param pagename   the wiki page name as a String
     */
    protected final void fireEvent( final int type, final String pagename ) {
        if( WikiEventManager.isListening( m_engine ) ) {
            WikiEventManager.fireEvent( m_engine, new WikiPageEvent( m_engine, type, pagename ) );
        }
    }

}
