/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.openjpa.jdbc.kernel;

import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.MappingRepository;
import org.apache.openjpa.jdbc.meta.QueryResultMapping;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.ResultSetResult;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.jdbc.sql.SQLExceptions;
import org.apache.openjpa.kernel.AbstractStoreQuery;
import org.apache.openjpa.kernel.QueryContext;
import org.apache.openjpa.kernel.StoreQuery;
import org.apache.openjpa.lib.rop.RangeResultObjectProvider;
import org.apache.openjpa.lib.rop.ResultObjectProvider;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.util.UserException;


/**
 * A SQL query.
 *
 * @author Abe White
 */
public class SQLStoreQuery
    extends AbstractStoreQuery {

    
    private static final long serialVersionUID = 1L;

    private static final Localizer _loc = Localizer.forPackage
        (SQLStoreQuery.class);

    private transient final JDBCStore _store;

    /**
     * Construct a query managed by the given context.
     */
    public SQLStoreQuery(JDBCStore store) {
        _store = store;
    }

    public JDBCStore getStore() {
        return _store;
    }

    @Override
    public boolean supportsParameterDeclarations() {
        return false;
    }

    @Override
    public boolean supportsDataStoreExecution() {
        return true;
    }

    @Override
    public Executor newDataStoreExecutor(ClassMetaData meta,
        boolean subclasses) {
        return new SQLExecutor(this, meta);
    }

    @Override
    public boolean requiresCandidateType() {
        return false;
    }

    @Override
    public boolean requiresParameterDeclarations() {
        return false;
    }

    /**
     * Executes the filter as a SQL query.
     */
    public static class SQLExecutor
        extends AbstractExecutor {

        private final ClassMetaData _meta;
        private final boolean _select;
        private final boolean _call;   // native call stored procedure
        private final QueryResultMapping _resultMapping;

        public SQLExecutor(SQLStoreQuery q, ClassMetaData candidate) {
            QueryContext ctx = q.getContext();
            String resultMapping = ctx.getResultMappingName();
            if (resultMapping == null)
                _resultMapping = null;
            else {
                ClassLoader envLoader = ctx.getStoreContext().getClassLoader();
                MappingRepository repos = q.getStore().getConfiguration().
                    getMappingRepositoryInstance();
                _resultMapping = repos.getQueryResultMapping
                    (ctx.getResultMappingScope(), resultMapping, envLoader,
                        true);
            }
            _meta = candidate;

            String sql = StringUtil.trimToNull(ctx.getQueryString());
            if (sql == null)
                throw new UserException(_loc.get("no-sql"));
            _select = q.getStore().getDBDictionary().isSelect(sql);
            _call = sql.length() > 4
                && sql.substring(0, 4).equalsIgnoreCase("call");
        }

        @Override
        public int getOperation(StoreQuery q) {
           return _select ? OP_SELECT :
                (q.getContext().getCandidateType() != null
                        || q.getContext().getResultType() != null
                        || q.getContext().getResultMappingName() != null
                        || q.getContext().getResultMappingScope() != null)
                        ? OP_SELECT : OP_UPDATE;
        }

        @Override
        public Number executeUpdate(StoreQuery q, Object[] params) {
            JDBCStore store = ((SQLStoreQuery) q).getStore();
            DBDictionary dict = store.getDBDictionary();
            String sql = q.getContext().getQueryString();

            List paramList = new ArrayList(Arrays.asList(params));
            SQLBuffer buf = new SQLBuffer(dict).append(sql);

            // we need to make sure we have an active store connection
            store.getContext().beginStore();
            Connection conn = store.getConnection();
            JDBCFetchConfiguration fetch = (JDBCFetchConfiguration)
                q.getContext().getFetchConfiguration();

            PreparedStatement stmnt = null;
            try {
                if (_call)
                    stmnt = prepareCall(conn, buf);
                else
                    stmnt = prepareStatement(conn, buf);

                buf.setParameters(paramList);
                if (stmnt != null)
                    buf.setParameters(stmnt);

                dict.setTimeouts(stmnt, fetch, true);

                int count = executeUpdate(store, conn, stmnt, buf);

                return count;
            } catch (SQLException se) {
                throw SQLExceptions.getStore(se, dict);
            } finally {
                if (stmnt != null) {
                    try {
                        stmnt.close();
                    } catch (SQLException se) {
                        // safe to ignore
                    } finally {
                        stmnt = null;
                    }
                }
                try {
                    conn.close();
                } catch (SQLException se) {
                }
            }
        }

        @Override
        public ResultObjectProvider executeQuery(StoreQuery q,
            Object[] params, Range range) {
            JDBCStore store = ((SQLStoreQuery) q).getStore();
            DBDictionary dict = store.getDBDictionary();
            String sql = q.getContext().getQueryString();

            List paramList = new ArrayList(Arrays.asList(params));
            SQLBuffer buf = new SQLBuffer(dict).append(sql);
            Connection conn = store.getConnection();
            JDBCFetchConfiguration fetch = (JDBCFetchConfiguration)
                q.getContext().getFetchConfiguration();

            ResultObjectProvider rop;
            PreparedStatement stmnt = null;
            try {
                // use the right method depending on sel vs. proc, lrs setting
                if (_select && !range.lrs)
                    stmnt = prepareStatement(conn, buf);
                else if (_select)
                    stmnt = prepareStatement(conn, buf, fetch, -1, -1);
                else if (!range.lrs)
                    stmnt = prepareCall(conn, buf);
                else
                    stmnt = prepareCall(conn, buf, fetch, -1, -1);

                int index = 0;
                for (Iterator i = paramList.iterator(); i.hasNext() &&
                    stmnt != null;)
                    dict.setUnknown(stmnt, ++index, i.next(), null);

                dict.setTimeouts(stmnt, fetch, false);
                ResultSet rs = executeQuery(store, conn, stmnt, buf, paramList);
                ResultSetResult res = stmnt != null ?
                    new ResultSetResult(conn, stmnt, rs, store) :
                    new ResultSetResult(conn, rs, dict);
                if (_resultMapping != null)
                    rop = new MappedQueryResultObjectProvider(_resultMapping,
                        store, fetch, res);
                else if (q.getContext().getCandidateType() != null)
                    rop = new GenericResultObjectProvider((ClassMapping) _meta,
                        store, fetch, res);
                else
                    rop = new SQLProjectionResultObjectProvider(store, fetch,
                        res, q.getContext().getResultType());
            } catch (SQLException se) {
                if (stmnt != null)
                    try { stmnt.close(); } catch (SQLException se2) {}
                try { conn.close(); } catch (SQLException se2) {}
                throw SQLExceptions.getStore(se, dict);
            }

            if (range.start != 0 || range.end != Long.MAX_VALUE)
                rop = new RangeResultObjectProvider(rop, range.start,range.end);
            return rop;
        }

        @Override
        public String[] getDataStoreActions(StoreQuery q, Object[] params,
            Range range) {
            return new String[]{ q.getContext().getQueryString() };
        }

        @Override
        public boolean isPacking(StoreQuery q) {
            return q.getContext().getCandidateType() == null;
        }

        /**
         * This method is to provide override for non-JDBC or JDBC-like
         * implementation of preparing call statement.
         */
        protected PreparedStatement prepareCall(Connection conn, SQLBuffer buf)
            throws SQLException {
            return buf.prepareCall(conn);
        }

        /**
         * This method is to provide override for non-JDBC or JDBC-like
         * implementation of executing update.
         */
        protected int executeUpdate(JDBCStore store, Connection conn,
            PreparedStatement stmnt, SQLBuffer buf)
            throws SQLException {
            int count = 0;
            if (_call && !stmnt.execute()) {
                count = stmnt.getUpdateCount();
            }
            else {
                // native insert, update, delete
                count = stmnt.executeUpdate();
            }
            return count;
        }

        /**
         * This method is to provide override for non-JDBC or JDBC-like
         * implementation of preparing call statement.
         */
        protected PreparedStatement prepareCall(Connection conn, SQLBuffer buf,
            JDBCFetchConfiguration fetch, int rsType, int rsConcur)
            throws SQLException {
            return buf.prepareCall(conn, fetch, rsType, rsConcur);
        }

        /**
         * This method is to provide override for non-JDBC or JDBC-like
         * implementation of preparing statement.
         */
        protected PreparedStatement prepareStatement(Connection conn,
            SQLBuffer buf) throws SQLException {
            return buf.prepareStatement(conn);
        }

        /**
         * This method is to provide override for non-JDBC or JDBC-like
         * implementation of preparing statement.
         */
        protected PreparedStatement prepareStatement(Connection conn,
            SQLBuffer buf, JDBCFetchConfiguration fetch, int rsType,
            int rsConcur) throws SQLException {
            return buf.prepareStatement(conn, fetch, rsType, rsConcur);
        }

        /**
         * This method is to provide override for non-JDBC or JDBC-like
         * implementation of executing query.
         */
        protected ResultSet executeQuery(JDBCStore store, Connection conn,
            PreparedStatement stmnt, SQLBuffer buf, List paramList)
            throws SQLException {
            return stmnt.executeQuery();
        }

        /**
         * The given query is parsed to find the parameter tokens of the form
         * <code>?n</code> which is different than <code>?</code> tokens in
         * actual SQL parameter tokens. These <code>?n</code> style tokens
         * are replaced in the query string by <code>?</code> tokens.
         *
         * During the token parsing, the ordering of the tokens is recorded.
         * The given userParam must contain parameter keys as Integer and
         * the same Integers must appear in the tokens.
         *
         */
        @Override
        public Object[] toParameterArray(StoreQuery q, Map userParams) {
            if (userParams == null || userParams.isEmpty())
                return StoreQuery.EMPTY_OBJECTS;
            String sql = q.getContext().getQueryString();
            List<Integer> paramOrder = new ArrayList<>();
            try {
                sql = substituteParams(sql, paramOrder);
            } catch (IOException ex) {
                throw new UserException(ex.getLocalizedMessage());
            }

            Object[] result = new Object[paramOrder.size()];
            int idx = 0;
            for (Integer key : paramOrder) {
                if (!userParams.containsKey(key))
                    throw new UserException(_loc.get("uparam-missing",
                        key, sql, userParams));
                result[idx++] = userParams.get(key);
            }
            // modify original JPA-style SQL to proper SQL
            q.getContext().getQuery().setQuery(sql);
            return result;
        }
    }

    /**
     * Utility method to substitute '?num' for parameters in the given SQL
     * statement, and fill-in the order of the parameter tokens
     */
    public static String substituteParams(String sql, List<Integer> paramOrder)
            throws IOException {
            // if there's no "?" parameter marker, then we don't need to
            // perform the parsing process
            if (sql.indexOf("?") == -1)
                return sql;

            sql = sql.replaceAll("\\\\", "\\\\\\\\");
            paramOrder.clear();
            StreamTokenizer tok = new StreamTokenizer(new StringReader(sql));
            tok.resetSyntax();
            tok.quoteChar('\'');
            tok.wordChars('0', '9');
            tok.wordChars('?', '?');

            StringBuilder buf = new StringBuilder(sql.length());
            for (int ttype; (ttype = tok.nextToken()) != StreamTokenizer.TT_EOF;) {
                switch (ttype) {
                    case StreamTokenizer.TT_WORD:
                        // a token is a positional parameter if it starts with
                        // a "?" and the rest of the token are all numbers
                        if (tok.sval.startsWith("?")) {
                            buf.append("?");
                            String pIndex = tok.sval.substring(1);
                            if (pIndex.length() > 0) {
                                paramOrder.add(Integer.valueOf(pIndex));
                            } else { // or nothing
                                paramOrder.add(paramOrder.size()+1);
                            }
                        } else
                            buf.append(tok.sval);
                        break;
                    case'\'':
                        buf.append('\'');
                        if (tok.sval != null) {
                            buf.append(tok.sval);
                            buf.append('\'');
                        }
//                        // StreamTokenizer can not differentiate the last quoted token as in ^.*'.*$ and ^.*',*'$
//                        // need to check the last quote ends with "'" and process accordingly.
//                        if(endsWithQuote) {
//                            buf.append('\'');
//                        } else if (tok.nextToken() != StreamTokenizer.TT_EOF) {
//                        	tok.pushBack();
//                            buf.append('\'');
//                        }
                        break;
                    default:
                        buf.append((char) ttype);
                }
            }
            return buf.toString();
    }
}
