/**
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 1999 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: OQLQueryImpl.java 8141 2009-03-28 11:36:50Z rjoachim $
 */
package org.exolab.castor.jdo.engine;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.Vector;

import org.castor.core.util.AbstractProperties;
import org.castor.core.util.Messages;
import org.castor.cpa.CPAProperties;
import org.castor.cpa.persistence.convertor.TypeConvertorRegistry;
import org.castor.jdo.util.ClassLoadingUtils;
import org.castor.persist.TransactionContext;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.DbMetaInfo;
import org.exolab.castor.jdo.OQLQuery;
import org.exolab.castor.jdo.ObjectNotFoundException;
import org.exolab.castor.jdo.PersistenceException;
import org.exolab.castor.jdo.Query;
import org.exolab.castor.jdo.QueryException;
import org.exolab.castor.jdo.QueryResults;
import org.exolab.castor.jdo.engine.nature.ClassDescriptorJDONature;
import org.exolab.castor.jdo.oql.Lexer;
import org.exolab.castor.jdo.oql.ParamInfo;
import org.exolab.castor.jdo.oql.ParseTreeNode;
import org.exolab.castor.jdo.oql.ParseTreeWalker;
import org.exolab.castor.jdo.oql.Parser;
import org.exolab.castor.mapping.AccessMode;
import org.exolab.castor.mapping.ClassDescriptor;
import org.exolab.castor.mapping.FieldDescriptor;
import org.exolab.castor.mapping.FieldHandler;
import org.exolab.castor.mapping.MappingException;
import org.exolab.castor.mapping.TypeConvertor;
import org.exolab.castor.persist.ClassMolder;
import org.exolab.castor.persist.LockEngine;
import org.exolab.castor.persist.spi.Identity;
import org.exolab.castor.persist.spi.PersistenceQuery;
import org.exolab.castor.persist.spi.QueryExpression;

/**
 * An OQLQuery implementation to execute a query based upon an OQL statement.
 *
 * @author <a href="arkin@intalio.com">Assaf Arkin</a>
 * @version $Revision: 8141 $ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
 */
public class OQLQueryImpl implements Query, OQLQuery {
    private static TypeConvertorRegistry _typeConvertorRegistry = null;
    
    private LockEngine _dbEngine;

    private Database _database;

    private Class _objClass;

    private ClassDescriptor _clsDesc;

    private QueryExpression _expr;

    /** Stored procedure call. */
    private String _spCall;

    private Class[] _bindTypes;

    private Object[] _bindValues;

    private Hashtable<Integer, ParamInfo> _paramInfo;

    private int _fieldNum;

    private int _projectionType;
    
    private Vector<String> _projectionInfo;

    private PersistenceQuery _query;

    private QueryResults _results;

    /**
     * Creates an instance to execute a query based upon an OQL statement.
     * 
     * @param database The Castor database to run the query against.
     */
    OQLQueryImpl(final Database database) {
        _database = database;
    }
    
    private TypeConvertorRegistry getTypeConvertorRegistry() {
        if (_typeConvertorRegistry == null) {
            AbstractProperties properties = CPAProperties.getInstance();
            _typeConvertorRegistry = new TypeConvertorRegistry(properties);
        }
        return _typeConvertorRegistry;
    }

