/*   Copyright 2004 The Apache Software Foundation
 *
 *   Licensed 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.xmlbeans.impl.store;

import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.apache.xmlbeans.XmlRuntimeException;
import org.apache.xmlbeans.impl.common.XPath;
import org.apache.xmlbeans.impl.store.Cursor.PathEngine;
import org.apache.xmlbeans.impl.store.Cursor.Selections;

import java.util.HashMap;
import java.util.List;

/**
 * Represents a precompiled path expression
 */

public abstract class Path
{
    static Selections newSelections ( )
    {
        return new Selections();
    }

    static PathEngine select (
        Root r, Splay s, int p, String pathExpr, XmlOptions options )
    {
        Path path = getPath( pathExpr, options );
        return (path == null) ? null : path.execute( r, s, p, options );
    }

    public static String _useXqrlForXpath = "use xqrl for xpath";

    public static String _useXbeanForXpath = "use xbean for xpath";

    public static Path getPath ( String pathExpr )
    {
        return getPath( pathExpr, false, null );
    }
    
    public static Path getPath ( String pathExpr, XmlOptions options )
    {
        return
            getPath(
                pathExpr,
                XmlOptions.maskNull( options ).hasOption( _useXqrlForXpath ),
                options );
    }

    public static Path getPath ( String pathExpr, boolean xqrl, XmlOptions options )
    {
        Object path = null;
        
        synchronized ( _xbeanPathCache )
        {
            if (!xqrl)
            {
                path = _xbeanPathCache.get( pathExpr );
            }

            if (path == null)
                path = _xqrlPathCache.get( pathExpr );

            if (path == null)
            {
                String pathStr = getCompiledPath( pathExpr, xqrl, options );
                
                path =
                    (pathStr == null)
                        ? null
                        : getPath( pathExpr, xqrl, options );
            }
        }

        return (Path) path;
    }
    
    public static String getCompiledPath ( String pathExpr, XmlOptions options )
    {
        return getCompiledPath( pathExpr, false, options );
    }

    private static String getCurrentNodeVar ( XmlOptions options )
    {
        String currentNodeVar = "this";

        options = XmlOptions.maskNull( options );
        
        if (options.hasOption( XmlOptions.XQUERY_CURRENT_NODE_VAR ))
        {
            currentNodeVar = (String) options.get( XmlOptions.XQUERY_CURRENT_NODE_VAR );

            if (currentNodeVar.startsWith( "$" ))
            {
                throw
                    new IllegalArgumentException(
                        "Omit the '$' prefix for the current node variable" );
            }
        }

        return currentNodeVar;
    }
    
    public static String getCompiledPath ( String pathExpr, boolean xqrl, XmlOptions options )
    {
        Path path = null;
        
        options = XmlOptions.maskNull( options );
        
        // ensure options doesn't contain XQuery variable mapping.
        // someday we should implement this in all the pathing engines
        // but for now it only applies to the xqrl.
        if (options.hasOption( XmlOptions.XQUERY_VARIABLE_MAP ))
        {
            throw
                new XmlRuntimeException(
                    "XmlOptions.XQUERY_VARIABLE_MAP is not allowed in XPath expressions.");
        }

        String currentNodeVar = getCurrentNodeVar( options );

        synchronized ( _xbeanPathCache )
        {
            assert (xqrl |= options.hasOption( _useXqrlForXpath )) || true;

            if (!xqrl || options.hasOption( _useXbeanForXpath ))
            {
                path = (Path) _xbeanPathCache.get( pathExpr );

                if (path == null)
                {
                    path = XbeanPathImpl.create( pathExpr, currentNodeVar );

                    if (path != null)
                        _xbeanPathCache.put( path.getPathExpr(), path );
                }
                if (path == null)
                {
                    path = JaxenPathImpl.create( pathExpr, currentNodeVar );

                    if (path != null)
                        _xbeanPathCache.put( path.getPathExpr(), path );
                }
            }

            if (path == null)
            {
                assert ! options.hasOption( _useXbeanForXpath );

                path = (Path) _xqrlPathCache.get( pathExpr );

                if (path == null)
                {
                    path = XqrlPathImpl.create( pathExpr, options );

                    if (path != null)
                        _xqrlPathCache.put( path.getPathExpr(), path );
                }
            }
        }

        return path == null ? null : path.getPathExpr();
    }

    public interface Query
    {
        PathEngine  executePath  ( Root r, Splay s, int p, XmlOptions options );
        XmlCursor   executeQuery ( Cursor c, XmlOptions options );
        XmlObject[] executeQuery ( Type type, XmlOptions options );
        String      getQueryExpr ( );
    }

//    public static Query getQuery ( String queryExpr )
//    {
//        return getQuery( queryExpr, null );
//    }
    
