/*
 * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All Rights Reserved.
 * Copyright (c) 2012, 2018 Pervasive Software Inc. All Rights Reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//       Peter Lohman - initial implementation
//

 /*

    For minimal implementation, compare with:
         C:\PL\JPA\EclipseLink\SVN\org.eclipse.persistence\foundation\org.eclipse.persistence.core\src\org\eclipse\persistence\platform\database\CloudscapePlatform.java

    For PVSW data type mapping, see: getColumnClassName():C:\cmsynergy\psql11.20_pnl\psql\comp\sdk\jdbc\pvjdbc2\src\com\pervasive\jdbc\v2\ResultSetMetaData.java

*/
package org.eclipse.persistence.platform.database;

import java.io.IOException;
import java.io.Writer;
import java.util.Hashtable;
import java.util.Map;
import java.util.Vector;

import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.expressions.ExpressionOperator;
import org.eclipse.persistence.internal.databaseaccess.FieldTypeDefinition;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.queries.ValueReadQuery;
import org.eclipse.persistence.tools.schemaframework.FieldDefinition;

/** <p><b>Purpose</b>: Provides Pervasive SQL DBMS specific behavior.
*
* <br><br>
* Pervasive SQL Platform file <br>
*  Contributed by: Pervasive Software, Inc.<br>
*  Contributed under bug: 392109
*  <p>
*
* <u><b>Developed on Pervasive PSQL Server 11.30 </b></u>
* <blockquote>
* <ul>
* <li>Eclipselink Core SRG Test passes with known limitations.
* <li>Eclipselink JPA SRG Test passes with known limitations.
* <li>Eclipselink stored procedure tests "CustomSQLTestModel", "StoredProcedureGeneratorModel" pass with known limitations.
* </ul>
* </blockquote>
*
* <p><u><b>Limitations</b></u>
* <ul>
* <li> Updates are not supported on joined queries or queries with group by.
* <li> The platform method getSelectForUpdateString() currently returns an empty string. This is
* to avoid avoid joined queries with FOR UPDATE in them, which Pervasive does not support.
* <li> Columns used in indexes must total no more than 255 bytes in length.
* <li> Pervasive SQL does not support dynamic parameters in the SELECT list.
* <li> IDENTITY columns are either 2- or 4-byte integers. Foreign keys referencing such columns must use the same datatypes.
* </ul>
*
**/


public class PervasivePlatform extends org.eclipse.persistence.platform.database.DatabasePlatform {

    public static final int DEFAULT_CHAR_SIZE = 80;

    //
    // Cloned from AccessPlatform.java
    //
    @Override
    protected Map<String, Class> buildClassTypes() {
        Map<String, Class> classTypeMapping = super.buildClassTypes();

        // Causes BLOB to translate to LONGVARBINARY(via java.sql.Blob) instead of BINARY (via Byte[])
        classTypeMapping.put("BLOB", java.sql.Blob.class);

        return classTypeMapping;
    }


