/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program 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 program 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.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.xml;

import org.orbeon.oxf.common.OXFException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Implements a streamable subset of XPath.
 *
 * This implementation requires that all searched XPath expressions be set beforehand.
 *
 * The class can be used as a regular XMLReceiver, or can use a callback to start reading the
 * input SAX stream.
 *
 * Right now, only the first result asked will be streamed. All other results will be stored in SAX
 * stores. This is not optimal, but since there are no coroutines in Java, it is hard to implement
 * the more efficient way, which is to store only what is strictly required. In the best case
 * scenario, if the matches occur in the document in the order asked, everything will be streamed
 * (at least, in single node mode).
 *
 * TODO:
 *       o Do we need to build a stack of prefix mappings?
 *       o Implement other expressions!
 *       o Handle single node vs. all nodes
 *       o Handle duplicate expressions (e.g. /a/b/c added several times)
 */
public class XPathXMLReceiver implements XMLReceiver {

    private Map expressions;
    private List expressionsList;
    private Expression[] expressionsArray;
    private boolean canStream = true;
    private Runnable readInputCallback;
    private boolean readStarted;

    private Expression searchedExpression;
    private XMLReceiver output;

    private static class Expression {
        public Expression(String xpathExpression) {
            this.xpathExpression = xpathExpression;
        }

        public String xpathExpression;
        public ContentHandler expressionContentHandler;
        public SAXStore result;
    }

    public XPathXMLReceiver() {
    }

    public boolean addExpresssion(String xpathExpression, boolean nodeSet) {
        if (!supportsExpression(xpathExpression))
            canStream = false;
        if (expressions == null) {
            expressions = new HashMap();
            expressionsList = new ArrayList();
        }
        // TODO: Detect duplicate expressions?
        // Create expression
        Expression expression = new Expression(xpathExpression);
        expression.expressionContentHandler = getExpressionContentHandler(expression);

        expressions.put(xpathExpression, expression);
        expressionsList.add(expression);

        return canStream;
    }

    public boolean containsExpression(String xpathExpression) {
        return expressions.containsKey(xpathExpression);
    }

    public void setReadInputCallback(Runnable readInputCallback) {
        this.readInputCallback = readInputCallback;
    }

    public void selectContentHandler(String xpathExpression, XMLReceiver xmlReceiver) throws SAXException {
        if (!canStream)
            throw new OXFException("Cannot stream");
        Expression expression = (Expression) expressions.get(xpathExpression);
        if (expression == null)
            throw new OXFException("Undefined expression: " + xpathExpression);

        // Check if the expression has already been computed
        if (expression.result != null) {
            expression.result.replay(xmlReceiver);
            return;
        }

        // Start the read if needed
        if (!readStarted) {
            if (readInputCallback == null)
                throw new OXFException("Read not started and no read callback specified");
            searchedExpression = expression;
            output = xmlReceiver;
            readInputCallback.run();
            // At this point, all the stream is read
            searchedExpression = null;
            output = null;
        }

        // Well, if we had coroutines, we could continue the search from here. Instead, we have to
        // compute everything before. If we get here, it means we got an empty result.
    }

    public static boolean supportsExpression(String xpathExpression) {
        return getExpressionId(xpathExpression) != -1;
    }

    private static int getExpressionId(String xpathExpression) {
        // Add other expressions here when implemented
        // Identity transformation
        if ("/*".equals(xpathExpression)) // should also match on "/" ? can also match on /abc, /abc[1], etc.
            return 1;
        return -1;
    }

    private ContentHandler getExpressionContentHandler(Expression expression) {
        int expressionId = getExpressionId(expression.xpathExpression);
        switch (expressionId) {
            case 1:
                return new Handler1(expression);
//            case 2:
//                return new Handler2(expression);
            // Etc.
            default:
                return null;
        }
    }

    public void characters(char ch[], int start, int length) throws SAXException {
        for (int i = 0; i < expressionsArray.length; i++)
            expressionsArray[i].expressionContentHandler.characters(ch, start, length);
    }

    public void endDocument() throws SAXException {
        for (int i = 0; i < expressionsArray.length; i++)
            expressionsArray[i].expressionContentHandler.endDocument();
    }

