/*
 * JBoss, Home of Professional Open Source
 * Copyright 2013, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.io;

import java.io.IOException;
import java.io.Writer;

import javax.faces.component.UIComponent;
import javax.faces.context.ResponseWriter;

import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;

/**
 * @author shura
 *         <p/>
 *         Realization of Faces <code>ResponseWriter</code> for Cocoon Environment. Use ONLY Markup-specific calls , send it as
 *         SAX events to <code>XMLConsumer</code> Use "State" pattern for control of events flow. TODO - implement namespace
 *         capabilites
 */
public class SAXResponseWriter extends ResponseWriter {
    /**
     * As we in XML framework, only UTF-8 supported for <code>CHARTER_ENCODING</code>
     */
    private static final String CHARTER_ENCODING = "UTF-8";
    /**
     * As we in XML framework, only xml supported for <code>CONTENT_TYPE</code>
     */
    private static final String CONTENT_TYPE = "text/xml";
    private String namespaceURI = "http://www.w3.org/1999/xhtml";
    private LexicalHandler xmlLexicalHandler = null;
    private AttributesImpl attributes;
    private XMLResponseWriterState cdataState;
    private String element;
    /**
     * State after startElement. Collect Attributes for SAX startElement <code>elementState</code>
     */
    private XMLResponseWriterState elementState;
    /**
     * State in normal document <code>inDocumentState</code>
     */
    private XMLResponseWriterState inDocumentState;
    /**
     * Before StartDocument or after EndDocument <code>notDocumentState</code>
     */
    private XMLResponseWriterState notDocumentState;
    private XMLResponseWriterState state;
    /**
     * Hold Cocoon Generator XML <code>consumer</code>
     */
    private ContentHandler xmlConsumer;

    /**
     * @param consumer - SAX events receiver for Cocoon pipeline.
     */
    public SAXResponseWriter(ContentHandler consumer) {
        super();
        this.xmlConsumer = consumer;

        if (consumer instanceof LexicalHandler) {
            xmlLexicalHandler = (LexicalHandler) consumer;
        }

        // Initialise states. May be must implemented in static block ?
        this.notDocumentState = new NotDocumentState();

        // inside document. allow any events exclude attributes and
        // startDocument.
        this.inDocumentState = new InDocumentState();
        this.cdataState = new CDATAState();

        // In element, collect attributes ...
        this.elementState = new ElementState();
        this.state = notDocumentState;
    }

    public ContentHandler getXmlConsumer() {
        return xmlConsumer;
    }

    /**
     * @return Returns the namespaceURI.
     */
    public String getNamespaceURI() {
        return namespaceURI;
    }

