/*
 * 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
package org.eclipse.persistence.internal.expressions;

import java.util.*;
import java.io.*;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.mappings.*;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.expressions.*;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;

/**
 * Field expressions represent a field of a table.
 * Their base is either a table or object expression.
 * This is used for mapping queries and to allow queries on non-mapped field or tables.
 */
public class FieldExpression extends DataExpression {
    protected DatabaseField field;
    protected transient DatabaseField aliasedField;

    /**
     * FieldExpression constructor comment.
     */
    public FieldExpression() {
        super();
    }

    /**
     * FieldExpression constructor comment.
     */
    public FieldExpression(DatabaseField newField) {
        super();
        field = newField;
    }

    /**
     * FieldExpression constructor comment.
     */
    public FieldExpression(DatabaseField newField, Expression myBase) {
        super();
        field = newField;
        baseExpression = myBase;
    }

    /**
     * INTERNAL:
     * Return if the expression is equal to the other.
     * This is used to allow dynamic expression's SQL to be cached.
     * This must be over written by each subclass.
     */
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (!super.equals(object)) {
            return false;
        }
        FieldExpression expression = (FieldExpression)object;
        return ((getField() == expression.getField()) || ((getField() != null) && getField().equals(expression.getField())));
    }

    /**
     * INTERNAL:
     * Compute a consistent hash-code for the expression.
     * This is used to allow dynamic expression's SQL to be cached.
     */
    public int computeHashCode() {
        int hashCode = super.computeHashCode();
        if (getField() != null) {
            hashCode = hashCode + getField().hashCode();
        }
        return hashCode;
    }

    /**
     * INTERNAL:
     */
    public void clearAliases() {
        super.clearAliases();
        aliasedField = null;
    }

    /**
     * INTERNAL:
     * Used for debug printing.
     */
    public String descriptionOfNodeType() {
        return "Field";
    }

    /**
     * INTERNAL:
     * Return the field appropriately aliased
     */
    public DatabaseField getAliasedField() {
        if (aliasedField == null) {
            initializeAliasedField();
        }
        return aliasedField;

    }

    /**
     * Return the alias for our table
     */
    private DatabaseTable getAliasedTable() {
        DataExpression base = (DataExpression)getBaseExpression();
        DatabaseField field = getField();
        if (!field.hasTableName()) {
            if (base.getDescriptor() != null){
                field = base.getDescriptor().buildField(field);
            }
        }

        DatabaseTable alias = base.aliasForTable(field.getTable());
        if (alias == null) {
            return field.getTable();
        } else {
            return alias;
        }
    }

    /**
     * INTERNAL:
     * If there are any fields associated with this expression, return them
     */
    public DatabaseField getClonedField() {
        return getField().clone();
    }

    /**
     * INTERNAL:
     * If there are any fields associated with this expression, return them
     */
    public Vector getClonedFields() {
        Vector result = new Vector(1);
        result.addElement(getField().clone());
        return result;
    }

    /**
     * INTERNAL:
     */
    public DatabaseField getField() {
        return field;
    }

    /**
     * INTERNAL:
     * Return all the fields
     */
    public Vector getFields() {
        Vector result = new Vector(1);
        result.addElement(getField());
        return result;
    }

    /**
     * INTERNAL:
     * Provide for conversion of the passed object's value, based on the referenced field value.
     */
    @Override
    public Object getFieldValue(Object value, AbstractSession session) {
        // Bug 418705
        if (getField() != null && value != null && (value.getClass() != getField().getType()) && !(value instanceof Collection) && !(value instanceof Enum)) {
            try {
                return session.getDatasourcePlatform().convertObject(value, getField().getType());
            } catch (ConversionException c) {}
        }
        return super.getFieldValue(value, session);
    }

    /**
     * INTERNAL:
     * Alias the database field for our current environment
     */
    private void initializeAliasedField() {
        DatabaseField tempField = getField().clone();
        DatabaseTable aliasedTable = getAliasedTable();

        //  Put in a special check here so that if the aliasing does nothing we don't cache the
        // result because it's invalid. This saves us from caching premature data if e.g. debugging
        // causes us to print too early"
        //    if (aliasedTable.equals(getField().getTable())) {
        //        return;
        //    } else {
        aliasedField = tempField;
        aliasedField.setTable(aliasedTable);
        //    }
    }

    /**
     * INTERNAL:
     */
    public boolean isAttribute() {
        return true;
    }

    public boolean isFieldExpression() {
        return true;
    }

    /**
     * INTERNAL:
     * Normalize the expression into a printable structure.
     */
    public Expression normalize(ExpressionNormalizer normalizer) {
        if (this.hasBeenNormalized) {
            return this;
        }
        Expression expression = super.normalize(normalizer);
        // to support custom types, print expressions derived from field expressions, table expressions and direct query keys with their aliases
        if (getBaseExpression() != null && getBaseExpression().isFieldExpression() || getBaseExpression().isTableExpression() ||
               (getBaseExpression().isQueryKeyExpression() && ((QueryKeyExpression)getBaseExpression()).isAttribute())){
            getBuilder().getStatement().setRequiresAliases(true);
        }
        return expression;
    }

    /**
     * INTERNAL:
     * Print SQL onto the stream, using the ExpressionPrinter for context
     */
    public void printSQL(ExpressionSQLPrinter printer) {
        // to support custom types, print expressions derived from field expressions and direct query keys with their aliases
        // Note: This is also necessary for TableExpressions, but they are taken care of by their associated FieldExpression.
        if (getBaseExpression() != null &&  getBaseExpression().isFieldExpression() ||
                (getBaseExpression().isQueryKeyExpression() && ((QueryKeyExpression)getBaseExpression()).isAttribute())){
            getBaseExpression().printSQL(printer);
            printer.printString(".");
        }
        printer.printField(getAliasedField());
    }

    /**
     * INTERNAL:
     * Print java for project class generation
     */
    public void printJava(ExpressionJavaPrinter printer) {
        getBaseExpression().printJava(printer);
        printer.printString(".getField(\"" + getField().getQualifiedName() + "\")");
    }

    /**
     * INTERNAL:
     * This expression is built on a different base than the one we want. Rebuild it and
     * return the root of the new tree
     */
    public Expression rebuildOn(Expression newBase) {
        DatabaseField field = getField();
        ClassDescriptor descriptor = null;
        // Check for possible table per class rebuild and translate fields to correct table.
        // TODO: JPA also allows for field to be renamed in subclasses, this needs to account for that (never has...).
        if (getBaseExpression().isExpressionBuilder() && newBase.isObjectExpression()) {
            if (((ObjectExpression)newBase).getSession() != null) {
                descriptor = ((ObjectExpression)newBase).getDescriptor();
            }
            if ((descriptor != null) && descriptor.hasTablePerClassPolicy()) {
                field = field.clone();
                field.setTable(descriptor.getDefaultTable());
            }
        }
        FieldExpression expression = new FieldExpression(field, getBaseExpression().rebuildOn(newBase));
        expression.setSelectIfOrderedBy(selectIfOrderedBy());
        return expression;
    }

    /**
     * INTERNAL:
     * Set the field in the mapping.
     */
    public void setField(DatabaseField newField) {
        field = newField;
    }

    /**
     * INTERNAL:
     * Rebuild myself against the base, with the values of parameters supplied by the context
     * expression. This is used for transforming a standalone expression (e.g. the join criteria of a mapping)
     * into part of some larger expression. You normally would not call this directly, instead calling twist
     * See the comment there for more details"
     */
    @Override
    public Expression twistedForBaseAndContext(Expression newBase, Expression context, Expression oldBase) {
        Expression twistedBase = this.baseExpression.twistedForBaseAndContext(newBase, context, oldBase);
        DatabaseField field = getField();
        ClassDescriptor descriptor = null;
        // Check for possible table per class rebuild and translate fields to correct table.
        // TODO: JPA also allows for field to be renamed in subclasses, this needs to account for that (never has...).
        if (this.baseExpression.isExpressionBuilder() && newBase.isObjectExpression()
                && ((this.baseExpression == oldBase) || (oldBase == null))) {
            if (((ObjectExpression)newBase).getSession() != null) {
                descriptor = ((ObjectExpression)newBase).getDescriptor();
            }
            if ((descriptor != null) && descriptor.hasTablePerClassPolicy()) {
                field = field.clone();
                field.setTable(descriptor.getDefaultTable());
            }
        }
        return twistedBase.getField(field);
    }

    /**
     * Do any required validation for this node. Throw an exception if it's incorrect.
     */
    public void validateNode() {
        DataExpression base = (DataExpression)getBaseExpression();
        if (getField().getTable().hasName()) {
            List<DatabaseTable> tables = base.getOwnedTables();
            if ((tables != null) && (!tables.contains((getField().getTable())))) {
                throw QueryException.invalidTableForFieldInExpression(getField());
            }
        }
    }

    /**
     * INTERNAL:
     * Return the value for in memory comparison.
     * This is only valid for valueable expressions.
     */
    public Object valueFromObject(Object object, AbstractSession session, AbstractRecord translationRow, int valueHolderPolicy, boolean isObjectUnregistered) {
        // Joins not supported.
        if (!getBaseExpression().isExpressionBuilder()) {
            throw QueryException.cannotConformExpression();
        }

        // For bug 2780817 get the mapping directly from the object.  In EJB 2.0
        // inheritance, each child must override mappings defined in an abstract
        // class with its own.
        DatabaseMapping mapping = session.getDescriptor(object.getClass()).getObjectBuilder().getMappingForField(getField());
        if (mapping == null) {
            throw QueryException.cannotConformExpression();
        }

        return mapping.valueFromObject(object, getField(), session);
    }

    /**
     * INTERNAL:
     * Used to print a debug form of the expression tree.
     */
    public void writeDescriptionOn(BufferedWriter writer) throws IOException {
        writer.write(getField().toString());
    }

    /**
     * INTERNAL: called from SQLSelectStatement.writeFieldsFromExpression(...)
     */
    @Override
    public void writeFields(ExpressionSQLPrinter printer, Vector newFields, SQLSelectStatement statement) {
        DatabaseField field = getField();

        if (field != null) {
            newFields.addElement(field);
            writeField(printer, field, statement);
        }
    }

    protected void writeField(ExpressionSQLPrinter printer, DatabaseField field, SQLSelectStatement statement) {
        if (this.field == field){
            //print ", " before each selected field except the first one
            if (printer.isFirstElementPrinted()) {
                printer.printString(", ");
            } else {
                printer.setIsFirstElementPrinted(true);
            }
            printSQL(printer);
        } else {
            super.writeField(printer, field, statement);
        }

    }

    /**
     * INTERNAL:
     * writes the field for fine-grained pessimistic locking.
     */
    protected void writeForUpdateOf(ExpressionSQLPrinter printer, SQLSelectStatement statement) {
        if (printer.getPlatform().shouldPrintAliasForUpdate()) {
            writeAlias(printer, field, statement);
        } else {
            writeField(printer, field, statement);
        }
    }
}
