/**
 * License Agreement.
 *
 *  JBoss RichFaces - Ajax4jsf Component Library
 *
 * Copyright (C) 2007  Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */

package org.richfaces.json;

import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;

import org.ajax4jsf.Messages;
import org.ajax4jsf.javascript.JSEncoder;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;


/**
 * @author shura
 * SAX content handler for serialise events as JavaScript function.
 */
public class JSContentHandler implements ContentHandler,LexicalHandler {

    protected Writer outputWriter;

    private static final JSEncoder encoder = new JSEncoder();

    private static final char S_EOL[] = System.getProperty("line.separator")
            .toCharArray();

    private static final char S_DOCUMENT_START[] = "(".toCharArray();

    private static final char S_DOCUMENT_ENF[] = ");".toCharArray();

    private static final char S_DOCUMENT_3[] = "\"?>".toCharArray();

    private static final char S_OBJECT_START[] = "{".toCharArray();
    private static final char[] S_OBJECT_END = "}".toCharArray();

    private static final char S_ELEMENT_START[] = "new E(".toCharArray();

    private static final char[] S_ELEMENT_CLOSE = ")".toCharArray();

    private static final char S_ATTRIBUTES_END[] = "}".toCharArray();

    private static final char S_ATTRIBUTES_START[] = ",{".toCharArray();

    private static final char S_ELEMENT_END_START_TAG[] = ",[".toCharArray();

    private static final char S_ELEMENT_END[] = "])".toCharArray();

    private static final char S_TEXT_START[] = "new T(".toCharArray();

    private static final char S_TEXT_END[] = ")".toCharArray();

    private static final char S_CDATA_START[] = "new D('".toCharArray();

    private static final char S_CDATA_END[] = "')".toCharArray();

    private static final char S_COMMENT_START[] = "new C('".toCharArray();

    private static final char S_COMMENT_END[] = "')".toCharArray();

    private static final char S_PROCINSTR_START[] = "<?".toCharArray();

    private static final char S_PROCINSTR_END[] = "?>".toCharArray();

    private static final char C_LT = '<';

    private static final char C_GT = '>';

    private static final char C_SPACE = ' ';

    private static final char C_QUOTE = '\'';

    private static final char C_NSSEP = ':';

    private static final char C_COMMA = ',';

    private static final boolean DEBUG = false;

    /* ====================================================================== */
    private boolean hanging_element = false;

    /** True if we are processing the prolog. */
    private boolean beforeDocumentStart = true;

    /** True if we are processing the DTD. */
    private boolean processing_dtd = false;

    /** True if we are processing the DTD. */
    private boolean processing_cdata = false;
    /** The <code>DocType</code> instance representing the document. */
    // protected DocType doctype = null;

    protected int level;

    protected char[] indentBuffer;


    private Locator locator;

    public static final String DEFAULT_ENCODING = "ISO-8859-1";
    
    public JSContentHandler() {
	}
	/**
     * @param outputWriter
     */
    public JSContentHandler(Writer outputWriter) {
        this.outputWriter = outputWriter;
    }
    /**
     * @throws java.io.IOException
     */
    public void close() throws IOException {
        outputWriter.close();
    }

    // ContentHandler Methods
    /* (non-Javadoc)
     * @see org.xml.sax.ContentHandler#characters(char[], int, int)
     */
    public void characters(char[] ch, int start, int length)
            throws SAXException {
        if (this.beforeDocumentStart || level < 0)
            return;
        try {
        	if (level != 0 && !this.closeElement(false) && this.processing_cdata == false) {
                this.outputWriter.write(C_COMMA);
            }
            if (DEBUG) {
                this.outputWriter.write('[');
                this.outputWriter.write(Integer.toString(level));
                this.outputWriter.write(']');
            }
            if (this.processing_cdata == false) {
                this.outputWriter.write(S_TEXT_START);
            }
            this.encodeText(ch, start, length);
            if (this.processing_cdata == false) {
                this.outputWriter.write(S_TEXT_END);
            }
        } catch (IOException e) {
            throw new SAXException("Write error",e);
        }
    }
    /* (non-Javadoc)
     * @see org.xml.sax.ContentHandler#endDocument()
     */
    public void endDocument() throws SAXException {
        this.beforeDocumentStart = true;
    	
    	if (this.level != 0) {
            throw new SAXException(Messages.getMessage(Messages.OPEN_CLOSE_TAGS_DO_NOT_MATCH_ERROR));
        }
        // Write parameters after parsing and final function )
        try {
        	this.outputWriter.write(S_EOL);
            this.outputWriter.flush();
        } catch (IOException e) {
            throw new SAXException("Write error",e);
        }
    }
    
    
    /* (non-Javadoc)
     * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
     */
    public void endElement(String uri, String localName, String qName)
            throws SAXException {
        this.level--;
        if (closeElement(true))
            return;
        try {
            this.outputWriter.write(S_ELEMENT_END); // [</]
            if (DEBUG) {
                this.outputWriter.write('[');
                this.outputWriter.write(qName);
                this.outputWriter.write('-');
                this.outputWriter.write(uri);
                this.outputWriter.write(']');
            }
        } catch (IOException e) {
            throw new SAXException("Write error",e);
        }
        //        this.outputWriter.write(qual);
        //        this.outputWriter.write(C_GT); // [>]
    }