    @Override
    protected Hashtable buildFieldTypes() {
        Hashtable fieldTypeMapping;

        fieldTypeMapping = new Hashtable();
        fieldTypeMapping.put(String.class, new FieldTypeDefinition("VARCHAR", DEFAULT_CHAR_SIZE));
        // fieldTypeMapping.put(java.math.BigDecimal.class, new FieldTypeDefinition("BIGINT", false));
        fieldTypeMapping.put(java.math.BigInteger.class, new FieldTypeDefinition("BIGINT", false));
        fieldTypeMapping.put(Integer.class, new FieldTypeDefinition("INTEGER", false));
        fieldTypeMapping.put(Long.class, new FieldTypeDefinition("INTEGER", false));
        fieldTypeMapping.put(Short.class, new FieldTypeDefinition("SMALLINT", false));
        fieldTypeMapping.put(Byte.class, new FieldTypeDefinition("TINYINT", false));
        fieldTypeMapping.put(Float.class, new FieldTypeDefinition("REAL", false));
        fieldTypeMapping.put(Double.class, new FieldTypeDefinition("DOUBLE", false));
        fieldTypeMapping.put(Character.class, new FieldTypeDefinition("CHAR", 1));
        fieldTypeMapping.put(java.sql.Date.class, new FieldTypeDefinition("DATE", false));
        fieldTypeMapping.put(java.sql.Time.class, new FieldTypeDefinition("TIME", false));
        fieldTypeMapping.put(java.sql.Timestamp.class, new FieldTypeDefinition("TIMESTAMP", false));
        fieldTypeMapping.put(byte[].class, new FieldTypeDefinition("BINARY", DEFAULT_CHAR_SIZE ));
        fieldTypeMapping.put(Byte[].class, new FieldTypeDefinition("LONGVARBINARY", false));
        fieldTypeMapping.put(Character[].class, new FieldTypeDefinition("CHAR", DEFAULT_CHAR_SIZE));
        fieldTypeMapping.put(Boolean.class, new FieldTypeDefinition("BIT", false));
        fieldTypeMapping.put(java.sql.Blob.class, new FieldTypeDefinition("LONGVARBINARY", false));
        fieldTypeMapping.put(java.sql.Clob.class, new FieldTypeDefinition("LONGVARCHAR", false));

        fieldTypeMapping.put(java.math.BigDecimal.class, new FieldTypeDefinition("DECIMAL",38, 0));        // From MySQL
        fieldTypeMapping.put(Number.class, new FieldTypeDefinition("DECIMAL",38,0));                      // From MySQL


        // fieldTypeMapping.put(java.lang.Number.class, new FieldTypeDefinition("BIGINT", false));
        fieldTypeMapping.put(char[].class, new FieldTypeDefinition("LONGVARCHAR", false));
        fieldTypeMapping.put(java.util.Calendar.class, new FieldTypeDefinition("TIMESTAMP"));
        fieldTypeMapping.put(java.util.Date.class, new FieldTypeDefinition("TIMESTAMP"));

        return fieldTypeMapping;
    }

    /**
     *
     * Pervasive uses the INOUT keyword, as opposed to "IN OUT".
     */
    @Override
    public String getInOutputProcedureToken() {
        return "INOUT";
    }

    /**
     * Pervasive uses IN prefix for INPUT parameters.
     *
     */
    @Override
    public String getInputProcedureToken() {
        return "IN";
    }

    /**
     * Pervasive uses ":" as prefix for procedure arguments.
     */
    @Override
    public String getProcedureArgumentString() {
        return ":";
    }

    /**
     *
     * Pervasive requires BEGIN in a procedure statement.
     */
    @Override
    public String getProcedureBeginString() {
        return "BEGIN ";
    }

    /**
     * In CREATE PROCEDURE, Pervasive requires brackets after the procedure name, even if there are no arguments.
     */
    @Override
    public boolean requiresProcedureBrackets() {
        return true;
    }

    /**
     * Pervasive uses CALL or EXECUTE not CALL PROCEDURE or EXECUTE PROCEDURE
     */
    @Override
    public String getProcedureCallHeader() {
        return "CALL ";
    }

    /**
     *
     * Pervasive requires END in a procedure statement.
     */
    @Override
    public String getProcedureEndString() {
        return "END";
    }


    /**
     * Pervasive uses ":" as prefix for procedure parameters.
     */
    @Override
    public String getStoredProcedureParameterPrefix() {
        return ":";
    }


    /**
     * Pervasive requires the OUTPUT keyword for output parameters
     */
    @Override
    public boolean requiresProcedureCallOuputToken() {
        return true;
    }


    @Override
    protected void initializePlatformOperators() {
        super.initializePlatformOperators();

        addOperator(ExpressionOperator.simpleThreeArgumentFunction(ExpressionOperator.Substring, "SUBSTRING"));
        addOperator(singleArgumentSubstringOperator());
        addOperator(ExpressionOperator.simpleTwoArgumentFunction(ExpressionOperator.Nvl, "ISNULL"));
        addOperator(ExpressionOperator.simpleFunction(ExpressionOperator.Ceil, "CEILING"));
        addOperator(toNumberOperator());
        addOperator(toCharOperator());
        addOperator(toDateOperator());
    }