    public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
        for (int i = 0; i < expressionsArray.length; i++)
            expressionsArray[i].expressionContentHandler.endElement(namespaceURI, localName, qName);
    }

    public void endPrefixMapping(String prefix) throws SAXException {
        for (int i = 0; i < expressionsArray.length; i++)
            expressionsArray[i].expressionContentHandler.endPrefixMapping(prefix);
    }

    public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
        for (int i = 0; i < expressionsArray.length; i++)
            expressionsArray[i].expressionContentHandler.ignorableWhitespace(ch, start,  length);
    }

    public void processingInstruction(String target, String data) throws SAXException {
        for (int i = 0; i < expressionsArray.length; i++)
            expressionsArray[i].expressionContentHandler.processingInstruction(target, data);
    }

    public void setDocumentLocator(Locator locator) {
    }

    public void skippedEntity(String name) throws SAXException {
        for (int i = 0; i < expressionsArray.length; i++)
            expressionsArray[i].expressionContentHandler.skippedEntity(name);
    }

    public void startDocument() throws SAXException {
        // Setup expression content handlers
        expressionsArray = new Expression[expressionsList.size()];
        expressionsList.toArray(expressionsArray);
        readStarted = true;

        for (int i = 0; i < expressionsArray.length; i++)
            expressionsArray[i].expressionContentHandler.startDocument();
    }

    public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
        for (int i = 0; i < expressionsArray.length; i++)
            expressionsArray[i].expressionContentHandler.startElement(namespaceURI, localName, qName, atts);
    }

    public void startPrefixMapping(String prefix, String uri) throws SAXException {
        for (int i = 0; i < expressionsArray.length; i++)
            expressionsArray[i].expressionContentHandler.startPrefixMapping(prefix, uri);
    }

    private static abstract class ExpressionSearchHandler extends ForwardingXMLReceiver {
        protected Expression expresssion;

        protected ExpressionSearchHandler(Expression expresssion) {
            this.expresssion = expresssion;
        }
    }

    /**
     * The root element.
     */
    private class Handler1 extends ExpressionSearchHandler {

        private int level = 0;

        public Handler1(Expression expresssion) {
            super(expresssion);
        }

        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
            level++;
            if (level == 1) {
                // Found!
                setForward(true);
                if (searchedExpression == expresssion) {
                    // Output directly
                    setXMLReceiver(output);
                } else {
                    // Store result
                    expresssion.result = new SAXStore();
                    setXMLReceiver(expresssion.result);
                }
            }
            super.startElement(namespaceURI, localName, qName, atts);
        }

        public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
            level--;
            super.endElement(namespaceURI, localName, qName);
            if (level == 0)
                setForward(false);
        }
    }

    public void startDTD(String name, String publicId, String systemId) throws SAXException {
        // Ignore
    }

    public void endDTD() throws SAXException {
        // Ignore
    }

    public void startEntity(String name) throws SAXException {
        // Ignore
    }

    public void endEntity(String name) throws SAXException {
        // Ignore
    }

    public void startCDATA() throws SAXException {
        // Ignore
    }

    public void endCDATA() throws SAXException {
        // Ignore
    }

    public void comment(char[] ch, int start, int length) throws SAXException {
        // Ignore
    }

    //    private class Handler2 extends ExpressionSearchHandler {
//
//        public Handler2(Expression expresssion) {
//            super(expresssion);
//        }
//
//        public void characters(char ch[], int start, int length) throws SAXException {
//            output.characters(ch, start, length);
//        }
//
//        public void endDocument() throws SAXException {
//            output.endDocument();
//        }
//
//        public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
//            output.endElement(namespaceURI, localName, qName);
//        }
//
//        public void endPrefixMapping(String prefix) throws SAXException {
//            output.endPrefixMapping(prefix);
//        }
//
//        public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
//            output.ignorableWhitespace(ch, start,  length);
//        }
//
//        public void processingInstruction(String target, String data) throws SAXException {
//            output.processingInstruction(target, data);
//        }
//
//        public void setDocumentLocator(Locator locator) {
//        }
//
//        public void skippedEntity(String name) throws SAXException {
//            output.skippedEntity(name);
//        }
//
//        public void startDocument() throws SAXException {
//            output.startDocument();
//        }
//
//        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
//            output.startElement(namespaceURI, localName, qName, atts);
//        }
//
//        public void startPrefixMapping(String prefix, String uri) throws SAXException {
//            output.startPrefixMapping(prefix, uri);
//        }
//    }
}
