/*
 * 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.sql;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.DatabaseMetaData;
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 org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.Schemas;
import org.apache.openjpa.meta.MultiQueryMetaData;

/**
 * Holds metadata about a Database Stored Procedure.
 * This is different than {@link MultiQueryMetaData} which holds the metadata
 * about what the user has specified.
 * <br>
 * An instance of this class can be constructed either by reading from database meta data
 * or by programatic assignment. If an instance if created programmatically, then
 * its SQL body or parameters can be added.
 * <br>
 * This class can generate the SQL statement to create, drop or delete this procedure.
 *
 *
 * @author Pinaki Poddar
 *
 */
public class StoredProcedure {
    private DBIdentifier _catalog;
    private DBIdentifier _schema;
    private DBIdentifier _name;
    private List<Column> _cols = new ArrayList<>();
    private List<String> _params = new ArrayList<>();


    private List<String> _sql = new ArrayList<>();
    private final boolean _fromDatabase;

    /**
     * An enumeration on type of parameter for a Stored Procedure.
     * The enumerted values has the same ordinal numbers as found
     * in corresponding integer values in {@link DatabaseMetaData}.
     */
    public enum PARAM {UNKNOW, IN, INOUT, RESULT, OUT, RETURN}

    public enum SQL {NONE,MODIFY,READ, CONTAINS}

    /**
     * Create a procedure of the given name.
     */
    public StoredProcedure(String name) {
        _name = DBIdentifier.newProcedure(name);
        _fromDatabase = false;
    }

    /**
     * <pre>
     *
     * 	1. PROCEDURE_CAT - String - the procedure catalog name
     * 	2. PROCEDURE_SCHEM - String - the procedure schema name (possibly null)
     * 	3. PROCEDURE_NAME - String - the procedure name
     * 	4. COLUMN_NAME - String - the name of the column
     * 	5. COLUMN_TYPE - short - the kind of column or parameter, as follows:
     * 			DatabaseMetaData.procedureColumnUnknown - type unknown
     * 			DatabaseMetaData.procedureColumnIn - an IN parameter
     * 			DatabaseMetaData.procedureColumnInOut - an INOUT parameter
     * 			DatabaseMetaData.procedureColumnOut - an OUT parameter
     * 			DatabaseMetaData.procedureColumnReturn - a return value
     * 			DatabaseMetaData.procedureReturnsResult - a result column in a result set
     * 	6. DATA_TYPE - int - the SQL type of the data, as in java.sql.Types
     * 	7. TYPE_NAME - String - the SQL type name, for a UDT it is fully qualified
     * 	8. PRECISION - int - the precision
     * 	9. LENGTH - int - the length of the data in bytes
     * 	10.SCALE - short - the scale for numeric types
     * 	11.RADIX - short - the Radix for numeric data (typically 2 or 10)
     * 	12.NULLABLE - short - can the data contain null:
     * 			DatabaseMetaData.procedureNoNulls - NULLs not permitted
     * 			DatabaseMetaData.procedureNullable - NULLs are permitted
     * 			DatabaseMetaData.procedureNullableUnknown - NULL status unknown
     * 	13.REMARKS - String - an explanatory comment about the data item
     * </pre>
     **/

    public StoredProcedure(ResultSet rs) throws SQLException {
        _fromDatabase = true;
        int i = 0;
        do {
            if (i == 0) {
                // get stored procedure metadata
                _catalog = DBIdentifier.newCatalog(rs.getString(1));
                _schema = DBIdentifier.newSchema(rs.getString(2));
                _name = DBIdentifier.newIdentifier(rs.getString(3), DBIdentifier.DBIdentifierType.PROCEDURE, false);
            }
            Column col = new Column();
            _cols.add(col);
            col.setIdentifier(DBIdentifier.newColumn(rs.getString(4)));
            col.setFlag(rs.getShort(5), true);
            col.setType(rs.getInt(6));
            col.setTypeIdentifier(DBIdentifier.newConstant(rs.getString(7)));
            col.setPrecision(rs.getInt(8));
            col.setSize(rs.getInt(9));
            col.setScale(rs.getInt(10));
            col.setRadix(rs.getShort(11));
            col.setNullability(rs.getShort(12));
            col.setComment(rs.getString(13));
            col.setIndex(i);
            _params.add(col.getIdentifier().getName() + " " + col.getTypeIdentifier().getName());
            i++;
        } while (rs.next());
    }

    public void setCatalog(DBIdentifier catalog) {
        this._catalog = catalog;
    }

    public void setSchema(DBIdentifier schema) {
        this._schema = schema;
    }

    public void setName(String name) {
        this._name = DBIdentifier.newIdentifier(name, DBIdentifier.DBIdentifierType.PROCEDURE, false);
    }

    public Column[] getInColumns() {
        return getColumns((short)DatabaseMetaData.procedureColumnIn);
    }

    public Column[] getInOutColumns() {
        return getColumns((short)DatabaseMetaData.procedureColumnInOut);
    }

    public Column[] getOutColumns() {
        return getColumns((short)DatabaseMetaData.procedureColumnOut);
    }