    public static Query getQuery ( String queryExpr, XmlOptions options )
    {
        Object query = null;
        
        synchronized ( _xqrlQueryCache )
        {
            query = _xqrlQueryCache.get( queryExpr );

            if (query == null)
            {
                String queryStr = getCompiledQuery( queryExpr, options );
                
                query =
                    (queryStr == null) ? null : getQuery( queryExpr, options );
            }
        }

        return (Query) query;
    }
    
    public static String getCompiledQuery ( String queryExpr, XmlOptions options )
    {
        Query query = null;
        
        synchronized ( _xqrlQueryCache )
        {
            query = (Query) _xqrlQueryCache.get( queryExpr );

            if (query == null)
            {
                query = XqrlDelegate.compileQuery( queryExpr, options );

                if (query != null)
                    _xqrlQueryCache.put( query.getQueryExpr(), query );
            }
        }

        return query == null ? null : query.getQueryExpr();
    }

    public static XmlCursor query (
        Cursor c, String queryExpr, XmlOptions options )
    {
        Query query = getQuery( queryExpr, options );
        return (query == null) ? null : query.executeQuery( c, options );
    }

    public static XmlObject[] query (
        Type type, String queryExpr, XmlOptions options )
    {
        Query query = getQuery( queryExpr, options );
        return (query == null) ? null : query.executeQuery( type, options );
    }

    protected abstract PathEngine execute (
        Root r, Splay s, int p, XmlOptions options );

    protected abstract String getPathExpr ( );

    private static class XqrlPathImpl extends Path
    {
        private XqrlPathImpl ( String pathExpr, Query compiledPath )
        {
            _pathExpr = pathExpr;
            _compiledPath = compiledPath;
        }

        private String _pathExpr;
        private Query  _compiledPath;

        static Path create ( String pathExpr, XmlOptions options )
        {
            return new XqrlPathImpl(
                pathExpr,
                XqrlDelegate.compilePath( pathExpr, options ) );
        }

        protected PathEngine execute (
            Root r, Splay s, int p, XmlOptions options )
        {
            return _compiledPath.executePath( r, s, p, options );
        }
        
        protected String getPathExpr ( ) { return _pathExpr; }
    }

    //
    //
    //

    private static final class XbeanPathImpl extends Path
    {
        private XbeanPathImpl (
            XPath xpath, String pathExpr, String currentNodeVar )
        {
            _pathExpr = pathExpr;
            _xpath = xpath;
            _currentNodeVar = currentNodeVar;
        }

//            try
//            {
//                String currentNodeVar = "this";
//                
//                if (XmlOptions.XQUERY_CURRENT_NODE_VAR.has( options ))
//                {
//                    currentNodeVar =
//                        "$" + XmlOptions.XQUERY_CURRENT_NODE_VAR.get( options );
//
//                    if (currentNodeVar.startsWith( "$$" ))
//                    {
//                        throw
//                            new IllegalArgumentException(
//                                "Omit the '$p' refix for the current " +
//                                    "node variable" );
//                    }
//                }

        static Path create ( String pathExpr, String currentNodeVar )
        {
            assert !currentNodeVar.startsWith( "$" );

            try
            {
                return
                    new XbeanPathImpl(
                        XPath.compileXPath( pathExpr, currentNodeVar ),
                        pathExpr, currentNodeVar );
            }
            catch ( XPath.XPathCompileException e )
            {
                return null;
            }
        }

        protected String getPathExpr ( ) { return _pathExpr; }
        
        protected PathEngine execute (
            Root r, Splay s, int p, XmlOptions options )
        {
            // The builtin XPath engine works only on containers.  Delegate to
            // xqrl otherwise.  Also, if the path had a //. at the end, the
            // simple xpath engine can't do the generate case, it only handles
            // attrs and elements.

            if (p != 0 || !s.isContainer() || _xpath.sawDeepDot())
            {
                // If the xbean path compiler could could handle the path, then
                // the xqrl compiler better be able to!

                try
                {
                    return
                        getPath( _pathExpr, true, null ).
                            execute( r, s, p, options );
                }
                catch ( Throwable e )
                {
                    throw new RuntimeException( "Can't compile path", e );
                }
            }

            return new XBeanPathEngine( _xpath, r, s );
        }

// TODO - because this xpath engine does not use a saver, any attributes in the
// path which refer to namesapce attributes will require us to run the XQRL
// path engine which is based on the saver.