    /**
     * Write the end part of a start element (if necessary).
     * 
     * @param end_element
     *            Whether this method was called because an element is being
     *            closed or not.
     * @return <b>true </b> if this call successfully closed the element (and no
     *         further <code>&lt;/element&gt;</code> is required.
     */
    protected boolean closeElement(boolean end_element) throws SAXException {
    	if (!hanging_element)
            return (false);
        try {
            if (end_element)
                this.outputWriter.write(S_ELEMENT_CLOSE); // [ />]
            else
                this.outputWriter.write(S_ELEMENT_END_START_TAG); // [>]
        } catch (IOException e) {
            throw new SAXException("Write error",e);
        }
        this.hanging_element = false;
        return (true);
    }

    /* (non-Javadoc)
     * @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String)
     */
    public void endPrefixMapping(String prefix) throws SAXException {

   }
    /* (non-Javadoc)
     * @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int)
     */
    public void ignorableWhitespace(char[] ch, int start, int length)
            throws SAXException {
        // TODO Auto-generated method stub

    }
    /* (non-Javadoc)
     * @see org.xml.sax.ContentHandler#processingInstruction(java.lang.String, java.lang.String)
     */
    public void processingInstruction(String target, String data)
            throws SAXException {
        // TODO Auto-generated method stub

    }
    /* ====================================================================== */

    /**
     * Receive an object for locating the origin of SAX document events.
     */
    public final void setDocumentLocator(Locator locator) {
        this.locator = locator;
    }

    /**
     * Return the public identifier for the current document event.
     *
     * @return A <code>String</code> containing the public identifier,
     *         or <b>null</b> if none is available.
     */
    public String getPublicId() {
        return(this.locator == null? null: this.locator.getPublicId());
    }

    /**
     * Return the system identifier for the current document event.
     *
     * @return A <code>String</code> containing the system identifier,
     *         or <b>null</b> if none is available.
     */
    public String getSystemId() {
        return(this.locator == null? null: this.locator.getSystemId());
    }
    
    /**
     * Return the line number where the current document event ends.
     *
     * @return The line number, or -1 if none is available.
     */
    public int getLineNumber() {
        return(this.locator == null? -1: this.locator.getLineNumber());
    }

    /**
     * Return the column number where the current document event ends.
     *
     * @return The column number, or -1 if none is available.
     */
    public int getColumnNumber() {
        return(this.locator == null? -1: this.locator.getColumnNumber());
    }

    /**
     * Return a <code>String</code> describing the current location.
     */
    protected String getLocation() {
        if (this.locator == null) return("");
        StringBuffer buf = new StringBuffer(" (");
        if (this.getSystemId() != null) {
            buf.append(this.getSystemId());
            buf.append(' ');
        }
        buf.append("line " + this.getLineNumber());
        buf.append(" col " + this.getColumnNumber());
        buf.append(')');
        return(buf.toString());
    }

    /* ====================================================================== */
    /* (non-Javadoc)
     * @see org.xml.sax.ContentHandler#skippedEntity(java.lang.String)
     */
    public void skippedEntity(String name) throws SAXException {
        // TODO Auto-generated method stub

    }
    /* (non-Javadoc)
     * @see org.xml.sax.ContentHandler#startDocument()
     */
    public void startDocument() throws SAXException {
        this.beforeDocumentStart = false;
        this.processing_cdata = false;
        this.level = 0;
        /* We have a document type. */
//        if (this.doctype != null) {
//
//            String root_name = this.doctype.getName();
//            /* Check the DTD and the root element */
//            if (!root_name.equals(qual)) {
//                throw new SAXException("Root element name \"" + root_name
//                        + "\" declared by document type declaration differs "
//                        + "from actual root element name \"" + qual + "\"");
//            }
            /* Output the <!DOCTYPE ...> declaration. */
            // this.outputWriter.write(this.doctype.toString());
//        }
    }