    /**
     * Cloned from MySQLPlatform.java
     *
     */

    /**
     * INTERNAL:
     * Pervasive SQL stored procedure calls do not require the argument name be printed in the call string
     * e.g. call MyStoredProc(?) instead of call MyStoredProc(myvariable = ?)
     */
    @Override
    public boolean shouldPrintStoredProcedureArgumentNameInCall(){
        return false;
    }


    protected ExpressionOperator toNumberOperator() {
        ExpressionOperator exOperator = new ExpressionOperator();
        exOperator.setType(ExpressionOperator.FunctionOperator);
        exOperator.setSelector(ExpressionOperator.ToNumber);
        Vector v = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(2);
        v.addElement("CONVERT(");
        v.addElement(", SQL_NUMERIC)");
        exOperator.printsAs(v);
        exOperator.bePrefix();
        exOperator.setNodeClass(ClassConstants.FunctionExpression_Class);
        return exOperator;
    }

    /**
     * Cloned from MySQLPlatform.java
     */
    protected ExpressionOperator toDateOperator() {
        ExpressionOperator exOperator = new ExpressionOperator();
        exOperator.setType(ExpressionOperator.FunctionOperator);
        exOperator.setSelector(ExpressionOperator.ToDate);
        Vector v = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(2);
        v.addElement("CONVERT(");
        v.addElement(", DATETIME)");
        exOperator.printsAs(v);
        exOperator.bePrefix();
        exOperator.setNodeClass(ClassConstants.FunctionExpression_Class);
        return exOperator;
    }

    /**
     * Cloned from MySQLPlatform.java
     */
    protected ExpressionOperator toCharOperator() {
        ExpressionOperator exOperator = new ExpressionOperator();
        exOperator.setType(ExpressionOperator.FunctionOperator);
        exOperator.setSelector(ExpressionOperator.ToChar);
        Vector v = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(2);
        v.addElement("CONVERT(");
        v.addElement(", SQL_CHAR)");
        exOperator.printsAs(v);
        exOperator.bePrefix();
        exOperator.setNodeClass(ClassConstants.FunctionExpression_Class);
        return exOperator;
    }

    /**
     *
     * Cloned from MySQLPlatform.java
     */
    protected ExpressionOperator dateToStringOperator() {
        ExpressionOperator exOperator = new ExpressionOperator();
        exOperator.setType(ExpressionOperator.FunctionOperator);
        exOperator.setSelector(ExpressionOperator.DateToString);
        Vector v = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(2);
        v.addElement("CONVERT(");
        v.addElement(", SQL_CHAR)");
        exOperator.printsAs(v);
        exOperator.bePrefix();
        exOperator.setNodeClass(ClassConstants.FunctionExpression_Class);
        return exOperator;
    }



    /**
     * Answers whether platform is Pervasive
     */
    @Override
    public boolean isPervasive() {
        return true;
    }

    /**
     * JDBC defines an outer join syntax which many drivers do not support. So we normally avoid it.
     */
    @Override
    public boolean shouldUseJDBCOuterJoinSyntax() {
        return false; // not sure about this
    }

    /** Append the receiver's field 'identity' constraint clause to
    *   a writer.
    *
    *  Taken from
    *  org.eclipse.persistence\foundation\org.eclipse.persistence.core\src\org\eclipse\persistence\platform\database\AccessPlatform.java
    */
    @Override
    public void printFieldIdentityClause(Writer writer)    throws ValidationException {
        try {
            writer.write(" IDENTITY");
        } catch (IOException ioException) {
            throw ValidationException.fileError(ioException);
        }
    }


