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

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Iterator;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.InheritancePolicy;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.internal.expressions.SQLSelectStatement;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.queries.ReadObjectQuery;
import org.eclipse.persistence.queries.DataReadQuery;
import org.eclipse.persistence.sessions.DatabaseRecord;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.ObjectChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;

/**
 * INTERNAL:
 */
public class CascadeLockingPolicy {
    protected Class m_parentClass;
    protected ReadObjectQuery m_query;
    protected ClassDescriptor m_descriptor;
    protected ClassDescriptor m_parentDescriptor;
    protected Map<DatabaseField, DatabaseField> m_queryKeyFields;
    protected Map<DatabaseField, DatabaseField> m_mappedQueryKeyFields;
    protected Map<DatabaseField, DatabaseField> m_unmappedQueryKeyFields;
    protected DatabaseMapping m_parentMapping;
    protected boolean m_lookForParentMapping;
    protected boolean m_shouldHandleUnmappedFields;
    protected boolean m_hasCheckedForUnmappedFields;
    protected DataReadQuery m_unmappedFieldsQuery;

    /**
     * INTERNAL:
     */
    public CascadeLockingPolicy(ClassDescriptor parentDescriptor, ClassDescriptor descriptor) {
        m_descriptor = descriptor;
        m_parentDescriptor = parentDescriptor;
        m_parentClass = m_parentDescriptor.getJavaClass();
    }

    /**
     * INTERNAL:
     */
    protected ReadObjectQuery getQuery() {
        if (m_query == null) {
            m_query = new ReadObjectQuery(m_parentClass);

            Expression selectionCriteria = null;
            Iterator keys = m_queryKeyFields.keySet().iterator();
            ExpressionBuilder builder = new ExpressionBuilder();

            while (keys.hasNext()) {
                String keyField = ((DatabaseField) keys.next()).getQualifiedName();

                if (selectionCriteria == null) {
                    selectionCriteria = builder.getField(keyField).equal(builder.getParameter(keyField));
                } else {
                    selectionCriteria.and(builder.getField(keyField).equal(builder.getParameter(keyField)));
                }

                m_query.addArgument(keyField);
            }

            m_query.setSelectionCriteria(selectionCriteria);
            m_query.setShouldUseWrapperPolicy(false);
        }

        return m_query;
    }

    /**
     * INTERNAL:
     */
     protected DatabaseMapping getParentMapping() {
        // If the query is null, then we have not been initialized. Try to
        // look up a parent mapping first if we have lookup fields. For a
        // 1-M we can not perform the getMappingForField until the fields
        // have been initialized.
        // If the parent mapping is not found, a query will be initialized
        // and the following lookup will no longer hit.
        if (m_parentMapping == null && m_lookForParentMapping && m_query == null) {
            Iterator<DatabaseField> itFields = m_queryKeyFields.values().iterator();
            while(itFields.hasNext()) {
                DatabaseMapping mapping = m_descriptor.getObjectBuilder().getMappingForField(itFields.next());

                if(mapping == null) {
                    // at least one field is not mapped therefore no parent mapping exists.
                    m_parentMapping = null;
                    break;
                } else if(mapping.isObjectReferenceMapping()) {
                    if(m_parentMapping == null) {
                        m_parentMapping = mapping;
                    } else {
                        if(m_parentMapping != mapping) {
                            // there's more than one mapping therefore no parent mapping exists.
                            m_parentMapping = null;
                            break;
                        }
                    }
                }
            }
        }

        return m_parentMapping;
     }

     /**
      * Get the descriptor that really represents this object
      * In the case of inheritance, the object may represent a subclass of class the descriptor
      * represents.
      *
      * If there is no InheritancePolicy, we return our parentDescriptor
      * If there is inheritance we will search for a descriptor that represents parentObj and
      * return that descriptor
      * @param parentObj
      * @return
      */
     protected ClassDescriptor getParentDescriptorFromInheritancePolicy(Object parentObj){
         ClassDescriptor realParentDescriptor = m_parentDescriptor;
         if (realParentDescriptor.hasInheritance()){
             InheritancePolicy inheritancePolicy = realParentDescriptor.getInheritancePolicy();
             ClassDescriptor childDescriptor = inheritancePolicy.getDescriptor(parentObj.getClass());
             if (childDescriptor != null){
                 realParentDescriptor = childDescriptor;
             }
         }
         return realParentDescriptor;
     }