    /* (non-Javadoc)
     * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
     */
    public void startElement(String uri, String localName,
            String qName, Attributes attributes) throws SAXException {
        try {
            if (!this.closeElement(false) && this.level > 0) {
                this.outputWriter.write(C_COMMA);
            }
            this.outputWriter.write(S_ELEMENT_START); // [<]
            if (DEBUG) {
                this.outputWriter.write('[');
                this.outputWriter.write(uri);
                this.outputWriter.write(']');
            }
            this.outputWriter.write(C_QUOTE);
            this.outputWriter.write(qName);
            this.outputWriter.write(C_QUOTE);

//            if (attributes.getLength() > 0) {
                this.outputWriter.write(S_ATTRIBUTES_START);
                // TODO - implementing namespaces !
                //        for (int x = 0; x < namespaces.length; x++) {
                //            this.outputWriter.write(S_ELEMENT_4); // [ xmlns]
                //            if (namespaces[x][Namespaces.NAMESPACE_PREFIX].length() > 0) {
                //                this.outputWriter.write(C_NSSEP); // [:]
                //                this.outputWriter.write(namespaces[x][Namespaces.NAMESPACE_PREFIX]);
                //            }
                //            this.outputWriter.write(S_ELEMENT_1); // [="]
                //            this.encode(namespaces[x][Namespaces.NAMESPACE_URI]);
                //            this.outputWriter.write(C_QUOTE); // ["]
                //        }
                for (int x = 0; x < attributes.getLength(); x++) {
                    if (0 != x) {
                        this.outputWriter.write(C_COMMA);
                        this.outputWriter.write(C_SPACE); // [ ]
                    }
                    if (DEBUG) {
                        this.outputWriter.write('[');
                        this.outputWriter.write(attributes.getURI(x));
                        this.outputWriter.write(']');
                    }
                    String attrName = attributes.getQName(x);
                    // For JavaScript any attributes names illegal ...
                    // replate with correct names.
                    if (attrName.equalsIgnoreCase("class")) {
                        attrName = "className";
                    }
                    this.outputWriter.write(C_QUOTE); // [']
                    this.outputWriter.write(attrName);
                    this.outputWriter.write(C_QUOTE); // [']
                    this.outputWriter.write(C_NSSEP); // [:]
                	//TODO by nick - fix namespace handling - they shouldn't be encoded
                    this.encodeAttributeValue(attributes, x);
                }

                this.outputWriter.write(S_ATTRIBUTES_END);
//            }
            this.level++;
            this.hanging_element = true;
        } catch (IOException e) {

            throw new SAXException("write error",e);
        }
    }
    
    protected void encodeAttributeValue(Attributes attributes, int idx) throws SAXException, IOException {
        this.outputWriter.write(C_QUOTE); // [']
    	this.encode(attributes.getValue(idx));
        this.outputWriter.write(C_QUOTE); // [']
    }
    
    protected void encodeText(char[] chars, int start, int length) throws SAXException, IOException {
    	this.encode(chars, start, length);
    }
    
    /* (non-Javadoc)
     * @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String)
     */
    public void startPrefixMapping(String prefix, String uri)
            throws SAXException {

    }
    
