/*


 ========== licence begin GPL
    Copyright (C) 2002-2003 SAP AG

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    as published by the Free Software Foundation; either version 2
    of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    ========== licence end


 */

package com.sap.dbtech.jdbc;

import java.util.HashMap;
import java.util.Vector;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import com.sap.dbtech.jdbc.translators.DBTechTranslator;
import com.sap.dbtech.vsp001.FunctionCode;
import com.sap.dbtech.jdbc.exceptions.InternalJDBCError;
import com.sap.dbtech.jdbc.packet.*;
import com.sap.dbtech.vsp001.PartKind;
import com.sap.dbtech.util.MessageKey;
import com.sap.dbtech.util.MessageTranslator;

/**
 *  
 */
public class Parseinfo {
    ConnectionSapDB connection;
   
    String sqlCmd;

    private byte[] parseid;

    private byte[] massParseid;

    DBTechTranslator[] paramInfos;

    DBProcParameterInfo[] procParamInfos;

    int inputCount;

    private boolean isMassCmd; /*
                                * flag is set to true if command is a mass
                                * command
                                */

    boolean isSelect; /* flag is set to true if command is a select command */

    boolean isDBProc; /* flag is set to true if command is a call dbproc command */

    boolean hasLongs; /* flag is set to true if command handle long columns */

    boolean hasStreams;

    boolean cached; /* flag is set to true if command is in parseinfo cache */

    int functionCode;

    int sessionID; /* unique identifier for the connection */

    String[] columnNames;

    java.util.AbstractMap columnMap;

    DBTechTranslator[] columnInfos;

    boolean isClosed = false;

    boolean varDataInput = false;

    /* 11th Byte of Parseid coded application code */
    private static final int applicationCodeByte = 10;

    /**
     * tablename used for updateable resultsets
     */
    String updTableName;

    /**
     * creates a new Parseinfo
     */
    public Parseinfo(ConnectionSapDB connection, String sqlCmd, int functionCode)
            throws SQLException {
        this.connection = connection;
        this.sqlCmd = sqlCmd;
        this.massParseid = null;
        this.paramInfos = null;
        this.inputCount = 0;
        this.isSelect = false;
        this.isDBProc = false;
        this.hasLongs = false;
        this.hasStreams = false;
        this.isMassCmd = false;
        this.functionCode = functionCode;
        this.sessionID = -1;
        this.updTableName = null;
        this.cached = false;
        varDataInput = false;
        if ((functionCode == FunctionCode.Select_FC)
                || (functionCode == FunctionCode.Show_FC)
                || (functionCode == FunctionCode.DBProcWithResultSetExecute_FC)
                || (functionCode == FunctionCode.Explain_FC)) {
            this.isSelect = true;
        }
        if ((functionCode == FunctionCode.DBProcWithResultSetExecute_FC)
                || (functionCode == FunctionCode.DBProcExecute_FC)) {
            this.isDBProc = true;
        }
        this.columnNames = null;
        this.columnMap = null;
        if(functionCode == FunctionCode.DBProcExecute_FC) {
            describeProcedureCall();
        }
    }