    /**
     * INTERNAL:
     */
     protected AbstractRecord getMappedTranslationRow(Object changedObj, UnitOfWorkImpl uow) {
         AbstractRecord translationRow = new DatabaseRecord();
         Iterator<Map.Entry<DatabaseField, DatabaseField>> it = m_mappedQueryKeyFields.entrySet().iterator();
         while(it.hasNext()) {
             Map.Entry<DatabaseField, DatabaseField> entry = it.next();
             Object value = m_descriptor.getObjectBuilder().extractValueFromObjectForField(changedObj, entry.getValue(), uow);
             translationRow.add(entry.getKey(), value);
         }
         return translationRow;
     }

     /**
      * INTERNAL:
      */
      protected AbstractRecord getUnmappedTranslationRow(Object changedObj, UnitOfWorkImpl uow) {
         AbstractRecord unmappedFieldsQueryTranslationRow = new DatabaseRecord();
         Iterator<DatabaseField> itPrimaryKey = m_descriptor.getPrimaryKeyFields().iterator();
         while (itPrimaryKey.hasNext()) {
             DatabaseField primaryKey = itPrimaryKey.next();
             Object value = m_descriptor.getObjectBuilder().extractValueFromObjectForField(changedObj, primaryKey, uow);
             unmappedFieldsQueryTranslationRow.add(primaryKey, value);
         }
         List result = (List)uow.executeQuery(m_unmappedFieldsQuery, unmappedFieldsQueryTranslationRow);
         if(result == null || result.isEmpty()) {
             // the object is not in the db
             return null;
         }

         AbstractRecord unmappedValues = (AbstractRecord)result.get(0);

         AbstractRecord translationRow = new DatabaseRecord();
         Iterator<Map.Entry<DatabaseField, DatabaseField>> it = m_unmappedQueryKeyFields.entrySet().iterator();
         while(it.hasNext()) {
             Map.Entry<DatabaseField, DatabaseField> entry = it.next();
             Object value = unmappedValues.get(entry.getValue());
             translationRow.add(entry.getKey(), value);
         }
         return translationRow;
      }

     /**
      * INTERNAL:
      * Identify mapped and not mapped fields (should be done once).
      * The result - either two non-empty Maps m_unmappedQueryKeyFields and m_mappedQueryKeyFields,
      * or m_unmappedQueryKeyFields == null and m_mappedQueryKeyFields == m_queryKeyFields.
      */
     public void initUnmappedFields(UnitOfWorkImpl uow) {
         if(!m_hasCheckedForUnmappedFields) {
             m_mappedQueryKeyFields = new HashMap<DatabaseField, DatabaseField>();
             m_unmappedQueryKeyFields = new HashMap<DatabaseField, DatabaseField>();
             Iterator<Map.Entry<DatabaseField, DatabaseField>> it = m_queryKeyFields.entrySet().iterator();
             while(it.hasNext()) {
                 Map.Entry<DatabaseField, DatabaseField> entry = it.next();
                 if(m_descriptor.getObjectBuilder().getMappingForField(entry.getValue()) == null) {
                     m_unmappedQueryKeyFields.put(entry.getKey(), entry.getValue());
                 } else {
                     m_mappedQueryKeyFields.put(entry.getKey(), entry.getValue());
                 }
             }
             if(m_unmappedQueryKeyFields.isEmpty()) {
                 m_unmappedQueryKeyFields = null;
                 m_mappedQueryKeyFields = m_queryKeyFields;
             }
             initUnmappedFieldsQuery(uow);
             m_hasCheckedForUnmappedFields = true;
         }
     }

     /**
      * INTERNAL:
      * This method called in case there are m_unmappedQueryKeyFields.
      * It creates a query that would fetch the values for this fields from the db.
      */
     public void initUnmappedFieldsQuery(UnitOfWorkImpl uow) {
         if(m_unmappedFieldsQuery == null) {
             m_unmappedFieldsQuery = new DataReadQuery();

             Expression whereClause = null;
             Expression builder = new ExpressionBuilder();
             Iterator<DatabaseField> itPrimaryKey = m_descriptor.getPrimaryKeyFields().iterator();
             while (itPrimaryKey.hasNext()) {
                 DatabaseField primaryKey = itPrimaryKey.next();
                 Expression expression = builder.getField(primaryKey).equal(builder.getParameter(primaryKey));
                 whereClause = expression.and(whereClause);
                 m_unmappedFieldsQuery.addArgument(primaryKey.getQualifiedName());
             }

             SQLSelectStatement statement = new SQLSelectStatement();
             Iterator<DatabaseField> itUnmappedFields = m_unmappedQueryKeyFields.values().iterator();
             while (itUnmappedFields.hasNext()) {
                 DatabaseField field = itUnmappedFields.next();
                 statement.addField(field);
             }

             statement.setWhereClause(whereClause);
             statement.normalize(uow.getParent(), m_descriptor);
             m_unmappedFieldsQuery.setSQLStatement(statement);
             m_unmappedFieldsQuery.setSessionName(m_descriptor.getSessionName());
         }
     }