    /**
     * Override the default SubstringSingleArg operator.
     * Cloned from SybasePlatform.java
     */
    public ExpressionOperator singleArgumentSubstringOperator() {
        ExpressionOperator result = new ExpressionOperator();
        result.setSelector(ExpressionOperator.SubstringSingleArg);
        result.setType(ExpressionOperator.FunctionOperator);
        Vector v = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance();
        v.addElement("SUBSTRING(");
        v.addElement(",");
        v.addElement(", CHAR_LENGTH(");
        v.addElement("))");
        result.printsAs(v);
        int[] indices = new int[3];
        indices[0] = 0;
        indices[1] = 1;
        indices[2] = 0;

        result.setArgumentIndices(indices);
        result.setNodeClass(ClassConstants.FunctionExpression_Class);
        result.bePrefix();
        return result;
    }




    /**
     *
     *  Indicates whether the platform supports identity.
     *
     */
     @Override
    public boolean supportsIdentity() {
         return true;
     }

     //
     //  Most Temp Table settings cloned from SQLServerPlatform.java
     //
     /**
      * INTERNAL:
      */
     @Override
    public boolean supportsLocalTempTables() {
         return true;
     }


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


     /**
      * INTERNAL:
      */
     @Override
    protected String getCreateTempTableSqlPrefix() {
         return "CREATE TABLE ";
     }

     /**
      * INTERNAL:
      */
     @Override
    public DatabaseTable getTempTableForTable(DatabaseTable table) {
         return new DatabaseTable("#" + table.getName(), table.getTableQualifier(), table.shouldUseDelimiters(), getStartDelimiter(), getEndDelimiter());
     }


     /**
     *
     * Taken from org.eclipse.persistence\foundation\org.eclipse.persistence.core\src\org\eclipse\persistence\platform\database\AccessPlatform.java
     */
     @Override
    public void printFieldTypeSize(Writer writer, FieldDefinition field,FieldTypeDefinition fieldType, boolean shouldPrintFieldIdentityClause) throws IOException {
        if (!shouldPrintFieldIdentityClause) {
            // if type requires both precision and scale: NUMERIC, DECIMAL
            if ((fieldType.getName().equals("NUMERIC")) || (fieldType.getName().equals("DECIMAL"))) {
                writer.write(fieldType.getName());
                writer.write("(");
                if (field.getSize() == 0) {
                    writer.write(Integer.toString(fieldType.getDefaultSize()));
                } else {
                    writer.write(Integer.toString(field.getSize()));
                }
                writer.write(",");
                if (field.getSubSize() != 0) {
                    writer.write(Integer.toString(field.getSubSize()));
                } else {
                    writer.write(Integer.toString(fieldType.getDefaultSubSize()));
                }
                writer.write(")");
            } else {
                super.printFieldTypeSize(writer, field, fieldType,
                        shouldPrintFieldIdentityClause);
            }
        }
    }

    /**
     * INTERNAL:
     * Build the identity query for native sequencing.
     *
     * Taken verbatim from org.eclipse.persistence\foundation\org.eclipse.persistence.core\src\org\eclipse\persistence\platform\database\SQLServerPlatform.java
     *
     */
     @Override
     public ValueReadQuery buildSelectQueryForIdentity() {
         ValueReadQuery selectQuery = new ValueReadQuery();
         selectQuery.setSQLString("SELECT @@IDENTITY");
         return selectQuery;
    }

     /**
      * Temporary workaround to avoid joined queries with FOR UPDATE
      * in them
      *
      */
     @Override
    public String getSelectForUpdateString() {
         return "";
     }




        /**
     * INTERNAL:
     * Indicates whether SELECT DISTINCT ... FOR UPDATE is allowed by the platform (Oracle doesn't allow this).
     */
    @Override
    public boolean isForUpdateCompatibleWithDistinct() {
        return false;
    }


     /**
     * Setting this to false (cf. Sybase) to work around problem
     * that unspecified default delete rule is RESTRICT, even when
     * not allowed due to self-referencing table.
     */
    @Override
    public boolean supportsDeleteOnCascade() {
        return true;
    }


    /** Attempts to remove FOR UPDATE from queries */
    @Override
    public boolean shouldPrintLockingClauseAfterWhereClause() {
        return false;
    }

        /**
     * INTERNAL:
     * Indicates whether locking clause could be applied to the query that has more than one table
     */
    @Override
    public boolean supportsLockingQueriesWithMultipleTables() {
        return false;
    }




}
