/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. 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:
//     Oracle - initial API and implementation from Oracle TopLink
//     tware - added handling of database delimiters
//     03/24/2011-2.3 Guy Pelletier
//       - 337323: Multi-tenant with shared schema support (part 1)
//     05/30/2012-2.4 Guy Pelletier
//       - 354678: Temp classloader is still being used during metadata processing
//     02/11/2013-2.5 Guy Pelletier
//       - 365931: @JoinColumn(name="FK_DEPT",insertable = false, updatable = true) causes INSERT statement to include this data value that it is associated with
package org.eclipse.persistence.internal.helper;

//javase imports
import java.io.Serializable;
import java.security.AccessController;
import java.security.PrivilegedActionException;

import static java.lang.Integer.MIN_VALUE;

//EclipseLink imports
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.core.helper.CoreField;
import org.eclipse.persistence.internal.databaseaccess.DatabasePlatform;
import org.eclipse.persistence.internal.databaseaccess.DatasourcePlatform;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedClassForName;

/**
 * INTERNAL:
 * <p><b>Purpose</b>:
 * Define a fully qualified field name.<p>
 * <b>Responsibilities</b>:    <ul>
 * <li> Know its name and its table.
 * </ul>
 * @see DatabaseTable
 */
public class DatabaseField implements Cloneable, Serializable, CoreField  {
    /** Variables used for generating DDL **/
    protected int scale;
    protected int length;
    protected int precision;
    protected boolean isUnique;
    protected boolean isNullable;
    protected boolean isUpdatable;
    protected boolean isInsertable;
    protected boolean isCreatable;
    protected boolean isPrimaryKey;
    protected String columnDefinition;

    /** Column name of the field. */
    protected String name;

    /** PERF: Cache fully qualified table.field-name. */
    protected String qualifiedName;

    /** Fields table (encapsulates name + creator). */
    protected DatabaseTable table;

    /**
     * Respective Java type desired for the field's value, used to optimize performance and for binding.
     * PERF: Allow direct variable access from getObject.
     */
    public transient Class type;
    public String typeName; // shadow variable - string name of above Class type variable

    /**
     * Respective JDBC type of the field's value.
     * This overrides the class type, which the JDBC type is normally computed from.
     * PERF: Allow direct variable access from getObject.
     */
    public int sqlType;

    /**
     * Store normal index of field in result set to optimize performance.
     * PERF: Allow direct variable access from getIndicatingNoEntry.
     */
    public int index;

    protected boolean useDelimiters = false;

    /**
     * If this is set, it will be used in determining equality (unless delimiters are used) and the hashcode.
     * @see getNameForComparisons
     */
    protected String nameForComparisons;

    /**
     * setting to true will cause getNameForComparisons to lazy initialize nameForComparisons using
     * the value from getName().toUpperCase().
     */
    protected boolean useUpperCaseForComparisons = false;

    /**
     * used to represent the value when it has not being defined
     */
    public static final int NULL_SQL_TYPE = MIN_VALUE;

    /**
     * Returns true if this field was translated.
     */
    protected boolean isTranslated = false;

    /**
     * Indicates whether the field should be kept in the record after the object is created.
     * Used by ObjectLevelReadQuery ResultSetAccessOptimization.
     */
    public boolean keepInRow;

    public DatabaseField() {
        this("", new DatabaseTable());
    }

    public DatabaseField(String qualifiedName) {
        this(qualifiedName, null, null);
    }

    public DatabaseField(String qualifiedName, String startDelimiter, String endDelimiter) {
        this.index = -1;
        this.sqlType = NULL_SQL_TYPE;
        int index = qualifiedName.lastIndexOf('.');

        if (index == -1) {
            setName(qualifiedName, startDelimiter, endDelimiter);
            this.table = new DatabaseTable();
        } else {
            setName(qualifiedName.substring(index + 1, qualifiedName.length()), startDelimiter, endDelimiter);
            this.table = new DatabaseTable(qualifiedName.substring(0, index), startDelimiter, endDelimiter);
        }
        initDDLFields();
    }

    public DatabaseField(String fieldName, String tableName) {
        this(fieldName, new DatabaseTable(tableName));
    }

    public DatabaseField(String fieldName, DatabaseTable databaseTable) {
        this(fieldName, databaseTable, null, null);
    }

    public DatabaseField(String fieldName, DatabaseTable databaseTable, String startDelimiter, String endDelimiter) {
        this.index = -1;
        this.sqlType = NULL_SQL_TYPE;
        setName(fieldName, startDelimiter, endDelimiter);
        this.table = databaseTable;
        initDDLFields();
    }