     /**
     * INTERNAL:
     */
    public void lockNotifyParent(Object obj, UnitOfWorkChangeSet changeSet, UnitOfWorkImpl uow) {
        Object parentObj = null;

        // Check for a parent object via the parent (back pointer) mapping first.
        DatabaseMapping parentMapping = getParentMapping();
        if (parentMapping != null && parentMapping.isObjectReferenceMapping()) {
            parentObj = parentMapping.getRealAttributeValueFromObject(obj, uow);
        }

        // If the parent object is still null at this point, try a query.
        // check out why no query keys.
        if (parentObj == null) {
            AbstractRecord translationRow;
            if(m_shouldHandleUnmappedFields) {
                // should look for unmapped fields.
                initUnmappedFields(uow);
                if(m_unmappedQueryKeyFields != null) {
                    // there are some unmapped fields - fetch the values for the from the db.
                    AbstractRecord unmappedTranslationRow = getUnmappedTranslationRow(obj, uow);
                    if(unmappedTranslationRow == null) {
                        // the object is not yet in the db
                        return;
                    } else {
                        // merge mapped and unmapped values into the single translation row.
                        translationRow = getMappedTranslationRow(obj, uow);
                        translationRow.putAll(unmappedTranslationRow);
                    }
                } else {
                    // no unmapped fields
                    translationRow = getMappedTranslationRow(obj, uow);
                }
            } else {
                // no unmapped fields
                translationRow = getMappedTranslationRow(obj, uow);
            }
            // the query is set to return an unwrapped object.
            parentObj = uow.executeQuery(getQuery(), translationRow);

        } else {
            // make sure the parent object is unwrapped.
            if (m_parentDescriptor.hasWrapperPolicy()) {
                m_parentDescriptor.getWrapperPolicy().unwrapObject(parentObj, uow);
            }
        }
        ClassDescriptor realParentDescriptor = m_parentDescriptor;
        if (parentObj != null){
            realParentDescriptor = getParentDescriptorFromInheritancePolicy(parentObj);
        }

        // If we have a parent object, force update the version field if one
        // exists, and keep firing the notification up the chain.
        // Otherwise, do nothing.
        if (parentObj != null) {
            // Need to check if we are a non cascade locking node within a
            // cascade locking policy chain.
            if (realParentDescriptor.usesOptimisticLocking() && realParentDescriptor.getOptimisticLockingPolicy().isCascaded()) {
                ObjectChangeSet ocs = realParentDescriptor.getObjectBuilder().createObjectChangeSet(parentObj, changeSet, uow);

                if (!ocs.hasForcedChangesFromCascadeLocking()) {
                    ocs.setHasForcedChangesFromCascadeLocking(true);
                    changeSet.addObjectChangeSet(ocs, uow, true);
                }
            }

            // Keep sending the notification up the chain ...
            if (realParentDescriptor.hasCascadeLockingPolicies()) {
                for (CascadeLockingPolicy policy : realParentDescriptor.getCascadeLockingPolicies()) {
                    policy.lockNotifyParent(parentObj, changeSet, uow);
                }
            }
        }
    }

    /**
     * INTERNAL:
     */
    public void setQueryKeyFields(Map<DatabaseField, DatabaseField> queryKeyFields) {
        setQueryKeyFields(queryKeyFields, true);
    }

    /**
     * INTERNAL:
     */
    public void setQueryKeyFields(Map<DatabaseField, DatabaseField> queryKeyFields, boolean lookForParentMapping) {
        m_queryKeyFields = queryKeyFields;
        m_mappedQueryKeyFields = m_queryKeyFields;
        this.m_lookForParentMapping = lookForParentMapping;
    }

    /**
     * INTERNAL:
     * Indicates whether to expect unmapped fields.
     * That should be set to true for UnidirectionalOneToManyMapping.
     */
    public void setShouldHandleUnmappedFields(boolean shouldHandleUnmappedFields) {
        m_shouldHandleUnmappedFields = shouldHandleUnmappedFields;
    }

    /**
     * INTERNAL:
     */
    public boolean shouldHandleUnmappedFields() {
        return m_shouldHandleUnmappedFields;
    }
}