    /* (non-Javadoc)
     * @see org.xml.sax.ext.LexicalHandler#comment(char[], int, int)
     */
    public void comment(char[] ch, int start, int length) throws SAXException {
        if (this.beforeDocumentStart || level < 0)
            return;
        try {
            if (!this.closeElement(false) && level != 0) {
                this.outputWriter.write(C_COMMA);
            }
            if (DEBUG) {
                this.outputWriter.write('[');
                this.outputWriter.write(Integer.toString(level));
                this.outputWriter.write(']');
            }
//            this.outputWriter.write(C_QUOTE);
            this.outputWriter.write(S_COMMENT_START);
            this.encode(ch, start, length);
            this.outputWriter.write(S_COMMENT_END);
//            this.outputWriter.write(C_QUOTE);
        } catch (IOException e) {
            throw new SAXException("Write error",e);
        }


    }
    /* (non-Javadoc)
     * @see org.xml.sax.ext.LexicalHandler#endCDATA()
     */
    public void endCDATA() throws SAXException {
        if (this.beforeDocumentStart || level < 0)
            return;
        try {
            if (this.closeElement(false)) {
                return;
            }
            if (DEBUG) {
                this.outputWriter.write('[');
                this.outputWriter.write(Integer.toString(level));
                this.outputWriter.write(']');
            }
            this.outputWriter.write(S_CDATA_END);
            this.processing_cdata = false;
        } catch (IOException e) {
            throw new SAXException("Write error",e);
        }
    }
    /* (non-Javadoc)
     * @see org.xml.sax.ext.LexicalHandler#endDTD()
     */
    public void endDTD() throws SAXException {
        // TODO Auto-generated method stub

    }
    /* (non-Javadoc)
     * @see org.xml.sax.ext.LexicalHandler#endEntity(java.lang.String)
     */
    public void endEntity(String name) throws SAXException {
        // TODO Auto-generated method stub

    }
    /* (non-Javadoc)
     * @see org.xml.sax.ext.LexicalHandler#startCDATA()
     */
    public void startCDATA() throws SAXException {
        if (this.beforeDocumentStart || level < 0)
            return;
        try {
            if (!this.closeElement(false) && level != 0) {
                this.outputWriter.write(C_COMMA);
            }
            if (DEBUG) {
                this.outputWriter.write('[');
                this.outputWriter.write(Integer.toString(level));
                this.outputWriter.write(']');
            }
            this.outputWriter.write(S_CDATA_START);
            this.processing_cdata = true;
        } catch (IOException e) {
            throw new SAXException("Write error",e);
        }
    }
    /* (non-Javadoc)
     * @see org.xml.sax.ext.LexicalHandler#startDTD(java.lang.String, java.lang.String, java.lang.String)
     */
    public void startDTD(String name, String publicId, String systemId)
            throws SAXException {
        // TODO Auto-generated method stub

    }
    /* (non-Javadoc)
     * @see org.xml.sax.ext.LexicalHandler#startEntity(java.lang.String)
     */
    public void startEntity(String name) throws SAXException {
        // TODO Auto-generated method stub

    }

    /*
     * (non-Javadoc)
     * 
     * @see org.apache.cocoon.components.serializers.EncodingSerializer#writeIndent(int)
     */
    protected void writeIndent(int indent) throws SAXException {
        try {
            this.outputWriter.write("\n".toCharArray(), 0, 1);
            if (indent > 0) {
                this.outputWriter.write(assureIndentBuffer(indent), 0, indent);
            }
        } catch (IOException e) {
            throw new SAXException("Write error",e);
        }

    }

    private char[] assureIndentBuffer(int size) {
        if (indentBuffer == null || indentBuffer.length < size) {
            indentBuffer = new char[size];
            Arrays.fill(indentBuffer, ' ');
        }
        return indentBuffer;
    }
    
    /**
     * Encode and write a <code>String</code>
     */
    protected void encode(String data)
    throws SAXException {
        char array[] = data.toCharArray();
        this.encode(array, 0, array.length);
    }
    
    /**
     * Encode and write an array of characters.
     */
    protected void encode(char data[])
    throws SAXException {
        this.encode(data, 0, data.length);
    }
    
    /**
     * Encode and write a specific part of an array of characters.
     */
    protected void encode(char data[], int start, int length)
    throws SAXException {
        int end = start + length;
        
        if (data == null) throw new NullPointerException("Null data");
        if ((start < 0) || (start > data.length) || (length < 0) ||
            (end > data.length) || (end < 0))
            throw new IndexOutOfBoundsException("Invalid data");
        if (length == 0) return;
        
        try {
            for (int x = start; x < end; x++) {
                char c = data[x];
                
                if (  JSContentHandler.encoder.compile(c)) {
                    continue;
                }
                
                if (start != x) this.outputWriter.write(data, start, x - start );
                this.outputWriter.write(JSContentHandler.encoder.encode(c));
                start = x + 1;
                continue;
            }
            if (start != end) this.outputWriter.write(data, start, end - start );
        } catch (IOException e) {
            throw new SAXException("Write error",e);
        }
    }
    
    protected boolean isProcessingCdata() {
    	return processing_cdata;
    }

    protected boolean isBeforeDocumentStart() {
    	return beforeDocumentStart;
    }
}