    /**
     * @inheritDoc
     * @see org.exolab.castor.jdo.Query#bind(java.lang.Object)
     */
    public void bind(final Object value) {
        Object internalValue = value;
        if ((_expr == null) && (_spCall == null)) {
            throw new IllegalStateException("Must create query before using it");
        }
        if (_fieldNum == _paramInfo.size()) {
            throw new IllegalArgumentException("Only " + _paramInfo.size()
                    + " fields in this query");
        }
        try {
            ParamInfo info = _paramInfo.get(new Integer(_fieldNum + 1));

            //do type checking and conversion
            Class paramClass = info.getTheClass();
            Class fieldClass = info.getFieldType();
            Class sqlClass = info.getSQLType();

            if (internalValue != null) {
                Class valueClass = internalValue.getClass();

                if (paramClass.isAssignableFrom(valueClass)) {
                    LockEngine lockEngine = ((AbstractDatabaseImpl) _database).getLockEngine();
                    ClassMolder molder = lockEngine.getClassMolder(valueClass);

                    if (molder != null) {
                        Identity temp = molder.getActualIdentity(
                                _database.getClassLoader(), internalValue);
                        if (temp == null) {
                            internalValue = null;
                        } else  if (temp.size() == 1) {
                            internalValue = temp.get(0);
                        } else {
                            throw new IllegalArgumentException(
                                    "Unable to bind multi column identities");
                        }
                    }
                } else if (info.isUserDefined()) {
                        //If the user specified a type they must pass that exact type.

                        throw new IllegalArgumentException(
                                "Query paramter " + (_fieldNum + 1)
                                + " is not of the expected type " + paramClass
                                + " it is an instance of the class " + valueClass);
                }
                if ((sqlClass != null) && !sqlClass.isAssignableFrom(valueClass)) {
                    // First convert the actual value to the field value
                    if (fieldClass != valueClass) {
                        try {
                            TypeConvertor tc = getTypeConvertorRegistry().getConvertor(
                                    valueClass, fieldClass, null);
                            internalValue = tc.convert(internalValue);
                        } catch (MappingException e) {
                            throw new IllegalArgumentException("Query parameter "
                                    + (_fieldNum + 1) + " cannot be converted from " + valueClass
                                    + " to " + paramClass + ", because no convertor can be found.");
                        }
                    }
                    // Perform conversion from field type to SQL type, if needed
                    if (info.getConvertor() != null) {
                        internalValue = info.getConvertor().convert(internalValue);
                    }
                }
            }
            if (_bindValues == null) {
                _bindValues = new Object[ _bindTypes.length ];
            }

            _bindValues[_fieldNum++] = internalValue;
        } catch (IllegalArgumentException except) {
            throw except;
        }
    }

    /**
     * @inheritDoc
     */
    public void bind(final boolean value) {
        bind(new Boolean(value));
    }

    /**
     * @inheritDoc
     */
    public void bind(final short value) {
        bind(new Short(value));
    }

    /**
     * @inheritDoc
     */
    public void bind(final int value) {
        bind(new Integer(value));
    }

    /**
     * @inheritDoc
     */
    public void bind(final long value) {
        bind(new Long(value));
    }

    /**
     * @inheritDoc
     */
    public void bind(final float value) {
        bind(new Float(value));
    }

    /**
     * @inheritDoc
     */
    public void bind(final double value) {
        bind(new Double(value));
    }

    /**
     * @inheritDoc
     */
    public void create(final String oql) throws PersistenceException {
        _fieldNum = 0;
        _expr = null;
        _spCall = null;

        // Separate parser for CALL-type queries (using stored procedured)
        if (oql.startsWith("CALL ")) {
            createCall(oql);
            return;
        }

        Lexer lexer = new Lexer(oql);
        Parser parser = new Parser(lexer);
        ParseTreeNode parseTree = parser.getParseTree();

        _dbEngine = ((AbstractDatabaseImpl) _database).getLockEngine();
        if (_dbEngine == null) {
            throw new QueryException("Could not get a persistence engine");
        }

        TransactionContext trans = ((AbstractDatabaseImpl) _database).getTransaction();
        DbMetaInfo dbInfo = trans.getConnectionInfo(_dbEngine);

        ParseTreeWalker walker = new ParseTreeWalker(_dbEngine, parseTree,
                _database.getClassLoader(), dbInfo);

        _objClass = walker.getObjClass();
        _clsDesc = walker.getClassDescriptor();
        _expr = walker.getQueryExpression();
        _paramInfo = walker.getParamInfo();
        _projectionType = walker.getProjectionType();
        _projectionInfo = walker.getProjectionInfo();

        // create the types array and fill it
        _bindTypes = new Class[_paramInfo.size()];
        int paramIndex = 0;
        for (Enumeration<ParamInfo> e = _paramInfo.elements(); e.hasMoreElements(); ) {
            ParamInfo info = e.nextElement();

            _bindTypes[paramIndex++] =
                ((info.getSQLType() == null)) ? info.getTheClass() : info.getSQLType();
        }
    }