    void describeProcedureCall() throws SQLException {

        if (this.connection.getKernelVersion() <= 70402) {
            this.procParamInfos = new DBProcParameterInfo[0];
            return;
        }

        // Syntax is one of
        // { CALL <procedure-name>(...) }
        // CALL <procedure-name>(...)
        // where procedure-name is something like IDENTIFIER, "IDENTIFIER",
        // "OWNER"."IDENTIFIER" etc.
        // we always simply give up if we find nothing that helps our needs

        //
        char cmdchars[] = sqlCmd.trim().toCharArray();
        int i = 0;
        int cmdchars_len = cmdchars.length;
        // ODBC like dbfunction call.
        if (cmdchars[i] == '{') {
            ++i;
        }
        if (i == cmdchars_len) {
            return;
        }
        while (Character.isSpace(cmdchars[i])) {
            ++i;
            if (i == cmdchars_len) {
                return;
            }
        }
        // 'call'
        if (cmdchars[i] == 'C' || cmdchars[i] == 'c') {
            ++i;
            if (i == cmdchars_len) {
                return;
            }
        } else {
            return;
        }
        if (cmdchars[i] == 'A' || cmdchars[i] == 'a') {
            ++i;
            if (i == cmdchars_len) {
                return;
            }
        } else {
            return;
        }
        if (cmdchars[i] == 'L' || cmdchars[i] == 'l') {
            ++i;
            if (i == cmdchars_len) {
                return;
            }
        } else {
            return;
        }
        if (cmdchars[i] == 'L' || cmdchars[i] == 'l') {
            ++i;
            if (i == cmdchars_len) {
                return;
            }
        } else {
            return;
        }
        while (Character.isSpace(cmdchars[i])) {
            ++i;
            if (i == cmdchars_len) {
                return;
            }
        }
        // now to the mess of parsing the first identifier.
        int idstart = i;
        int idend = i;
        boolean quoted = false;
        if (cmdchars[i] == '"') {
            ++idstart;
            ++idend;
            quoted = true;
            ++i;
            if (i == cmdchars_len) {
                return;
            }
        }
        do {
            if (cmdchars[i] == '.' && !quoted) {
                break;
            }
            if (cmdchars[i] == '(' && !quoted) {
                break;
            }
            if (Character.isSpace(cmdchars[i]) && !quoted) {
                break;
            }
            if (quoted && cmdchars[i] == '"') {
                break;
            }
            ++idend;
            ++i;
            if (i == cmdchars_len) {
                return;
            }
        } while (true);
        String procedureName = new String(cmdchars, idstart, idend - idstart);
        String ownerName = null;
        if (!quoted) {
            procedureName = procedureName.toUpperCase();
        }
        if (cmdchars[i] == '"') {
            ++i;
        }
        while (i < cmdchars_len && Character.isSpace(cmdchars[i])) {
            ++i;
            if (i == cmdchars_len) {
                break;
            }
        }
        if (i < cmdchars_len) {
            if (cmdchars[i] == '.') {
                ++i;
                if (i == cmdchars_len) {
                    return;
                }
                while (Character.isSpace(cmdchars[i])) {
                    ++i;
                    if (i == cmdchars_len) {
                        return;
                    }
                }
                idstart = i;
                idend = i;
                quoted = false;
                if (cmdchars[i] == '"') {
                    ++idstart;
                    ++idend;
                    quoted = true;
                    ++i;
                    if (i == cmdchars_len) {
                        return;
                    }
                }
                do {
                    if (cmdchars[i] == '.' && !quoted) {
                        break;
                    }
                    if (cmdchars[i] == '(' && !quoted) {
                        break;
                    }
                    if (Character.isSpace(cmdchars[i]) && !quoted) {
                        break;
                    }
                    if (quoted && cmdchars[i] == '"') {
                        break;
                    }
                    ++idend;
                    ++i;
                    if (i == cmdchars_len) {
                        return;
                    }
                } while (true);
                procedureName = new String(cmdchars, idstart, idend - idstart);
                if (!quoted) {
                    procedureName = procedureName.toUpperCase();
                }
            }
        }

        // Now we have procedure name and possibly the user name.
        PreparedStatement ps = null;
        String stmtstring = "SELECT 1 FROM DUAL WHERE FALSE";
        if (ownerName == null) {
            if (this.connection.getKernelVersion() > 70402) {
                stmtstring = "SELECT PARAM_NO, "
                        + "DATATYPE, CODE, LEN, DEC, \"IN/OUT-TYPE\", OFFSET, ASCII_OFFSET, "
                        + "UNICODE_OFFSET FROM DBPROCPARAMINFO WHERE OWNER=USER AND "
                        + "DBPROCEDURE=? ORDER BY PARAM_NO,ASCII_OFFSET";
            } else {
                stmtstring = "SELECT PARAM_NO, "
                        + "DATATYPE, CODE, LEN, DEC, \"IN/OUT-TYPE\", OFFSET, OFFSET AS ASCII_OFFSET, "
                        + "OFFSET AS UNICODE_OFFSET FROM DBPROCPARAMINFO WHERE OWNER=USER AND "
                        + "DBPROCEDURE=? ORDER BY PARAM_NO,OFFSET";
            }
            ps = this.connection.prepareStatement(stmtstring);
            ps.setString(1, procedureName);
        } else {
            if (this.connection.getKernelVersion() > 70402) {
                stmtstring = "SELECT PARAM_NO, "
                        + "DATATYPE, CODE, LEN, DEC, \"IN/OUT-TYPE\", OFFSET, ASCII_OFFSET, "
                        + "UNICODE_OFFSET FROM DBPROCPARAMINFO WHERE OWNER=? AND "
                        + "DBPROCEDURE=? ORDER BY PARAM_NO,ASCII_OFFSET";
            } else {
                stmtstring = "SELECT PARAM_NO, "
                        + "DATATYPE, CODE, LEN, DEC, \"IN/OUT-TYPE\", OFFSET, OFFSET AS ASCII_OFFSET, "
                        + "OFFSET AS UNICODE_OFFSET FROM DBPROCPARAMINFO WHERE OWNER=? AND "
                        + "DBPROCEDURE=? ORDER BY PARAM_NO,OFFSET";
            }
            ps.setString(1, ownerName);
            ps.setString(2, procedureName);
        }
        // We have a result set and can now create a parameter info.
        ResultSet rs = ps.executeQuery();
        if (!rs.next()) {
            this.procParamInfos = new DBProcParameterInfo[0];
            rs.close(); rs=null;
            ps.close(); ps=null;
            return;
        }
        Vector parameterInfos = new Vector();
        DBProcParameterInfo currentInfo = null;
        int currentIndex = 0;
        do {
            int index = rs.getInt(1);
            // Check if we have a structure element or a
            // new parameter.
            if (index != currentIndex) {
                String datatype = rs.getString(2);
                if (datatype.equals("ABAPTABLE")
                        || datatype.equals("STRUCTURE")) {
//                    String code = rs.getString(3);
                    int len = rs.getInt(4);
                    int dec = rs.getInt(5);
                    currentInfo = new DBProcParameterInfo(datatype, len, dec);
                    parameterInfos.addElement(currentInfo);
                } else {
                    currentInfo = null;
                    parameterInfos.addElement(currentInfo);
                }
                currentIndex = index;
            } else {
                String datatype = rs.getString(2);
                String code = rs.getString(3);
                int len = rs.getInt(4);
                int dec = rs.getInt(5);
                int offset = rs.getInt(7);
                int asciiOffset = rs.getInt(8);
                int unicodeOffset = rs.getInt(9);
                currentInfo.addStructureElement(datatype, code, len, dec,
                        offset, asciiOffset, unicodeOffset);
            }
        } while (rs.next());
        rs.close();
        rs = null;
        ps.close();
        ps = null;
        this.procParamInfos = (DBProcParameterInfo[]) parameterInfos
                .toArray(new DBProcParameterInfo[0]);
    }

