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

import java.util.*;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.internal.sessions.*;
import org.eclipse.persistence.internal.databaseaccess.*;
import org.eclipse.persistence.internal.expressions.ObjectExpression;
import org.eclipse.persistence.mappings.*;
import org.eclipse.persistence.queries.*;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.expressions.*;
import org.eclipse.persistence.internal.queries.*;

/**
 * <p><b>Purpose:</b>
 * Nested tables are similar to <code>VARRAYs</code> except internally they store their information in a separate table
 * from their parent structure's table. The advantage of nested tables is that they support querying and
 * joining much better than varrays that are inlined into the parent table. A nested table is typically
 * used to represent a one-to-many or many-to-many relationship of references to another independent
 * structure. TopLink supports storing a nested table of values into a single field.
 *
 * <p>NOTE: Only Oracle8i supports nested tables type.
 *
 * @since TOPLink/Java 2.5
 */
public class NestedTableMapping extends CollectionMapping {
    protected DatabaseMapping nestedMapping;

    /** A ref is always stored in a single field. */
    protected DatabaseField field;

    /** Arrays require a structure name, this is the ADT defined for the VARRAY. */
    protected String structureName;

    /**
     * PUBLIC:
     * Default constructor.
     */
    public NestedTableMapping() {
        super();
    }

    /**
     * INTERNAL:
     * In case Query By Example is used, this method builds and returns an expression that
     * corresponds to a single attribute and it's value.
     */
    public Expression buildExpression(Object queryObject, QueryByExamplePolicy policy, Expression expressionBuilder, Map processedObjects, AbstractSession session) {
        if (policy.shouldValidateExample()){
            throw QueryException.unsupportedMappingQueryByExample(queryObject.getClass().getName(), this);
        }
        return null;
    }

    /**
     * INTERNAL:
     * The mapping clones itself to create deep copy
     */
    public Object clone() {
        NestedTableMapping clone = (NestedTableMapping)super.clone();
        return clone;
    }

    /**
     * Returns all the aggregate fields.
     */
    protected Vector collectFields() {
        Vector fields = new Vector(1);
        fields.addElement(getField());
        return fields;
    }

    /**
     * INTERNAL:
     * Returns the field which this mapping represents.
     */
    public DatabaseField getField() {
        return field;
    }

    /**
     * PUBLIC:
     * Return the name of the field this mapping represents.
     */
    public String getFieldName() {
        return getField().getName();
    }

    /**
     * INTERNAL:
     * Join criteria is created to read target records (nested table) from the table.
     */
    @Override
    public Expression getJoinCriteria(ObjectExpression context, Expression base) {
        return context.ref().equal(base.value());
    }

    /**
     * PUBLIC:
     * Return the structure name of the nestedTable.
     * This is the name of the user defined data type as defined on the database.
     */
    public String getStructureName() {
        return structureName;
    }

    /**
     * INTERNAL:
     * The returns if the mapping has any constraint dependencies, such as foreign keys and join tables.
     */
    @Override
    public boolean hasConstraintDependency() {
        return true;
    }

    /**
     * INTERNAL:
     * Initialize the mapping.
     */
    @Override
    public void initialize(AbstractSession session) throws DescriptorException {
        super.initialize(session);
        if (getField() == null) {
            throw DescriptorException.fieldNameNotSetInMapping(this);
        }

        // For bug 2730536 convert the field to be an ObjectRelationalDatabaseField.
        ObjectRelationalDatabaseField field = (ObjectRelationalDatabaseField)getField();
        field.setSqlType(java.sql.Types.ARRAY);
        field.setSqlTypeName(getStructureName());

        setField(getDescriptor().buildField(getField()));
    }

    /**
     * INTERNAL:
     * Selection criteria is created to read target records (nested table) from the table.
     */
    protected void initializeSelectionCriteria(AbstractSession session) {
        Expression exp1;
        Expression exp2;
        ExpressionBuilder builder = new ExpressionBuilder();
        Expression queryKey = builder.getManualQueryKey(getAttributeName(), getDescriptor());

        exp1 = builder.ref().equal(queryKey.get(getAttributeName()).value());
        exp2 = getDescriptor().getObjectBuilder().getPrimaryKeyExpression().rebuildOn(queryKey);

        setSelectionCriteria(exp1.and(exp2));
    }

    /**
     * INTERNAL:
     */
    @Override
    public boolean isNestedTableMapping() {
        return true;
    }

    /**
     * INTERNAL:
     * Post Initialize the mapping.
     */
    @Override
    public void postInitialize(AbstractSession session) throws DescriptorException {
        initializeSelectionCriteria(session);
    }