    /**
     * @param oql
     * @throws QueryException
     */
    public void createCall(final String oql) throws QueryException {
        StringBuffer sql;
        int as;
        int leftParen;
        int rightParen;
        int paramCnt;
        String objType;
        ParamInfo info;
        StringBuffer sb;
        Integer paramNo;

        if (!oql.startsWith("CALL ")) {
            throw new QueryException("Stored procedure call must start with CALL");
        }

        // Fix for bug #995
        // as = oql.indexOf( " AS " );
        as = oql.lastIndexOf(" AS ");
        if (as < 0) {
            throw new QueryException("Stored procedure call must end with \"AS <class-name>\"");
        }
        leftParen = oql.indexOf("(");
        rightParen = oql.indexOf(")");
        sql = new StringBuffer();
        paramCnt = 0;
        _paramInfo = new Hashtable<Integer, ParamInfo>();
        if (oql.startsWith("CALL SQL")) {
            int startOff = oql.toUpperCase().indexOf("WHERE "); // parameters begin here!

            if (!(startOff < 0)) {
                startOff += 6;
                sql.append(oql.substring(5, startOff));

                int i = startOff;
                while (i < as) {
                    if (oql.charAt(i) == '$') {
                        // get parameter number if given
                        sb = new StringBuffer();
                        for (int j = i + 1; j < as; j++) {
                            char c = oql.charAt(j);
                            if (!Character.isDigit(c)) {
                                break;
                            }
                            sb.append(c);
                        }
                        sql.append('?'); // replace "$" with "?"
                        if (sb.length() > 0) {
                            sql.append(sb); // and add parameter number to it
                            paramNo = Integer.valueOf(sb.toString());
                        } else {
                            paramNo = new Integer(paramCnt + 1);
                        }
                        info = _paramInfo.get(paramNo);
                        if (info == null) {
                            info = new ParamInfo("", "java.lang.Object", null,
                                    _database.getClassLoader());
                        }
                        //info.mapToSQLParam( paramCnt + 1 );
                        _paramInfo.put(paramNo , info);
                        paramCnt++;

                        i += sb.length() + 1;
                    } else {
                        sql.append(oql.charAt(i));
                        i++;
                    }
                }
            } else {
                sql.append(oql.substring(5, as));
            }
        } else if ((leftParen < 0) && (rightParen < 0)) {
            sql.append(oql.substring(5, as));
        } else {
            if (((leftParen < 0) && (rightParen >= 0))
                    || (leftParen > rightParen)) {
                throw new QueryException("Syntax error: parenthesis");
            }
            sql.append(oql.substring(5, leftParen));
            sql.append('(');
            for (int i = leftParen + 1; i < rightParen; i++) {
                if (oql.charAt(i) == '$') {
                    // get parameter number if given
                    sb = new StringBuffer();
                    for (int j = i + 1; j < rightParen; j++) {
                        char c = oql.charAt(j);
                        if (!Character.isDigit(c)) {
                            break;
                        }
                        sb.append(c);
                    }
                    if (sb.length() > 0) {
                        paramNo = Integer.valueOf(sb.toString());
                    } else {
                        paramNo = new Integer(paramCnt + 1);
                    }
                    info = _paramInfo.get(paramNo);
                    if (info == null) {
                        info = new ParamInfo("", "java.lang.Object", null,
                                _database.getClassLoader());
                    }
                    //info.mapToSQLParam( paramCnt + 1 );
                    _paramInfo.put(paramNo , info);
                    paramCnt++;
                }
            }
            for (int i = 0; i < paramCnt; i++) {
                sql.append('?');
                if (i < paramCnt - 1) {
                    sql.append(',');
                }
            }
            sql.append(')');
        }
        _spCall = sql.toString();
        _projectionType = ParseTreeWalker.PARENT_OBJECT;
        _bindTypes = new Class[ paramCnt ];
        for (int i = 0; i < paramCnt; i++) {
            _bindTypes[i] = Object.class;
        }

        objType = oql.substring(as + 4).trim();
        if (objType.length() == 0) {
            throw new QueryException("Missing object name");
        }
        try {
            _objClass = ClassLoadingUtils.loadClass(_database.getClassLoader(), objType);
        } catch (ClassNotFoundException except) {
            throw new QueryException("Could not find class " + objType);
        }
        _dbEngine = ((AbstractDatabaseImpl) _database).getLockEngine();
        if ((_dbEngine == null) || (_dbEngine.getPersistence(_objClass) == null)) {
            throw new QueryException("Could not find an engine supporting class " + objType);
        }
    }

    /**
     * @inheritDoc
     * @see org.exolab.castor.jdo.Query#execute()
     */
    public QueryResults execute() throws PersistenceException {
        return execute(null);
    }