        private static class XBeanPathEngine
            extends XPath.ExecutionContext implements PathEngine
        {
            XBeanPathEngine ( XPath xpath, Root r, Splay s )
            {
                assert s.isContainer();

                _root = r;
                _curr = _top = s;
                _version = r.getVersion();

                init( xpath );
            }

            public boolean next ( Selections selections )
            {
                int initialSize = selections.currentSize();

                for ( ; ; )
                {
                    if (_root.getVersion() != _version)
                        throw new IllegalStateException( "Document changed" );

                    if (_curr == null)
                        return false;

                    advance( selections );

                    if (initialSize < selections.currentSize())
                        return true;
                }
            }

            private Splay doAttrs ( int ret, Splay s, Selections selections )
            {
                if ((ret & ATTRS) == 0)
                    return null;

                for ( s = _curr.nextSplay() ; s.isAttr() ;
                      s = s.nextSplay() )
                {
                    if (s.isNormalAttr() && attr( s.getName() ))
                        selections.add( _root, s );
                }

                return s;
            }

            private void advance ( Selections selections )
            {
                if (_curr == _top)
                {
                    int ret = start();

                    if ((ret & HIT) != 0)
                        selections.add( _root, _curr );

                    Splay s = doAttrs( ret, _curr, selections );

                    if ((ret & DESCEND) == 0 || _curr.isLeaf())
                        _curr = null;
                    else
                        _curr = s == null ? _curr.nextNonAttrSplay() : s;

                    return;
                }

                for ( ; ; )
                {
                    if (_curr.isFinish())
                    {
                        if (_curr.getContainer() == _top)
                            _curr = null;
                        else
                        {
                            end();
                            _curr = _curr.nextSplay();
                        }

                        return;
                    }

                    if (_curr.isBegin())
                    {
                        int ret = element( _curr.getName() );

                        if ((ret & HIT) != 0)
                            selections.add( _root, _curr );

                        Splay s = doAttrs( ret, _curr, selections );

                        if (_curr.isLeaf())
                        {
                            end();
                            _curr = s == null ? _curr.nextNonAttrSplay() : s;
                        }
                        else if ((ret & DESCEND) == 0)
                            _curr = ((Splay.Container) _curr).getFinish();
                        else
                            _curr = s == null ? _curr.nextNonAttrSplay() : s;

                        return;
                    }

                    _curr = _curr.nextSplay();
                }
            }

            private Root  _root;
            private long  _version;
            private Splay _top;
            private Splay _curr;
        }

        private String _pathExpr;
        private XPath  _xpath;
        private String _currentNodeVar;
    }


    private static final class JaxenPathImpl extends Path
    {
        private String _pathExpr;
        private JaxenXBeansDelegate.SelectPathInterface _xpathImpl;

        private JaxenPathImpl (
            JaxenXBeansDelegate.SelectPathInterface xpathImpl, String pathExpr )
        {
            _xpathImpl = xpathImpl;
            _pathExpr = pathExpr;
        }

        static Path create ( String pathExpr, String currentNodeVar )
        {
            assert !currentNodeVar.startsWith( "$" ); // cezar review with ericvas

            JaxenXBeansDelegate.SelectPathInterface impl = JaxenXBeansDelegate.createInstance( pathExpr );
            if (impl == null)
                return null;

            return new JaxenPathImpl( impl , pathExpr );
        }

        protected String getPathExpr ( ) { return _pathExpr; }

        protected PathEngine execute ( Root r, Splay s, int p, XmlOptions options )
        {
            return new JaxenPathEngine( _xpathImpl, r, s, p);
        }

        private static class JaxenPathEngine
            extends XPath.ExecutionContext implements PathEngine
        {
            JaxenPathEngine( JaxenXBeansDelegate.SelectPathInterface xpathImpl, Root r, Splay s, int p )
            {
                _jaxenXpathImpl = xpathImpl;
                _root = r;
                _splay = s;
                _p = p;
                _version = r.getVersion();
            }

            public boolean next ( Selections selections )
            {
                if (!_firstCall)
                    return false;

                _firstCall = false;

                if (_root.getVersion() != _version)
                    throw new IllegalStateException( "Document changed" );

                List resultsList;
                Cursor cur = new Cursor(_root, _splay, _p);
                resultsList = _jaxenXpathImpl.selectPath(cur);

                int i;
                for (i = 0; i<resultsList.size(); i++)
                {
                    XmlCursor.XmlBookmark b = (XmlCursor.XmlBookmark)resultsList.get(i);
                    Splay.Annotation ann = ((Splay.Annotation)b._currentMark);
                    selections.add(_root, ann.getSplay(), ann.getPos());
                }
                cur.dispose();
                _root = null;
                _splay = null;
                _jaxenXpathImpl = null;

                return false;
            }

            private JaxenXBeansDelegate.SelectPathInterface _jaxenXpathImpl;
            private Root  _root;
            private Splay _splay;
            private int _p;
            private long  _version;
            private boolean _firstCall = true;
        }
    }

    private static HashMap _xqrlPathCache = new HashMap();
    private static HashMap _xbeanPathCache = new HashMap();
    private static HashMap _xqrlQueryCache = new HashMap();
}