    /**
     * INTERNAL:
     * Delete privately owned parts
     */
    @Override
    public void preDelete(DeleteObjectQuery query) throws DatabaseException, OptimisticLockException {
        if (!shouldObjectModifyCascadeToParts(query)) {
            return;
        }

        Object objects = getRealCollectionAttributeValueFromObject(query.getObject(), query.getSession());
        ContainerPolicy containerPolicy = getContainerPolicy();

        Object objectsIterator = containerPolicy.iteratorFor(objects);

        // delete parts one by one
        while (containerPolicy.hasNext(objectsIterator)) {
            DeleteObjectQuery deleteQuery = new DeleteObjectQuery();
            deleteQuery.setIsExecutionClone(true);
            deleteQuery.setObject(containerPolicy.next(objectsIterator, query.getSession()));
            deleteQuery.setCascadePolicy(query.getCascadePolicy());
            query.getSession().executeQuery(deleteQuery);
        }
        if (!query.getSession().isUnitOfWork()) {
            // This deletes any objects on the database, as the collection in memory may has been changed.
            // This is not required for unit of work, as the update would have already deleted these objects,
            // and the backup copy will include the same objects causing double deletes.
            verifyDeleteForUpdate(query);
        }
    }

    /**
     * INTERNAL:
     * Insert privately owned parts
     */
    @Override
    public void preInsert(WriteObjectQuery query) throws DatabaseException, OptimisticLockException {
        if (!shouldObjectModifyCascadeToParts(query)) {
            return;
        }

        Object objects = getRealCollectionAttributeValueFromObject(query.getObject(), query.getSession());

        // insert each object one by one
        ContainerPolicy cp = getContainerPolicy();
        for (Object iter = cp.iteratorFor(objects); cp.hasNext(iter);) {
            Object object = cp.next(iter, query.getSession());
            if (isPrivateOwned()) {
                InsertObjectQuery insertQuery = new InsertObjectQuery();
                insertQuery.setIsExecutionClone(true);
                insertQuery.setObject(object);
                insertQuery.setCascadePolicy(query.getCascadePolicy());
                query.getSession().executeQuery(insertQuery);
            } else {
                // Will happen in unit of work or cascaded query.
                // This is done only for persistence by reachablility and it not require if the targets are in the queue anyway
                // Avoid cycles by checking commit manager, this is allowed because there is no dependency.
                if (!query.getSession().getCommitManager().isCommitInPreModify(object)) {
                    ObjectChangeSet changeSet = null;
                    UnitOfWorkChangeSet uowChangeSet = null;
                    if (query.getSession().isUnitOfWork() && (((UnitOfWorkImpl)query.getSession()).getUnitOfWorkChangeSet() != null)) {
                        uowChangeSet = (UnitOfWorkChangeSet)((UnitOfWorkImpl)query.getSession()).getUnitOfWorkChangeSet();
                        changeSet = (ObjectChangeSet)uowChangeSet.getObjectChangeSetForClone(object);
                    }
                    WriteObjectQuery writeQuery = new WriteObjectQuery();
                    writeQuery.setIsExecutionClone(true);
                    writeQuery.setObject(object);
                    writeQuery.setObjectChangeSet(changeSet);
                    writeQuery.setCascadePolicy(query.getCascadePolicy());
                    query.getSession().executeQuery(writeQuery);
                }
            }
        }
    }

    /**
     * INTERNAL:
     * Update the privately owned parts
     */
    @Override
    public void preUpdate(WriteObjectQuery query) throws DatabaseException, OptimisticLockException {
        if (!shouldObjectModifyCascadeToParts(query)) {
            return;
        }

        // If objects are not instantiated that means they are not changed.
        if (!isAttributeValueInstantiatedOrChanged(query.getObject())) {
            return;
        }

        if (query.getObjectChangeSet() != null) {
            // UnitOfWork
            writeChanges(query.getObjectChangeSet(), query);
        } else {
            // OLD COMMIT
            compareObjectsAndWrite(query);
        }
    }

    /**
     * Set the field in the mapping.
     */
    protected void setField(DatabaseField theField) {
        field = theField;
    }

    /**
     * PUBLIC:
     * Set the field name in the mapping.
     */
    public void setFieldName(String FieldName) {
        setField(new ObjectRelationalDatabaseField(FieldName));
    }

    /**
     * PUBLIC:
     * Set the name of the structure.
     * This is the name of the user defined nested table data type as defined on the database.
     */
    public void setStructureName(String structureName) {
        this.structureName = structureName;
    }

    /**
     * INTERNAL:
     * Verifying deletes make sure that all the records privately owned by this mapping are
     * actually removed. If such records are found then those are all read and removed one
     * by one taking their privately owned parts into account.
     */
    protected void verifyDeleteForUpdate(DeleteObjectQuery query) throws DatabaseException, OptimisticLockException {
        Object objects = readPrivateOwnedForObject(query);

        // Delete all objects one by one.
        ContainerPolicy cp = getContainerPolicy();
        for (Object iter = cp.iteratorFor(objects); cp.hasNext(iter);) {
            query.getSession().deleteObject(cp.next(iter, query.getSession()));
        }
    }