    /**
     * @inheritDoc
     * @see org.exolab.castor.jdo.Query#execute(boolean)
     */
    public QueryResults execute(final boolean scrollable) throws PersistenceException {
        return execute(null, scrollable);
    }

    /**
     * @inheritDoc
     * @see org.exolab.castor.jdo.Query#execute(org.exolab.castor.mapping.AccessMode)
     */
    public QueryResults execute(final AccessMode accessMode) throws PersistenceException {
        return execute(accessMode, false);
    }

    /**
     * @inheritDoc
     * @see org.exolab.castor.jdo.Query#execute(org.exolab.castor.mapping.AccessMode, boolean)
     */
    public QueryResults execute(final AccessMode accessMode, final boolean scrollable)
    throws PersistenceException {
        org.exolab.castor.persist.QueryResults      results;
        SQLEngine         engine;

        if ((_expr == null) && (_spCall == null)) {
            throw new IllegalStateException("Must create query before using it");
        }
        if (_results != null) {
            _results.close();
        }
        try {
            switch (_projectionType) {
                case ParseTreeWalker.PARENT_OBJECT:
                case ParseTreeWalker.DEPENDANT_OBJECT:
                case ParseTreeWalker.DEPENDANT_OBJECT_VALUE:
                    try {
                        engine = (SQLEngine) _dbEngine.getPersistence(_objClass);
                        if (_expr != null) {
                            _query = engine.createQuery(_expr, _bindTypes, accessMode);
                        } else {
                            _query = engine.createCall(_spCall, _bindTypes);
                        }
                        if (_bindValues != null) {
                            for (int i = 0; i < _bindValues.length; ++i) {
                                _query.setParameter(i, _bindValues[i]);
                            }
                        }
                    } catch (QueryException except) {
                        throw new QueryException(except.getMessage());
                    }
                    results = ((AbstractDatabaseImpl) _database).getTransaction().query(
                            _dbEngine, _query, accessMode, scrollable);
                    _fieldNum = 0;

                    if (_projectionType == ParseTreeWalker.PARENT_OBJECT) {
                        _results = new OQLEnumeration(results);
                    } else {
                        _results = new OQLEnumeration(results, _projectionInfo, _clsDesc);
                    }
                    break;

                case ParseTreeWalker.DEPENDANT_VALUE:
                case ParseTreeWalker.AGGREGATE:
                case ParseTreeWalker.FUNCTION:
                    try {
                        TransactionContext tx = ((AbstractDatabaseImpl) _database).getTransaction();
                        java.sql.Connection conn = tx.getConnection(_dbEngine);
                        SimpleQueryExecutor sqe = new SimpleQueryExecutor(_database);
                        _results =  sqe.execute(conn, _expr, _bindValues);
                    } catch (QueryException except) {
                        throw new QueryException(Messages.message(
                                "persist.simple.query.failed"), except);
                    }
                    _fieldNum = 0;
                    break;
                default:
                    throw new PersistenceException("Unknown projection type: " + _projectionType);
            }
        } catch (PersistenceException except) {
            throw except;
        }

        return _results;
    }

    /**
     * Get the generated SQL statement for this OQLQuery.
     * 
     * @return A SQL statement.
     * @throws QueryException If the SQL query cannot be generated.
     */
    public String getSQL() throws org.exolab.castor.jdo.QueryException {
      if (_expr != null) {
          return _expr.getStatement(true);
      }

      return _spCall;
    }

    /**
     * @inheritDoc
     */
    public void close() {
        if (_query != null) {
            _query.close();
            _query = null;
        }
        if (_results != null) {
            _results.close();
            _results = null;
        }
    }

    /**
     * {@link java.util.Enumeration} implementation to traverse the result as returned by the
     * execution of the OQL query.
     */
    class OQLEnumeration implements QueryResults, Enumeration<Object> {
        private Object _lastObject;

        private Vector<String> _pathInfo;

        private ClassDescriptor _classDescriptor;

        private org.exolab.castor.persist.QueryResults _results;

        /**
         * Creates an instance of this class.
         * @param results
         * @param pathInfo
         * @param clsDesc
         */
        OQLEnumeration(final org.exolab.castor.persist.QueryResults results,
                final Vector<String> pathInfo, final ClassDescriptor clsDesc) {
            _results = results;
            _pathInfo = pathInfo;
            _classDescriptor = clsDesc;
        }