    /**
     *  
     */
    public byte[] getMassParseid() {
        return this.massParseid;
    }

    /**
     *  
     */
    public boolean setMassParseid(byte[] aMassParseid) {
        this.massParseid = aMassParseid;
        if (aMassParseid == null) {
            return false;
        }
        for (int i = 0; i < FunctionCode.massCmdAppCodes.length; i++) {
            if (aMassParseid[applicationCodeByte] == FunctionCode.massCmdAppCodes[i]) {
                this.isMassCmd = true;
                return true;
            }
        }
        return false;
    }

    /**
     *  
     */
    public boolean isMassCmd() {
        return this.isMassCmd;
    }

    /**
     *  
     */
    public void setUpdateTableName(String atablename) {
        this.updTableName = atablename;
    }

    /**
     * Checks the validity. A parse info is valid if the session is the same as
     * of the current connection.
     * 
     * @return <code>true</code> if the session ids are equal
     */
    public boolean isValid() {
        return this.sessionID == this.connection.sessionID;
    }

    /**
     * Gets the column infos, needed for result set meta data. If no result
     * set/no result set meta data available then <code>null</code> is
     * returned.
     */
    public DBTechTranslator[] getColumnInfos() {
        return columnInfos;
    }

    /**
     * Sets a parse id, together with the correct session id.
     * 
     * @param parseId
     *            the parse id.
     * @param sessionId
     *            the session id of the parse id.
     */
    public void setParseIdAndSession(byte[] parseId, int sessionId) {
        this.sessionID = sessionId;
        this.parseid = parseId;
    }

    public synchronized void dropParseIDs() {
        if (this.parseid != null && this.connection != null) {
            this.connection.dropParseid(this.parseid);
            this.parseid = null;
        }
        if (this.massParseid != null && this.connection != null) {
            this.connection.dropParseid(this.massParseid);
            this.massParseid = null;
        }

    }

    /**
     * 
     * @exception java.sql.SQLException
     *                The exception description.
     */
    protected void finalize() {
        this.cached = false;
        dropParseIDs();
    }