    /**
     * INTERNAL:
     * Get a value from the object and set that in the respective field of the row.
     */
    @Override
    public void writeFromObjectIntoRow(Object object, AbstractRecord record, AbstractSession session, WriteType writeType) {
        if (isReadOnly()) {
            return;
        }

        Object values = getRealCollectionAttributeValueFromObject(object, session);
        ContainerPolicy cp = getContainerPolicy();
        Object[] fields = new Object[cp.sizeFor(values)];
        Object valuesIterator = cp.iteratorFor(values);
        for (int index = 0; index < cp.sizeFor(values); index++) {
            Object value = cp.next(valuesIterator, session);
            fields[index] = ((ObjectRelationalDataTypeDescriptor)getReferenceDescriptor()).getRef(value, session);
        }

        java.sql.Array array;
        try {
            ((DatabaseAccessor)session.getAccessor()).incrementCallCount(session);
            java.sql.Connection connection = ((DatabaseAccessor)session.getAccessor()).getConnection();
            array = session.getPlatform().createArray(getStructureName(), fields, session,connection);
        } catch (java.sql.SQLException exception) {
            throw DatabaseException.sqlException(exception, session.getAccessor(), session, false);
        } finally {
            ((DatabaseAccessor)session.getAccessor()).decrementCallCount();
        }

        record.put(getField(), array);
    }

    /**
     * INTERNAL:
     * Get a value from the object and set that in the respective field of the row.
     */
    @Override
    public void writeFromObjectIntoRowWithChangeRecord(ChangeRecord changeRecord, AbstractRecord record, AbstractSession session, WriteType writeType) {
        if (isReadOnly()) {
            return;
        }

        Object object = ((ObjectChangeSet)changeRecord.getOwner()).getUnitOfWorkClone();
        Object values = getRealAttributeValueFromObject(object, session);
        ContainerPolicy containterPolicy = getContainerPolicy();

        if (values == null) {
            values = containterPolicy.containerInstance(1);
        }

        Object[] fields = new Object[containterPolicy.sizeFor(values)];
        Object valuesIterator = containterPolicy.iteratorFor(values);
        for (int index = 0; index < containterPolicy.sizeFor(values); index++) {
            Object value = containterPolicy.next(valuesIterator, session);
            fields[index] = ((ObjectRelationalDataTypeDescriptor)getReferenceDescriptor()).getRef(value, session);
        }

        java.sql.Array array;
        try {
            ((DatabaseAccessor)session.getAccessor()).incrementCallCount(session);
            java.sql.Connection connection = ((DatabaseAccessor)session.getAccessor()).getConnection();
            array = session.getPlatform().createArray(getStructureName(), fields, session, connection);
        } catch (java.sql.SQLException exception) {
            throw DatabaseException.sqlException(exception, session.getAccessor(), session, false);
        } finally {
            ((DatabaseAccessor)session.getAccessor()).decrementCallCount();
        }

        record.put(getField(), array);
    }

    /**
     * INTERNAL:
     * This row is built for shallow insert which happens in case of bidirectional inserts.
     * The foreign keys must be set to null to avoid constraints.
     */
    @Override
    public void writeFromObjectIntoRowForShallowInsert(Object object, AbstractRecord record, AbstractSession session) {
        if (isReadOnly()) {
            return;
        }

        if (getField().isNullable()) {
            record.put(getField(), null);
        } else {
            writeFromObjectIntoRow(object, record, session, WriteType.INSERT);
        }
    }

    /**
     * INTERNAL:
     * This row is built for update after shallow insert which happens in case of bidirectional inserts.
     * It contains the foreign keys with non null values that were set to null for shallow insert.
     */
    @Override
    public void writeFromObjectIntoRowForUpdateAfterShallowInsert(Object object, AbstractRecord record, AbstractSession session, DatabaseTable table) {
        if (!getField().getTable().equals(table) || !getField().isNullable()) {
            return;
        }

        writeFromObjectIntoRow(object, record, session, WriteType.UPDATE);
    }

    /**
     * INTERNAL:
     * This row is built for shallow insert which happens in case of bidirectional inserts.
     * The foreign keys must be set to null to avoid constraints.
     */
    @Override
    public void writeFromObjectIntoRowForShallowInsertWithChangeRecord(ChangeRecord changeRecord, AbstractRecord record, AbstractSession session) {
        if (isReadOnly()) {
            return;
        }

        record.put(getField(), null);
    }

    /**
     * INTERNAL:
     * Write the entire structure into the row as a special type that prints as the constructor.
     * If any part of the structure has changed the whole thing is written.
     */
    @Override
    public void writeFromObjectIntoRowForUpdate(WriteObjectQuery writeQuery, AbstractRecord record) throws DescriptorException {
        if (!isAttributeValueInstantiatedOrChanged(writeQuery.getObject())) {
            return;
        }

        if (writeQuery.getSession().isUnitOfWork()) {
            if (compareObjects(writeQuery.getObject(), writeQuery.getBackupClone(), writeQuery.getSession())) {
                return;// Nothing has changed, no work required
            }
        }

        writeFromObjectIntoRow(writeQuery.getObject(), record, writeQuery.getSession(), WriteType.UPDATE);
    }

    /**
     * INTERNAL:
     * Write fields needed for insert into the template for with null values.
     */
    @Override
    public void writeInsertFieldsIntoRow(AbstractRecord record, AbstractSession session) {
        if (isReadOnly()) {
            return;
        }

        record.put(getField(), null);
    }
}
