/* -*-             c-basic-offset: 4; indent-tabs-mode: nil; -*-  //------100-columns-wide------>|*/
// for license please see accompanying LICENSE.txt file (available also at http://www.xmlpull.org/)

package org.xmlpull.v1.sax2;

import java.io.InputStream;
import java.io.IOException;
import java.io.Reader;

// not J2ME classes -- remove if you want to run in MIDP devices
import java.net.URL;
import java.net.MalformedURLException;


// not J2ME classes
import java.io.FileInputStream;
import java.io.FileNotFoundException;

import org.xml.sax.Attributes;
import org.xml.sax.DTDHandler;
import org.xml.sax.ContentHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

/**
 * SAX2 Driver that pulls events from XmlPullParser
 * and converts them into SAX2 callbacks.
 *
 * @author <a href="http://www.extreme.indiana.edu/~aslom/">Aleksander Slominski</a>
 */

public class Driver implements Locator, XMLReader, Attributes
{

    protected static final String DECLARATION_HANDLER_PROPERTY =
        "http://xml.org/sax/properties/declaration-handler";

    protected static final String LEXICAL_HANDLER_PROPERTY =
        "http://xml.org/sax/properties/lexical-handler";

    protected static final String NAMESPACES_FEATURE =
        "http://xml.org/sax/features/namespaces";

    protected static final String NAMESPACE_PREFIXES_FEATURE =
        "http://xml.org/sax/features/namespace-prefixes";

    protected static final String VALIDATION_FEATURE =
        "http://xml.org/sax/features/validation";

    protected static final String APACHE_SCHEMA_VALIDATION_FEATURE =
        "http://apache.org/xml/features/validation/schema";

    protected static final String APACHE_DYNAMIC_VALIDATION_FEATURE =
        "http://apache.org/xml/features/validation/dynamic";

    protected ContentHandler contentHandler = new DefaultHandler();
    protected ErrorHandler errorHandler = new DefaultHandler();;

    protected String systemId;

    protected XmlPullParser pp;

    //private final static boolean DEBUG = false;

    /**
     */
    public Driver() throws XmlPullParserException {
        final XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
        factory.setNamespaceAware(true);
        pp = factory.newPullParser();
    }

    public Driver(XmlPullParser pp) throws XmlPullParserException {
        this.pp = pp;
    }

    // -- Attributes interface

    public int getLength() { return pp.getAttributeCount(); }
    public String getURI(int index) { return pp.getAttributeNamespace(index); }
    public String getLocalName(int index) { return pp.getAttributeName(index); }
    public String getQName(int index) {
        final String prefix = pp.getAttributePrefix(index);
        if(prefix != null) {
            return prefix+':'+pp.getAttributeName(index);
        } else {
            return pp.getAttributeName(index);
        }
    }
    public String getType(int index) { return pp.getAttributeType(index); }
    public String getValue(int index) { return pp.getAttributeValue(index); }

    public int getIndex(String uri, String localName) {
        for (int i = 0; i < pp.getAttributeCount(); i++)
        {
            if(pp.getAttributeNamespace(i).equals(uri)
               && pp.getAttributeName(i).equals(localName))
            {
                return i;
            }

        }
        return -1;
    }

    public int getIndex(String qName) {
        for (int i = 0; i < pp.getAttributeCount(); i++)
        {
            if(pp.getAttributeName(i).equals(qName))
            {
                return i;
            }

        }
        return -1;
    }

    public String getType(String uri, String localName) {
        for (int i = 0; i < pp.getAttributeCount(); i++)
        {
            if(pp.getAttributeNamespace(i).equals(uri)
               && pp.getAttributeName(i).equals(localName))
            {
                return pp.getAttributeType(i);
            }

        }
        return null;
    }
    public String getType(String qName) {
        for (int i = 0; i < pp.getAttributeCount(); i++)
        {
            if(pp.getAttributeName(i).equals(qName))
            {
                return pp.getAttributeType(i);
            }

        }
        return null;
    }
    public String getValue(String uri, String localName) {
        return pp.getAttributeValue(uri, localName);
    }
    public String getValue(String qName) {
        return pp.getAttributeValue(null, qName);
    }

    // -- Locator interface

    public String getPublicId() { return null; }
    public String getSystemId() { return systemId; }
    public int getLineNumber() { return pp.getLineNumber(); }
    public int getColumnNumber() { return pp.getColumnNumber(); }

    // --- XMLReader interface