    /**
     * Gets the information about parameters in sql statement
     * 
     * @return a <code>DBTechTranslator []</code holding the parameter infos
     */
    public DBTechTranslator[] getParamInfo() {
        return this.paramInfos;
    }

    /**
     * Marks/Unmarks the statement as select.
     * 
     * @param select
     *            the select mark
     */
    public void setSelect(boolean select) {
        this.isSelect = select;
    }

    /**
     * Gets the parse id.
     */
    public byte[] getParseId() {
        return parseid;
    }

    /**
     * Retrieves whether the statement is already executed during parse. (by
     * checking byte 11 of the parse if for <code>csp1_p_command_executed</code>.
     */
    public boolean isAlreadyExecuted() {
        return (parseid != null && parseid[Parseinfo.applicationCodeByte] == FunctionCode.csp1_p_command_executed);
    }

    /**
     * Sets the infos about parameters and result columns.
     * 
     * @param shortInfo
     *            info about the parameters and result columns
     * @param columnames
     *            the names of the result columns
     */
    public void setShortInfosAndColumnNames(DBTechTranslator[] shortInfo,
            String[] columnNames) throws SQLException {
        // clear the internal dependent fields
        inputCount = 0;
        hasLongs = false;
        hasStreams = false;
        this.columnNames = null;
        this.paramInfos = null;
        this.columnMap = null;
        this.columnInfos = null;
        this.columnNames = columnNames;

        if (shortInfo == null && columnNames == null) {
            this.paramInfos = this.columnInfos = new DBTechTranslator[0];
            return;
        }

        // we have variants:
        // only a select is really good. All other variants
        // do not and never deliver information on being prepared.
        if (functionCode == FunctionCode.Select_FC) {
            if (columnNames == null || columnNames.length == 0) {
                // this.columnInfos=null;
                this.paramInfos = shortInfo;
                for (int i = 0; i < this.paramInfos.length; ++i) {
                    DBTechTranslator current = shortInfo[i];
                    if (current.isInput()) {
                        current.setColIndex(i);
                        inputCount++;
                    }
                    hasLongs |= current.isLongKind();
                    hasStreams |= current.isStreamKind();
                }
            } else {
                int column_count = columnNames.length;
                this.columnInfos = new DBTechTranslator[column_count];
                this.paramInfos = new DBTechTranslator[shortInfo.length
                        - column_count];

                int colInfoIdx = 0;
                int paramInfoIdx = 0;

                for (int i = 0; i < shortInfo.length; ++i) {
                    DBTechTranslator current = shortInfo[i];
                    if (current.isInput()) {
                        if (paramInfoIdx == this.paramInfos.length) {
                            throw new InternalJDBCError(
                                    MessageTranslator
                                            .translate(
                                                    MessageKey.ERROR_INTERNAL_UNEXPECTEDINPUT,
                                                    Integer
                                                            .toString(paramInfoIdx)));
                        }
                        current.setColIndex(paramInfoIdx);
                        paramInfos[paramInfoIdx] = current;
                        paramInfoIdx++;
                        inputCount++;
                    } else {
                        if (colInfoIdx == this.columnInfos.length) {
                            throw new InternalJDBCError(
                                    MessageTranslator
                                            .translate(
                                                    MessageKey.ERROR_INTERNAL_UNEXPECTEDOUTPUT,
                                                    Integer
                                                            .toString(colInfoIdx)));
                        }
                        columnInfos[colInfoIdx] = current;
                        current.setColIndex(colInfoIdx);
                        current.setColName(columnNames[colInfoIdx]);
                        colInfoIdx++;
                    }
                    hasLongs |= shortInfo[i].isLongKind();
                    hasStreams |= shortInfo[i].isStreamKind();
                }
            }
        } else { // no result set data, as we cannot to be sure
            this.paramInfos = shortInfo;
            if (columnNames != null) {
                // fortunately at least column names
                // sometimes only output parameters are named
                if (columnNames.length == paramInfos.length) {
                    for (int i = 0; i < columnNames.length; ++i) {
                        DBTechTranslator current = paramInfos[i];
                        current.setColIndex(i);
                        current.setColName(columnNames[i]);
                        if (this.procParamInfos != null
                                && i < procParamInfos.length) {
                            current.setProcParamInfo(procParamInfos[i]);
                        }
                        inputCount += current.isInput() ? 1 : 0;
                        hasLongs |= current.isLongKind();
                        hasStreams |= current.isStreamKind();
                    }
                } else { // we will leave out the input parameters
                    int colNameIdx = 0;
                    for (int j = 0; j < paramInfos.length; ++j) {
                        DBTechTranslator current = paramInfos[j];
                        current.setColIndex(j);
                        if (this.procParamInfos != null
                                && j < procParamInfos.length) {
                            current.setProcParamInfo(procParamInfos[j]);
                        }
                        if (current.isOutput()) {
                            current.setColName(columnNames[colNameIdx++]);
                        } else {
                            ++inputCount;
                        }
                        hasLongs |= current.isLongKind();
                        hasStreams |= current.isStreamKind();
                    }
                }
            } else {
                // No column names at all. OK.
                for (int i = 0; i < paramInfos.length; ++i) {
                    DBTechTranslator current = paramInfos[i];
                    current.setColIndex(i);
                    if (this.procParamInfos != null
                            && i < procParamInfos.length) {
                        current.setProcParamInfo(procParamInfos[i]);
                    }
                    inputCount += current.isInput() ? 1 : 0;
                    hasLongs |= current.isLongKind();
                    hasStreams |= current.isStreamKind();
                }
            }
        }
    }

