/*
 * 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 static java.util.Arrays.asList;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.openjpa.jdbc.meta.MappingRepository;
import org.apache.openjpa.jdbc.meta.QueryResultMapping;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.StoredProcedure;
import org.apache.openjpa.kernel.AbstractStoreQuery;
import org.apache.openjpa.kernel.QueryContext;
import org.apache.openjpa.kernel.QueryOperations;
import org.apache.openjpa.kernel.StoreQuery;
import org.apache.openjpa.lib.rop.ResultObjectProvider;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.MultiQueryMetaData;
import org.apache.openjpa.meta.QueryMetaData;
import org.apache.openjpa.util.InternalException;

/**
 * Executes a stored procedure.
 *
 * @author ppoddar
 *
 */
public class StoredProcedureQuery extends AbstractStoreQuery {
    private static final long serialVersionUID = 1L;

    private static final Object[] NO_PARAM = new Object[0];

    JDBCStore _store;
    StoredProcedure _proc;
    private MultiQueryMetaData _meta;

    public StoredProcedureQuery(JDBCStore store) {
        _store = store;
    }

    public int getOperation() {
        return QueryOperations.OP_SELECT;
    }

    public StoredProcedure getProcedure() {
        return _proc;
    }

    public DBDictionary getDictionary() {
        return _store.getDBDictionary();
    }

    @Override
    public boolean setQuery(Object meta) {
        if (meta == null || meta instanceof MultiQueryMetaData) {
            _meta = (MultiQueryMetaData)meta;
            return true;
        } else {
            throw new InternalException("Unknown " + meta);
        }
    }

    @Override
    public Executor newDataStoreExecutor(ClassMetaData meta,  boolean subclasses) {
        List<QueryResultMapping> mappings = null;
        List<Class<?>> classes = null;
        if (_meta != null) {
            List<QueryMetaData> parts = _meta.getComponents();
            if (parts != null && !parts.isEmpty()) {
                mappings = new ArrayList<>();
                classes = new ArrayList<>();
                MappingRepository repos = _store.getConfiguration().getMappingRepositoryInstance();
                for (QueryMetaData part : parts) {
                    QueryResultMapping mapping = repos.getQueryResultMapping(ctx.getResultMappingScope(),
                            part.getResultSetMappingName(),
                            null, true);
                    if (mapping != null) {
                        mappings.add(mapping);
                    }
                    if (part.getResultType() != null) {
                        classes.add(part.getResultType());
                    }
                }
            }
        }
        return new StoredProcedureQueryExecutor(this, mappings, classes);
    }

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

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

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

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


    public class StoredProcedureQueryExecutor  extends AbstractExecutor {
        private final List<Class<?>> _resultClasses;
        private final List<QueryResultMapping> _resultMappings;

        public StoredProcedureQueryExecutor(StoredProcedureQuery q, List<QueryResultMapping> resultMapping,
                                            List<Class<?>> classes) {
            QueryContext ctx = q.getContext();
            _resultMappings = resultMapping;
            _resultClasses = classes;
            // Look for the named Stored Procedure in the database
            String procName = ctx.getQueryString();
            _proc = getStoredProcedure(_store.getConnection(), _store.getDBDictionary(), procName);
            if (_proc == null) {
                throw new RuntimeException("Can not find stored procedure " + procName);
            }
        }

        StoredProcedure getStoredProcedure(Connection conn, DBDictionary dict, String procedureName) {
            try {
                StoredProcedure sp = dict.getStoredProcedure(conn.getMetaData(), null, null, procedureName);
                if (sp != null) {
                    return sp;
                }
            } catch (SQLException ex) {
                throw new RuntimeException(ex);
            } finally {
                try {
                    conn.close();
                } catch (SQLException ex) {

                }
            }
            throw new RuntimeException("Procedure [" + procedureName + "] not found");
        }

        @Override
        public ResultObjectProvider executeQuery(StoreQuery q, Object[] params, Range range) {
            try {
                DBDictionary dict = _store.getDBDictionary();
                Connection conn = _store.getConnection();
                CallableStatement stmnt = conn.prepareCall(_proc.getCallSQL());

                final StoredProcedureQuery spq = StoredProcedureQuery.class.cast(q);
                for (Column c : spq.getProcedure().getInColumns()) {
                    dict.setUnknown(stmnt, c.getIndex() + 1, params[c.getIndex()], c);
                }
                for (Column c : spq.getProcedure().getInOutColumns()) {
                    final int index = c.getIndex() + 1;
                    stmnt.registerOutParameter(index, c.getType());
                    dict.setUnknown(stmnt, index, params[index - 1], c);
                }
                for (Column c : spq.getProcedure().getOutColumns()) {
                    stmnt.registerOutParameter(c.getIndex() + 1, c.getType());
                }

                JDBCFetchConfiguration fetch = (JDBCFetchConfiguration)q.getContext().getFetchConfiguration();
                ResultObjectProvider rop = new XROP(_resultMappings, _resultClasses, _store, fetch, stmnt);
                rop.open();
                return rop;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public Object[] toParameterArray(StoreQuery q, Map<?, ?> userParams) {
            if (userParams == null) return NO_PARAM;
            Object[] array = new Object[userParams.size()];
            int i = 0;
            StoredProcedureQuery storedProcedureQuery = StoredProcedureQuery.class.cast(q);
            for (final Column[] columns : asList(
                    storedProcedureQuery.getProcedure().getInColumns(),
                    storedProcedureQuery.getProcedure().getInOutColumns())) {
                for (Column c : columns) {
                    array[i] = userParams.get(c.getIdentifier().getName());
                    if (array[i++] == null) {
                        userParams.get(c.getIndex());
                    }
                }
            }
            return array;
        }

    }
}