    /**
     * Inits the DDL generation fields with our defaults. Note: we used to
     * initialize the length to the JPA default of 255 but since this default
     * value should only apply for string fields we set it to 0 to indicate
     * that it was not specified and rely on the default (255) to come from
     * individual platforms.
     */
    public void initDDLFields() {
        scale = 0;
        length = 0;
        precision = 0;
        isUnique = false;
        isNullable = true;
        isUpdatable = true;
        isInsertable = true;
        isCreatable = true;
        isPrimaryKey = false;
        columnDefinition = "";
    }

    /**
     * The table is not cloned because it is treated as an automatic value.
     */
    public DatabaseField clone() {
        try {
            return (DatabaseField)super.clone();
        } catch (CloneNotSupportedException exception) {
            throw new InternalError(exception.getMessage());
        }
    }

    /*
     * INTERNAL:
     * Convert all the class-name-based settings in this mapping to actual
     * class-based settings. This method is implemented by subclasses as
     * necessary.
     * @param classLoader
     */
    public void convertClassNamesToClasses(ClassLoader classLoader) {
        if (type == null && typeName != null) {
            try {
                if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){
                    try {
                        type = AccessController.doPrivileged(new PrivilegedClassForName(typeName, true, classLoader));
                    } catch (PrivilegedActionException e) {
                        throw ValidationException.classNotFoundWhileConvertingClassNames(typeName, e.getException());
                    }
                } else {
                    type = org.eclipse.persistence.internal.security.PrivilegedAccessHelper.getClassForName(typeName, true, classLoader);
                }
            } catch (Exception exception) {
                throw ValidationException.classNotFoundWhileConvertingClassNames(typeName, exception);
            }
        }
    }

    /**
     * Determine whether the receiver is equal to a DatabaseField.
     * Return true if the receiver and field have the same name and table.
     * Also return true if the table of the receiver or field are unspecified,
     * ie. have no name.
     */
    public boolean equals(Object object) {
        if (!(object instanceof DatabaseField)) {
            return false;
        }

        return equals((DatabaseField)object);
    }

    /**
     * Determine whether the receiver is equal to a DatabaseField.
     * Return true if the receiver and field have the same name and table.
     * Also return true if the table of the receiver or field are unspecified,
     * ie. have no name.
     */
    public boolean equals(DatabaseField field) {
        if (this == field) {
            return true;
        }

        if (field != null) {
            // PERF: Optimize common cases first.
            // PERF: Use direct variable access.
            if (getQualifiedName().equals(field.getQualifiedName())) {
                return true;
            }

            //preserve old behavior if static shouldIgnoreCaseOnFieldComparisons is set
            if (DatabasePlatform.shouldIgnoreCaseOnFieldComparisons()) {
                if (this.name.equalsIgnoreCase(field.name)) {
                    //getTableName will cause NPE if there isn't a table.  use hasTableName instead
                    if ((!hasTableName()) || (!field.hasTableName())) {
                        return true;
                    }
                    return (this.table.equals(field.table));
                }
            } else {
                String ourNameToCompare;
                String fieldNameToCompare;
                if (field.shouldUseDelimiters() || shouldUseDelimiters()) {
                    ourNameToCompare = this.name;
                    fieldNameToCompare = field.name;
                } else {
                    ourNameToCompare = getNameForComparisons();
                    fieldNameToCompare = field.getNameForComparisons();
                }

                if (this.name.equals(field.name) || ourNameToCompare.equals(fieldNameToCompare)) {
                    //getTableName will cause NPE if there isn't a table.  use hasTableName instead
                    if ((!hasTableName()) || (!field.hasTableName())) {
                        return true;
                    }
                    return (this.table.equals(field.table));
                }
            }
        }

        return false;
    }

    /**
     * Get the SQL fragment that is used when generating the DDL for the column.
     */
    public String getColumnDefinition() {
        return this.columnDefinition;
    }

    /**
     * Return the expected index that this field will occur in the result set
     * row. This is used to optimize performance of database row field lookups.
     */
    public int getIndex() {
        return index;
    }

    /**
     * Used to specify the column length when generating DDL.
     */
    public int getLength() {
        return this.length;
    }

    /**
     * Return the unqualified name of the field.
     */
    @Override
    public String getName() {
        return name;
    }

    /**
     * Returns this fields name with database delimiters if useDelimiters is true.
     * This method should be called any time the field name is requested for writing SQL.
     */
    public String getNameDelimited(DatasourcePlatform platform) {
        if (this.useDelimiters){
            return platform.getStartDelimiter() + this.name + platform.getEndDelimiter();
        }
        return this.name;
    }

    /**
     * Returns the precision for a decimal column when generating DDL.
     */
    public int getPrecision() {
        return this.precision;
    }

    public String getQualifiedName(){
        if (this.qualifiedName == null) {
            if (hasTableName()) {
                this.qualifiedName = this.table.getQualifiedName() + "." + getName();
            } else {
                this.qualifiedName = getName();
            }
        }
        return this.qualifiedName;
    }

    /**
     * Return the qualified name of the field.
     * PERF: Cache the qualified name.
     */
    public String getQualifiedNameDelimited(DatasourcePlatform platform) {
        if (hasTableName()) {
            return this.table.getQualifiedNameDelimited(platform) + "." + getNameDelimited(platform);
        } else {
            return getNameDelimited(platform);
        }
    }
    /**
     * Returns the scale for a decimal column when generating DDL.
     */
    public int getScale() {
        return this.scale;
    }

    public DatabaseTable getTable() {
        return table;
    }

    public String getTableName() {
        return getTable().getName();
    }

    public void setTableName(String tableName) {
        setTable(new DatabaseTable(tableName));
    }

    @Override
    public Class getType() {
        if ((this.type == null) && (this.typeName != null)) {
            convertClassNamesToClasses(getClass().getClassLoader());
        }
        return this.type;
    }

    public String getTypeName() {
        return typeName;
    }

    public void setTypeName(String typeName) {
        this.typeName = typeName;
    }

    /**
     * Return the JDBC type that corresponds to the field.
     * The JDBC type is normally determined from the class type,
     * but this allows it to be overridden for types that do not match directly to a Java type,
     * such as MONEY or ARRAY, STRUCT, XMLTYPE, etc.
     * This can be used for binding or stored procedure usage.
     */
    public int getSqlType() {
        return sqlType;
    }

    /**
     * Return the hashcode of the name, because it is fairly unique.
     */
    public int hashCode() {
        return getNameForComparisons().hashCode();
    }

    public boolean hasTableName() {
        if (this.table == null) {
            return false;
        }
        if (this.table.getName() == null) {
            return false;
        }
        return !(this.table.getName().equals(""));
    }

    /**
     *  PUBLIC:
     *  Return if this is an ObjectRelationalDatabaseField.
     */
    public boolean isObjectRelationalDatabaseField(){
        return false;
    }

    /**
     * Used to specify whether the column should be included in SQL UPDATE
     * statements.
     */
    public boolean isInsertable() {
        return this.isInsertable;
    }

    /**
     * Used for generating DDL. Returns true if the database column is
     * nullable.
     */
    public boolean isNullable() {
        return this.isNullable;
    }

    /**
     * Used to specify whether the column should be included in the primary
     * on the database table.
     */
    public boolean isPrimaryKey() {
        return this.isPrimaryKey;
    }

    /**
     * Return true if this database field is a translation.
     */
    public boolean isTranslated() {
        return this.isTranslated;
    }

    /**
     * Used for generating DDL. Returns true if the field is a unique key.
     */
    public boolean isUnique() {
        return this.isUnique;
    }

    /**
     * Returns true is this database field should be read only.
     */
    public boolean isReadOnly() {
        return (! isUpdatable && ! isInsertable);
    }

    public boolean keepInRow() {
        return keepInRow;
    }

    /**
     * Returns whether the column should be included in SQL INSERT
     * statements.
     */
    public boolean isUpdatable() {
        return this.isUpdatable;
    }

    /**
     * Reset the field's name and table from the qualified name.
     */
    public void resetQualifiedName(String qualifiedName) {
        setIndex(-1);
        int index = qualifiedName.lastIndexOf('.');

        if (index == -1) {
            setName(qualifiedName);
            getTable().setName("");
            getTable().setTableQualifier("");
        } else {
            setName(qualifiedName.substring(index + 1, qualifiedName.length()));
            getTable().setPossiblyQualifiedName(qualifiedName.substring(0, index));
        }
    }

    /**
     * Set the SQL fragment that is used when generating the DDL for the column.
     */
    public void setColumnDefinition(String columnDefinition) {
        this.columnDefinition = columnDefinition;
    }

    /**
     * Set the expected index that this field will occur in the result set row.
     * This is used to optimize performance of database row field lookups.
     */
    public void setIndex(int index) {
        this.index = index;
    }

    /**
     * Used to specify whether the column should be included in SQL UPDATE
     * statements.
     */
    public void setInsertable(boolean isInsertable) {
        this.isInsertable = isInsertable;
    }

    public void setKeepInRow(boolean keepInRow) {
        this.keepInRow = keepInRow;
    }

    /**
     * Set the isTranslated flag.
     */
    public void setIsTranslated(boolean isTranslated) {
        this.isTranslated = isTranslated;
    }

    /**
     * Used to specify the column length when generating DDL.
     */
    public void setLength(int length) {
        this.length = length;
    }

    /**
     * Set the unqualified name of the field.
     */
    @Override
    public void setName(String name) {
        setName(name, null, null);
    }

    /**
     * Set the unqualified name of the field.
     *
     * If the name contains database delimiters, they will be stripped and a flag will be set to have them
     * added when the DatabaseField is written to SQL
     */
    public void setName(String name, DatasourcePlatform platform){
        setName(name, platform.getStartDelimiter(), platform.getEndDelimiter());
    }

    /**
     * Set the unqualified name of the field.
     *
     * If the name contains database delimiters, they will be stripped and a flag will be set to have them
     * added when the DatabaseField is written to SQL
     */
    public void setName(String name, String startDelimiter, String endDelimiter) {
        if ((startDelimiter != null) && (endDelimiter != null) && !startDelimiter.equals("")&& !endDelimiter.equals("") && name.startsWith(startDelimiter) && name.endsWith(endDelimiter)){
            this.name = name.substring(startDelimiter.length(), name.length() - endDelimiter.length());
            this.useDelimiters = true;
        } else {
            this.name = name;
        }
        this.nameForComparisons = null;
        this.qualifiedName = null;
    }

    /**
     * Used for generating DDL. Set to true if the database column is
     * nullable.
     */
    public void setNullable(boolean isNullable) {
        this.isNullable = isNullable;
    }

    /**
     * Used to specify the precision for a decimal column when generating DDL.
     */
    public void setPrecision(int precision) {
        this.precision = precision;
    }

    /**
     * Used to specify whether the column should be included in primary key
     * on the database table.
     */
    public void setPrimaryKey(boolean isPrimaryKey) {
        this.isPrimaryKey = isPrimaryKey;
    }

    /**
     * Used to specify the scale for a decimal column when generating DDL.
     */
    public void setScale(int scale) {
        this.scale = scale;
    }

    /**
     * Set the JDBC type that corresponds to the field.
     * The JDBC type is normally determined from the class type,
     * but this allows it to be overridden for types that do not match directly
     * to a Java type, such as MONEY or ARRAY, STRUCT, XMLTYPE, etc.
     * This can be used for binding or stored procedure usage.
     */
    public void setSqlType(int sqlType) {
        this.sqlType = sqlType;
    }

    /**
     * Set the table for the field.
     */
    public void setTable(DatabaseTable table) {
        this.table = table;
        this.qualifiedName = null;
    }

    /**
     * Set the Java class type that corresponds to the field.
     * The JDBC type is determined from the class type,
     * this is used to optimize performance, and for binding.
     */
    @Override
    public void setType(Class type) {
        this.type = type;
        if (this.type != null && typeName == null) {
            typeName = this.type.getName();
        }
    }

    /**
     * Used for generating DDL. Set to true if the field is a unique key.
     */
    public void setUnique(boolean isUnique) {
        this.isUnique = isUnique;
    }

    /**
     * Used to specify whether the column should be included in SQL INSERT
     * statements.
     */
    public void setUpdatable(boolean isUpdatable) {
        this.isUpdatable = isUpdatable;
    }

    public String toString() {
        return this.getQualifiedName();
    }


    public void setUseDelimiters(boolean useDelimiters) {
        this.useDelimiters = useDelimiters;
    }

    public boolean shouldUseDelimiters() {
        return this.useDelimiters;
    }

    /**
     * INTERNAL:
     * Sets the useUpperCaseForComparisons flag which is used to force using the uppercase version of the field's
     * name to determine field equality and its hashcode, but will still use the original name when writing/printing
     * operations.  If this isn't a change, it is ignored, otherwise it sets the nameForComparisons to null.
     */
    public void useUpperCaseForComparisons(boolean useUpperCaseForComparisons){
        if (this.useUpperCaseForComparisons != useUpperCaseForComparisons){
            this.useUpperCaseForComparisons = useUpperCaseForComparisons;
            this.setNameForComparisons(null);
        }
    }

    public boolean getUseUpperCaseForComparisons(){
        return this.useUpperCaseForComparisons;
    }
    /**
     * INTERNAL:
     * sets the string to be used for equality checking and determining the hashcode of this field.
     * This will overwrite the useUpperCaseForEquality setting with the passed in string.
     */
    public void setNameForComparisons(String name){
        this.nameForComparisons = name;
    }

    public boolean isCreatable() {
        return isCreatable;
    }

    public void setCreatable(boolean isCreatable) {
        this.isCreatable = isCreatable;
    }

    /**
     * INTERNAL:
     * gets the string used for comparisons and in determining the hashcode.
     */
    public String getNameForComparisons(){
        if (this.nameForComparisons == null) {
            if ((!this.useUpperCaseForComparisons) || (this.name == null)) {
                this.nameForComparisons = this.name;
            } else {
                this.nameForComparisons = this.name.toUpperCase();
            }
        }
        return this.nameForComparisons;
    }
}