    /**
     * @param namespaceURI The namespaceURI to set.
     */
    public void setNamespaceURI(String namespaceURI) {
        this.namespaceURI = namespaceURI;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#getContentType()
     */
    public String getContentType() {
        return CONTENT_TYPE;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#getCharacterEncoding()
     */
    public String getCharacterEncoding() {
        return CHARTER_ENCODING;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.io.Flushable#flush()
     */
    public void flush() throws IOException {

        // DO NOTHING...
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#startDocument()
     */
    public void startDocument() throws IOException {
        state.startDocument();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#endDocument()
     */
    public void endDocument() throws IOException {
        state.endDocument();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#startElement(java.lang.String, javax.faces.component.UIComponent)
     */
    public void startElement(String name, UIComponent component) throws IOException {
        state.startElement(name, component);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#endElement(java.lang.String)
     */
    public void endElement(String name) throws IOException {
        state.endElement(name);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#writeAttribute(java.lang.String, java.lang.Object, java.lang.String)
     */
    public void writeAttribute(String name, Object value, String property) throws IOException {
        state.writeAttribute(name, value, property);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#writeURIAttribute(java.lang.String, java.lang.Object, java.lang.String)
     */
    public void writeURIAttribute(String name, Object value, String property) throws IOException {
        state.writeURIAttribute(name, value, property);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#writeComment(java.lang.Object)
     */
    public void writeComment(Object comment) throws IOException {
        state.writeComment(comment);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#writeText(java.lang.Object, java.lang.String)
     */
    public void writeText(Object text, String property) throws IOException {
        state.writeText(text, property);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#writeText(char[], int, int)
     */
    public void writeText(char[] text, int off, int len) throws IOException {
        state.writeText(text, off, len);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.faces.context.ResponseWriter#cloneWithWriter(java.io.Writer)
     */
    public ResponseWriter cloneWithWriter(Writer writer) {

        // TODO as used XML consumer to get sax Events, we simple return current
        // instance.
        // if will used wrapper to combine XML Consumer with plain Servlet
        // responce writer, must
        // perform real clone ...
        // We can use org.apache.cocoon.xml.SaxBuffer;
        return this;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.io.Writer#write(char[], int, int)
     */
    public void write(char[] cbuf, int off, int len) throws IOException {
        state.writeText(cbuf, off, len);
    }

    /*
     * (non-Javadoc)
     *
     * @see java.io.Closeable#close()
     */
    public void close() throws IOException {

        // TODO Auto-generated method stub
    }

    /**
     * @author shura
     *         <p/>
     *         CDATA section. allow regular write() functions, write any text.
     */
    private final class CDATAState extends XMLResponseWriterState {
        void flushCDATA() throws IOException {

            // try {
            // xmlConsumer.endCDATA();
            // } catch (SAXException e) {
            // throw new IOException("Exception in endCDATA: "+ e.getMessage());
            // }
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#endDocument()
         */
        void endDocument() throws IOException {
            flushCDATA();
            state = inDocumentState;
            state.endDocument();
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#endElement(java.lang.String)
         */
        void endElement(String name) throws IOException {
            flushCDATA();
            state = inDocumentState;
            state.endElement(name);
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#startElement(java.lang.String,
         * javax.faces.component.UIComponent)
         */
        void startElement(String name, UIComponent component) throws IOException {
            flushCDATA();
            element = name;
            attributes = new AttributesImpl();
            state = elementState;
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#writeComment(java.lang.Object)
         */
        void writeComment(Object comment) throws IOException {
            flushCDATA();
            state = inDocumentState;
            state.writeComment(comment);
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#writeText(char[], int, int)
         */
        void writeText(char[] text, int off, int len) throws IOException {
            try {
                xmlConsumer.characters(text, off, len);
            } catch (SAXException e) {
                throw new IOException("Sax exceptions in writeText: " + e.getMessage());
            }
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#writeText(java.lang.Object, java.lang.String)
         */
        void writeText(Object text, String property) throws IOException {
            writeText(text.toString().toCharArray(), 0, text.toString().length());
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#write(char[], int, int)
         */
        void write(char[] cbuf, int off, int len) throws IOException {
            writeText(cbuf, off, len);
        }
    }

    /**
     * @author shura State in element declsration. Collect attributes, on any other eventss - generate SAX startElement()
     */
    private final class ElementState extends XMLResponseWriterState {
        /**
         * Generate SAX StartElement event
         *
         * @throws IOException
         */
        void flushElement() throws IOException {
            try {
                xmlConsumer.startElement(getNamespaceURI(), element, element, attributes);
            } catch (SAXException e) {
                throw new IOException("Exception in startElement: " + e.getMessage());
            } finally {
                element = null;
                attributes = null;
            }
        }

        void writeAttribute(String name, Object value, String property) throws IOException {
            attributes.addAttribute(getNamespaceURI(), name, name, "id".equalsIgnoreCase(name) ? "ID" : "CDATA",
                value.toString());
        }

        void writeURIAttribute(String name, Object value, String property) throws IOException {
            String uri = value.toString();

            // TODO - perform encodeActionURL() or ???
            attributes.addAttribute(getNamespaceURI(), name, name, "CDATA", uri);
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#endElement(java.lang.String)
         */
        void endElement(String name) throws IOException {

            //
            flushElement();
            state = inDocumentState;
            state.endElement(name);
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#startElement(java.lang.String,
         * javax.faces.component.UIComponent)
         */
        void startElement(String name, UIComponent component) throws IOException {

            //
            flushElement();
            element = name;
            attributes = new AttributesImpl();
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#writeComment(java.lang.Object)
         */
        void writeComment(Object comment) throws IOException {

            // TODO Auto-generated method stub
            flushElement();
            state = inDocumentState;
            state.writeComment(comment);
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#writeText(char[], int, int)
         */
        void writeText(char[] text, int off, int len) throws IOException {

            // TODO Auto-generated method stub
            flushElement();
            state = cdataState;
            state.writeText(text, off, len);
        }

        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#writeText(java.lang.Object, java.lang.String)
         */
        void writeText(Object text, String property) throws IOException {

            // TODO Auto-generated method stub
            flushElement();
            state = cdataState;
            state.writeText(text, property);
        }
    }

    /**
     * @author shura
     *         <p/>
     *         State in regular document. Disabled attributes & startDocument.
     */
    private final class InDocumentState extends XMLResponseWriterState {
        void startElement(String name, UIComponent component) {
            element = name;
            attributes = new AttributesImpl();
            state = elementState;
        }

        void writeComment(Object comment) throws IOException {
            String remark = comment.toString();

            try {
                xmlLexicalHandler.comment(remark.toCharArray(), 0, remark.length());
            } catch (SAXException e) {
                throw new IOException("Comment SAX exception :" + e.getMessage());
            }
        }

        void writeText(Object o, String property) throws IOException {
            writeText(o.toString().toCharArray(), 0, o.toString().length());
        }

        void writeText(char[] text, int start, int lenght) throws IOException {

            // try {
            // xmlConsumer.startCDATA();
            // } catch (SAXException e) {
            // throw new IOException("Sax exceptions in writeText: "+
            // e.getMessage());
            // } finally {
            state = cdataState;

            // }
            state.writeText(text, start, lenght);
        }

        void endElement(String name) throws IOException {
            try {
                xmlConsumer.endElement(getNamespaceURI(), name, name);
            } catch (SAXException e) {
                throw new IOException("Sax exceptions in endElement: " + e.getMessage());
            }
        }

        void endDocument() throws IOException {
            try {
                xmlConsumer.endDocument();
            } catch (SAXException e) {
                throw new IOException("Sax exceptions in endDocument" + e.getMessage());
            } finally {

                // after endDocument all events disabled ...
                state = new XMLResponseWriterState();
            }
        }
    }

    // Private classes

    /**
     * @author shura
     *         <p/>
     *         state before startDocument - only allow startDocument.
     */
    private final class NotDocumentState extends XMLResponseWriterState {
        /*
         * (non-Javadoc)
         *
         * @see org.apache.cocoon.components.faces.context.XMLResponseWriterState#startDocument()
         */
        void startDocument() throws IOException {
            try {

                //
                xmlConsumer.startDocument();
            } catch (SAXException e) {

                //
                throw new IOException("StartDocument SAX exception :" + e.getMessage());
            } finally {
                state = inDocumentState;
            }
        }
    }
}