        OQLEnumeration(final org.exolab.castor.persist.QueryResults results) {
            _results = results;
            _pathInfo = null;
            _classDescriptor = null;
        }

        /**
         * @inheritDoc
         */
        public boolean absolute(final int row) throws PersistenceException {
            return _results.absolute(row);
        }
        
        /**
         * @inheritDoc
         */
        public int size() throws PersistenceException {
            return _results.size();
        }

        /**
         * @inheritDoc
         */
        public boolean hasMoreElements() {
            try {
                return hasMore(true);
            } catch (PersistenceException except) {
                // Will never happen
                return false;
            }
        }

        /**
         * @inheritDoc
         */
        public boolean hasMore() throws PersistenceException {
            return hasMore(false);
        }


        public boolean hasMore(final boolean skipError) throws PersistenceException {
            Object identity;

            if (_lastObject != null) {
                return true;
            }
            if (_results == null) {
                return false;
            }
            try {
                identity = _results.nextIdentity();
                while (identity != null) {
                    try {
                        _lastObject = _results.fetch();
                        if (_lastObject != null) {
                            break;
                        }
                    } catch (ObjectNotFoundException except) {
                        // Object not found, deleted, etc. Just skip to next one.
                        identity = _results.nextIdentity();
                    } catch (PersistenceException except) {
                        // Error occured. If not throwing exception just skip to
                        // next object.
                        identity = _results.nextIdentity();
                        if (!skipError) {
                            throw except;
                        }
                    }
                }
                if (identity == null) {
                    _results.close();
                    _results = null;
                }
            } catch (PersistenceException except) {
                _results.close();
                _results = null;
                if (!skipError) {
                    throw except;
                }
            }
            return (_lastObject != null);
        }

        /**
         * @inheritDoc
         */
        public Object nextElement() throws NoSuchElementException {
            try {
                return next(true);
            } catch (PersistenceException except) {
                // Will never happen
                return null;
            }
        }

        /**
         * @inheritDoc
         */
        public Object next() throws PersistenceException, NoSuchElementException {
            return next(false);
        }


        private Object next(final boolean skipError)
        throws PersistenceException, NoSuchElementException {
            Object identity;

            if (_lastObject != null) {
                Object result = _lastObject;

                _lastObject = null;
                if (_pathInfo == null) {
                    return result;
                }
                return followPath(result);
            }
            if (_results == null) {
                throw new NoSuchElementException();
            }
            try {
                identity = _results.nextIdentity();
                while (identity != null) {
                    try {
                        Object result = _results.fetch();

                        if (result != null) {
                            if (_pathInfo == null) {
                                return result;
                            }
                        }
                        return followPath(result);
                    } catch (ObjectNotFoundException except) {
                        // Object not found, deleted, etc. Just skip to next one.
                    } catch (PersistenceException except) {
                        // Error occured. If not throwing exception just skip to
                        // next object.
                        if (!skipError) {
                            throw except;
                        }
                    }
                    identity = _results.nextIdentity();
                }
                if (identity == null) {
                    _results.close();
                    _results = null;
                }
            } catch (PersistenceException except) {
                _results.close();
                _results = null;
                if (!skipError) {
                    throw except;
                }
            }
            throw new NoSuchElementException();
        }

        /**
         * @inheritDoc
         */
        public void close() {
            if (_results != null) {
                _results.close();
                _results = null;
            }
        }
        
        private Object followPath(final Object parent) {
            ClassDescriptor curClassDesc = _classDescriptor;
            Object curObject = parent;
            for (int i = 1; i < _pathInfo.size(); i++) {
                String curFieldName = _pathInfo.elementAt(i);
                try {
                    ClassDescriptorJDONature nature;
                    nature = new ClassDescriptorJDONature(curClassDesc);
                    FieldDescriptor curFieldDesc = nature.getField(curFieldName);
                    FieldHandler handler = curFieldDesc.getHandler();
                    curObject = handler.getValue(curObject);
                    curClassDesc = curFieldDesc.getClassDescriptor();
                } catch (Exception ex) {
                    throw new NoSuchElementException(
                            "An exception was thrown trying to access get methods to follow "
                            + "the path expression. " + ex.toString());
                }
            }
            
            return curObject;
        }
    }
}