    public boolean getFeature(String name)
        throws SAXNotRecognizedException, SAXNotSupportedException
    {
        if(NAMESPACES_FEATURE.equals(name)) {
            return pp.getFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES);
        } else if(NAMESPACE_PREFIXES_FEATURE.equals(name)) {
            return pp.getFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES);
        } else if(VALIDATION_FEATURE.equals(name)) {
            return pp.getFeature(XmlPullParser.FEATURE_VALIDATION);
            //        } else if(APACHE_SCHEMA_VALIDATION_FEATURE.equals(name)) {
            //            return false;  //TODO
            //        } else if(APACHE_DYNAMIC_VALIDATION_FEATURE.equals(name)) {
            //            return false; //TODO
        } else {
            return pp.getFeature(name);
            //throw new SAXNotRecognizedException("unrecognized feature "+name);
        }
    }

    public void setFeature (String name, boolean value)
        throws SAXNotRecognizedException, SAXNotSupportedException
    {
        try {
            if(NAMESPACES_FEATURE.equals(name)) {
                pp.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, value);
            } else if(NAMESPACE_PREFIXES_FEATURE.equals(name)) {
                if(pp.getFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES) != value) {
                    pp.setFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES, value);
                }
            } else if(VALIDATION_FEATURE.equals(name)) {
                pp.setFeature(XmlPullParser.FEATURE_VALIDATION, value);
                //          } else if(APACHE_SCHEMA_VALIDATION_FEATURE.equals(name)) {
                //              // can ignore as validation must be false ...
                //              //              if(true == value) {
                //              //                  throw new SAXNotSupportedException("schema validation is not supported");
                //              //              }
                //          } else if(APACHE_DYNAMIC_VALIDATION_FEATURE.equals(name)) {
                //              if(true == value) {
                //                  throw new SAXNotSupportedException("dynamic validation is not supported");
                //              }
            } else {
                pp.setFeature(name, value);
                //throw new SAXNotRecognizedException("unrecognized feature "+name);
            }
        } catch(XmlPullParserException ex) {
           // throw new SAXNotSupportedException("problem with setting feature "+name+": "+ex);
        }
    }

    public Object getProperty (String name)
        throws SAXNotRecognizedException, SAXNotSupportedException
    {
        if(DECLARATION_HANDLER_PROPERTY.equals(name)) {
            return null;
        } else if(LEXICAL_HANDLER_PROPERTY.equals(name)) {
            return null;
        } else {
            return pp.getProperty(name);
            //throw new SAXNotRecognizedException("not recognized get property "+name);
        }
    }

    public void setProperty (String name, Object value)
        throws SAXNotRecognizedException, SAXNotSupportedException
    {
        //
        if(DECLARATION_HANDLER_PROPERTY.equals(name)) {
            throw new SAXNotSupportedException("not supported setting property "+name);//+" to "+value);
        } else if(LEXICAL_HANDLER_PROPERTY.equals(name)) {
            throw new SAXNotSupportedException("not supported setting property "+name);//+" to "+value);
        } else {
            try {
                pp.setProperty(name, value);
            } catch(XmlPullParserException ex) {
                throw new SAXNotSupportedException("not supported set property "+name+": "+ ex);
            }
            //throw new SAXNotRecognizedException("not recognized set property "+name);
        }
    }

    public void setEntityResolver (EntityResolver resolver) {}

    public EntityResolver getEntityResolver () { return null; }

    public void setDTDHandler (DTDHandler handler) {}

    public DTDHandler getDTDHandler () { return null; }

    public void setContentHandler (ContentHandler handler)
    {
        this.contentHandler = handler;
    }

    public ContentHandler getContentHandler() { return contentHandler; }

    public void setErrorHandler(ErrorHandler handler) {
        this.errorHandler = handler;
    }

    public ErrorHandler getErrorHandler() { return errorHandler; }

    public void parse(InputSource source) throws SAXException, IOException
    {

        systemId = source.getSystemId();
        contentHandler.setDocumentLocator(this);

        final Reader reader = source.getCharacterStream();
        try {
            if (reader == null) {
                InputStream stream = source.getByteStream();
                final String encoding = source.getEncoding();

                if (stream == null) {
                    systemId = source.getSystemId();
                    if(systemId == null) {
                        SAXParseException saxException = new SAXParseException(
                            "null source systemId" , this);
                        errorHandler.fatalError(saxException);
                        return;
                    }
                    // NOTE: replace with Connection to run in J2ME environment
                    try {
                        final URL url = new URL(systemId);
                        stream = url.openStream();
                    } catch (MalformedURLException nue) {
                        try {
                            stream = new FileInputStream(systemId);
                        } catch (FileNotFoundException fnfe) {
                            final SAXParseException saxException = new SAXParseException(
                                "could not open file with systemId "+systemId, this, fnfe);
                            errorHandler.fatalError(saxException);
                            return;
                        }
                    }
                }
                pp.setInput(stream, encoding);
            } else {
                pp.setInput(reader);
            }
        } catch (XmlPullParserException ex)  {
            final SAXParseException saxException = new SAXParseException(
                "parsing initialization error: "+ex, this, ex);
            //if(DEBUG) ex.printStackTrace();
            errorHandler.fatalError(saxException);
            return;
        }

        // start parsing - move to first start tag
        try {
            contentHandler.startDocument();
            // get first event
            pp.next();
            // it should be start tag...
            if(pp.getEventType() != XmlPullParser.START_TAG) {
                final SAXParseException saxException = new SAXParseException(
                    "expected start tag not"+pp.getPositionDescription(), this);
                //throw saxException;
                errorHandler.fatalError(saxException);
                return;
            }
        } catch (XmlPullParserException ex)  {
            final SAXParseException saxException = new SAXParseException(
                "parsing initialization error: "+ex, this, ex);
            //ex.printStackTrace();
            errorHandler.fatalError(saxException);
            return;
        }

        // now real parsing can start!

        parseSubTree(pp);

        // and finished ...

        contentHandler.endDocument();
    }

    public void parse(String systemId) throws SAXException, IOException {
        parse(new InputSource(systemId));
    }


    public void parseSubTree(XmlPullParser pp) throws SAXException, IOException {
        this.pp = pp;
        final boolean namespaceAware = pp.getFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES);
        try {
            if(pp.getEventType() != XmlPullParser.START_TAG) {
                throw new SAXException(
                    "start tag must be read before skiping subtree"+pp.getPositionDescription());
            }
            final int[] holderForStartAndLength = new int[2];
            final StringBuilder rawName = new StringBuilder(16);
            String prefix = null;
            String name = null;
            int level = pp.getDepth() - 1;
            int type = XmlPullParser.START_TAG;

            LOOP:
            do {
                switch(type) {
                    case XmlPullParser.START_TAG:
                        if(namespaceAware) {
                            final int depth = pp.getDepth() - 1;
                            final int countPrev =
                                (level > depth) ? pp.getNamespaceCount(depth) : 0;
                            //int countPrev = pp.getNamespaceCount(pp.getDepth() - 1);
                            final int count = pp.getNamespaceCount(depth + 1);
                            for (int i = countPrev; i < count; i++)
                            {
                                contentHandler.startPrefixMapping(
                                    pp.getNamespacePrefix(i),
                                    pp.getNamespaceUri(i)
                                );
                            }
                            name = pp.getName();
                            prefix = pp.getPrefix();
                            if(prefix != null) {
                                rawName.setLength(0);
                                rawName.append(prefix);
                                rawName.append(':');
                                rawName.append(name);
                            }
                            startElement(pp.getNamespace(),
                                         name,
                                         // TODO Fixed this. Was "not equals".
                                         prefix == null ? name : rawName.toString());
                        } else {
                            startElement(pp.getNamespace(),
                                         pp.getName(),
                                         pp.getName());
                        }
                        //++level;

                        break;
                    case XmlPullParser.TEXT:
                        final char[] chars = pp.getTextCharacters(holderForStartAndLength);
                        contentHandler.characters(chars,
                                                  holderForStartAndLength[0], //start
                                                  holderForStartAndLength[1] //len
                                                 );
                        break;
                    case XmlPullParser.END_TAG:
                        //--level;
                        if(namespaceAware) {
                            name = pp.getName();
                            prefix = pp.getPrefix();
                            if(prefix != null) {
                                rawName.setLength(0);
                                rawName.append(prefix);
                                rawName.append(':');
                                rawName.append(name);
                            }
                            contentHandler.endElement(pp.getNamespace(),
                                                      name,
                                                      // TODO Fixed this. Was "not equals".
                                                      prefix == null ? name : rawName.toString()
                                                     );
                            // when entering show prefixes for all levels!!!!
                            final int depth = pp.getDepth();
                            final int countPrev =
                                (level > depth) ? pp.getNamespaceCount(pp.getDepth()) : 0;
                            int count = pp.getNamespaceCount(pp.getDepth() - 1);
                            // undeclare them in reverse order
                            for (int i = count - 1; i >= countPrev; i--)
                            {
                                contentHandler.endPrefixMapping(
                                    pp.getNamespacePrefix(i)
                                );
                            }
                        } else {
                            contentHandler.endElement(pp.getNamespace(),
                                                      pp.getName(),
                                                      pp.getName()
                                                     );

                        }
                        break;
                    case XmlPullParser.END_DOCUMENT:
                        break LOOP;
                }
                type = pp.next();
            } while(pp.getDepth() > level);
        } catch (XmlPullParserException ex)  {
            final SAXParseException saxException = new SAXParseException("parsing error: "+ex, this, ex);
            ex.printStackTrace();
            errorHandler.fatalError(saxException);
        }
    }

    /**
     * Calls {@link ContentHandler#startElement(String, String, String, Attributes) startElement}
     * on the <code>ContentHandler</code> with <code>this</code> driver object as the
     * {@link Attributes} implementation. In default implementation
     * {@link Attributes} object is valid only during this method call and may not
     * be stored. Sub-classes can overwrite this method to cache attributes.
     */
    protected void startElement(String namespace, String localName, String qName) throws SAXException {
        contentHandler.startElement(namespace, localName, qName, this);
    }

}
