/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 ****************************************************************/

package org.apache.cayenne.access;

import java.util.Map;

import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjRelationship;
import org.apache.cayenne.reflect.ArcProperty;
import org.apache.cayenne.reflect.AttributeProperty;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.cayenne.reflect.PropertyVisitor;
import org.apache.cayenne.reflect.ToManyProperty;
import org.apache.cayenne.reflect.ToOneProperty;

/**
 * DataRowUtils contains a number of static methods to work with DataRows. This is a
 * helper class for DataContext and ObjectStore.
 * 
 * @since 1.1
 */
class DataRowUtils {

    /**
     * Merges changes reflected in snapshot map to the object. Changes made to attributes
     * and to-one relationships will be merged. In case an object is already modified,
     * modified properties will not be overwritten.
     */
    static void mergeObjectWithSnapshot(
            DataContext context,
            ClassDescriptor descriptor,
            Persistent object,
            DataRow snapshot) {

        int state = object.getPersistenceState();

        if (state == PersistenceState.HOLLOW || descriptor.getEntity().isReadOnly()) {
            refreshObjectWithSnapshot(descriptor, object, snapshot, true);
        } else if (state != PersistenceState.COMMITTED) {
            forceMergeWithSnapshot(context, descriptor, object, snapshot);
        } else {
            // do not invalidate to-many relationships, since they might have just been prefetched...
            refreshObjectWithSnapshot(descriptor, object, snapshot, false);
        }
    }

    /**
     * Replaces all object attribute values with snapshot values. Sets object state to
     * COMMITTED, unless the snapshot is partial in which case the state is set to HOLLOW
     */
    static void refreshObjectWithSnapshot(
            ClassDescriptor descriptor,
            final Persistent object,
            final DataRow snapshot,
            final boolean invalidateToManyRelationships) {

        final boolean[] isPartialSnapshot = new boolean[1];

        descriptor.visitProperties(new PropertyVisitor() {

            public boolean visitAttribute(AttributeProperty property) {
                ObjAttribute attr = property.getAttribute();
                String dbAttrPath = attr.getDbAttributePath();

                Object value = snapshot.get(dbAttrPath);
                property.writePropertyDirectly(object, null, value);

                // note that a check "snaphsot.get(..) == null" would be incorrect in this
                // case, as NULL value is entirely valid; still save a map lookup by
                // checking for the null value first
                if (value == null && !snapshot.containsKey(dbAttrPath)) {
                    if(attr.isLazy()) {
                        property.writePropertyDirectly(object, null, new AttributeFault(property));
                    } else {
                        isPartialSnapshot[0] = true;
                    }
                }

                return true;
            }

            public boolean visitToMany(ToManyProperty property) {
                // "to many" relationships have no information to collect from
                // snapshot
                if (invalidateToManyRelationships) {
                    property.invalidate(object);
                }

                return true;
            }

            public boolean visitToOne(ToOneProperty property) {
                property.invalidate(object);
                return true;
            }
        });

        object.setPersistenceState(isPartialSnapshot[0]
                ? PersistenceState.HOLLOW
                : PersistenceState.COMMITTED);
    }

    static void forceMergeWithSnapshot(
            final DataContext context,
            ClassDescriptor descriptor,
            final Persistent object,
            final DataRow snapshot) {

        final ObjectDiff diff = context
                .getObjectStore()
                .getChangesByObjectId()
                .get(object.getObjectId());

        descriptor.visitProperties(new PropertyVisitor() {

            public boolean visitAttribute(AttributeProperty property) {
                String dbAttrPath = property.getAttribute().getDbAttributePath();

                // supports merging of partial snapshots...
                // check for null is cheaper than double lookup
                // for a key... so check for partial snapshot
                // only if the value is null
                Object newValue = snapshot.get(dbAttrPath);
                if (newValue != null || snapshot.containsKey(dbAttrPath)) {

                    Object curValue = property.readPropertyDirectly(object);
                    Object oldValue = diff != null ? diff.getSnapshotValue(property
                            .getName()) : null;

                    // if value not modified, update it from snapshot, otherwise leave it alone
                    if (property.equals(curValue, oldValue)
                            && !property.equals(newValue, curValue)) {
                        property.writePropertyDirectly(object, oldValue, newValue);
                    }
                }
                return true;
            }

            public boolean visitToMany(ToManyProperty property) {
                // noop - nothing to merge
                return true;
            }

            public boolean visitToOne(ToOneProperty property) {
                ObjRelationship relationship = property.getRelationship();
                if (relationship.isToPK()) {
                    // TODO: will this work for flattened, how do we save snapshots for them?

                    // if value not modified, update it from snapshot, otherwise leave it alone
                    if (!isToOneTargetModified(property, object, diff)) {

                        DbRelationship dbRelationship = relationship
                                .getDbRelationships()
                                .get(0);

                        // must check before creating ObjectId because of partial snapshots
                        if (hasFK(dbRelationship, snapshot)) {
                            ObjectId id = snapshot.createTargetObjectId(
                                    relationship.getTargetEntityName(),
                                    dbRelationship
                            );

                            if (diff == null
                                    || !diff.containsArcSnapshot(relationship.getName())
                                    || !property.equals(id, diff.getArcSnapshotValue(relationship.getName()))) {

                                if (id == null) {
                                    property.writeProperty(object, null, null);
                                } else {
                                    // we can't use 'localObject' if relationship is optional or inheritance is involved
                                    // .. must turn to fault instead
                                    if (!relationship
                                            .isSourceDefiningTargetPrecenseAndType(context.getEntityResolver())) {
                                        property.invalidate(object);
                                    } else {
                                        property.writeProperty(
                                                object,
                                                null,
                                                context.findOrCreateObject(id));
                                    }
                                }
                            }
                        }
                    }
                }
                return true;
            }
        });
    }

    static boolean hasFK(DbRelationship relationship, Map<String, Object> snapshot) {
        for (final DbJoin join : relationship.getJoins()) {
            if (snapshot.get(join.getSourceName()) == null) {
                return false;
            }
        }

        return true;
    }

    /**
     * Checks if an object has its to-one relationship target modified in memory.
     */
    static boolean isToOneTargetModified(
            ArcProperty property,
            Persistent object,
            ObjectDiff diff) {

        if (object.getPersistenceState() != PersistenceState.MODIFIED || diff == null) {
            return false;
        }

        if (property.isFault(object)) {
            return false;
        }

        Persistent toOneTarget = (Persistent) property.readPropertyDirectly(object);
        ObjectId currentId = (toOneTarget != null) ? toOneTarget.getObjectId() : null;

        // if ObjectId is temporary, target is definitely modified...
        // this would cover NEW objects (what are the other cases of temp id??)
        if (currentId != null && currentId.isTemporary()) {
            return true;
        }

        if (!diff.containsArcSnapshot(property.getName())) {
            return false;
        }

        ObjectId targetId = diff.getArcSnapshotValue(property.getName());
        return !property.equals(currentId, targetId);
    }

    // not for instantiation
    DataRowUtils() {
    }
}