    public java.util.AbstractMap getColumnMap() throws java.sql.SQLException {
        if (this.columnMap != null)
            return this.columnMap;

        if (this.columnNames == null)
            throw new InternalJDBCError(MessageTranslator
                    .translate(MessageKey.ERROR_NO_COLUMNNAMES));

        this.columnMap = new HashMap(columnNames.length);
        for (int i = 0; i < paramInfos.length; ++i) {
            DBTechTranslator current = paramInfos[i];
            String colname = current.getColumnName();
            if (colname != null) {
                columnMap.put(colname, current);
            }
        }

        return this.columnMap;
    }

    /**
     * 
     * @exception java.sql.SQLException
     *                The exception description.
     */
    void doDescribeParseId() throws SQLException {
        RequestPacket requestPacket;
        ReplyPacket replyPacket;
        PartEnumeration enuma;
        String[] columnNames = null;
        DBTechTranslator[] infos = null;

        requestPacket = this.connection.getRequestPacket(false);
        requestPacket.initDbsCommand(false, "Describe ",
                ResultSet.TYPE_FORWARD_ONLY);
        requestPacket.addParseidPart(this.parseid);
        replyPacket = this.connection.execute(requestPacket, this,
                ConnectionSapDB.GC_ALLOWED);

        /*
         * parse result
         */
        enuma = replyPacket.partEnumeration();
        while (enuma.hasMoreElements()) {
            enuma.nextElement();
            switch (enuma.partKind()) {
            case PartKind.Columnnames_C:
                columnNames = replyPacket.parseColumnNames();
                break;
            case PartKind.Shortinfo_C:
                infos = replyPacket.parseShortFields(
                        this.connection.isSpaceoptionSet, false, null, false);
                break;
            case PartKind.Vardata_Shortinfo_C:
                this.varDataInput = true;
                infos = replyPacket.parseShortFields(
                        this.connection.isSpaceoptionSet, false, null, true);
                break;
            default:
                //this.addWarning (new SQLWarning ("part " +
                //        PartKind.names [enum.partKind ()] + " not handled"));
                break;
            }
        }
        this.setMetaData(infos, columnNames);
    }

    /**
     * 
     * @exception java.sql.SQLException
     *                The exception description.
     */
    void setMetaData(DBTechTranslator[] info, String[] colName)
            throws SQLException {
        int colCount = info.length;
        DBTechTranslator currentInfo;
        String currentName;
        this.columnNames = colName;

        if (colCount == colName.length) {
            this.columnInfos = info;
            for (int i = 0; i < colCount; ++i) {
                currentInfo = info[i];
                currentName = colName[i];
                currentInfo.setColName(currentName);
                currentInfo.setColIndex(i);
            }
        } else {
            int outputColCnt = 0;
            this.columnInfos = new DBTechTranslator[colName.length];
            for (int i = 0; i < colCount; ++i) {
                if (info[i].isOutput()) {
                    currentInfo = this.columnInfos[outputColCnt] = info[i];
                    currentName = colName[outputColCnt];
                    currentInfo.setColName(currentName);
                    currentInfo.setColIndex(outputColCnt++);
                }
            }
        }
    }
}