    public Column[] getReturnColumns() {
        return getColumns((short)DatabaseMetaData.procedureColumnReturn);
    }

    public Column[] getResultColumns() {
        return getColumns((short)DatabaseMetaData.procedureColumnResult);
    }

    public Column[] getColumns() {
        return _cols.toArray(new Column[_cols.size()]);
    }

    /**
     * Counts the number of columns with the given flag.
     * @param flag
     * @return
     */
    int countColumn(short flag) {
        int count = 0;
        for (Column col : _cols) {
            if (col.getFlag(flag)) count++;
        }
        return count;
    }

    Column[] getColumns(short flag) { // TODO: cache?
        List<Column> cols = null;
        for (Column col : _cols) {
            if (col.getFlag(flag)) {
                if (cols == null) cols = new ArrayList<>();
                cols.add(col);
            }
        }
        return cols == null ? Schemas.EMPTY_COLUMNS : cols.toArray(new Column[cols.size()]);
    }


    /**
     * Gets the name of this procedure.
     */
    public String getName() {
        return _name.getName();
    }

    /**
     * Adds an {@code IN} parameter of the given name and type.
     * @param var name of the variable
     * @param typeName name of the SQL type e.g. {@code VARCAR(32)}
     * @return this procedure instance
     */
    public StoredProcedure addParameter(String var, String typeName) {
        return addParameter(PARAM.IN, var, typeName);
    }
    /**
     * Adds the given parameter declaration.
     *
     * @param param type of parameter.
     * @param var name of the variable
     * @param typeName name of the SQL type e.g. {@code VARCAR(32)}
     * @return this procedure instance
     */
    public StoredProcedure addParameter(PARAM param, String var, String typeName) {
        assertMutable();

        _params.add(param + " " + var + " " + typeName);
        return this;
    }

    public StoredProcedure setLanguage(String language) {
        _sql.add("LANGUAGE " + language);
        return this;
    }

    /**
     * Gets the SQL for creating this procedure.
     */
    public String getCreateSQL() {
        StringBuilder buf = new StringBuilder();
        buf.append("CREATE PROCEDURE ");
        buf.append(_name);
        buf.append(" (");
        for (Iterator<String> p = _params.iterator(); p.hasNext();) {
            buf.append(p.next());
            buf.append(p.hasNext() ? "," : "");
        }
        buf.append(") ");
        //buf.append("(");
        for (String s : _sql) {
            buf.append(s).append(" ");
        }
        //buf.append(")");

        return buf.toString().trim();
    }

    /**
     * Gets the SQL for dropping this procedure.
     */
    public String getDropSQL() {
        return "DROP PROCEDURE " + _name;
    }

    /**
     * Gets the SQL for calling this procedure.
     */
    public String getCallSQL() {
        StringBuilder buf = new StringBuilder();
        buf.append("CALL ");
        buf.append(_name); buf.append(" (");
        for (Iterator<String> p = _params.iterator(); p.hasNext();) {
            p.next();
            buf.append("?");
            if (p.hasNext()) buf.append(",");
        }
        buf.append(")");
        return buf.toString().trim();
    }

    /**
     * Adds a read SQL statement via an external method.
     */
    public StoredProcedure setSQL(SQL sql) {
        switch (sql) {
            case CONTAINS : _sql.add("CONTAINS SQL"); break;
            case NONE     : _sql.add("NO SQL"); break;
            case MODIFY   : _sql.add("MODIFIES SQL DATA"); break;
            case READ     : _sql.add("READS SQL DATA"); break;
        }
        return this;
    }

    /**
     * Sets the language whose parameter passing convention will be used to pass paramater values.
     * @param lang
     * @return
     */
    public StoredProcedure setParameterStyle(String lang) {
        _sql.add("PARAMETER STYLE " + lang);
        return this;
    }

    public StoredProcedure setExternalName(Class<?> cls, String method, Class<?>... paramTypes) {
        assertStaticMethod(cls, method, paramTypes);
        _sql.add("EXTERNAL NAME '" + cls.getName() + '.' + method + "'");
        return this;
    }

    public StoredProcedure setResult(int i) {
        return setResult(i, false);
    }

    public StoredProcedure setResult(int i, boolean dynamic) {
        assertMutable();
        _sql.add((dynamic ? "DYNAMIC " : "") + "RESULT SETS " + i);
        return this;
    }

    private void assertStaticMethod(Class<?> cls, String method, Class<?>...paramTypes) {
        try {
            Method m = cls.getMethod(method, paramTypes);
            if (m == null || !Modifier.isStatic(m.getModifiers())) {
                throw new RuntimeException("No static method " + method + " with arguments "
                        + Arrays.toString(paramTypes) + " in " + cls);
            }
        } catch (Exception ex) {
            throw new RuntimeException("No static method " + method + " with arguments "
                    + Arrays.toString(paramTypes) + " in " + cls, ex);
        }
    }

    private void assertMutable() {
        if (_fromDatabase) {
            throw new IllegalStateException(this + " is not mutable");
        }
    }

    @Override
    public String toString() {
        return getName();
    }
}
