/*****************************************************************
 *   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
 *
 *    http://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;

import org.apache.cayenne.graph.GraphChangeHandler;
import org.apache.cayenne.graph.GraphDiff;
import org.apache.cayenne.graph.GraphEvent;
import org.apache.cayenne.reflect.ArcProperty;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.cayenne.reflect.Property;
import org.apache.cayenne.reflect.ToManyProperty;
import org.apache.cayenne.util.Util;

/**
 * An object that merges "backdoor" modifications of the object graph coming from the
 * underlying DataChannel. When doing an update, CayenneContextMergeHandler blocks
 * broadcasting of GraphManager events.
 * 
 * @since 1.2
 */
class CayenneContextMergeHandler implements GraphChangeHandler, DataChannelListener {

    CayenneContext context;
    boolean active;

    CayenneContextMergeHandler(CayenneContext context) {
        this.context = context;
        this.active = true;
    }

    // ******* DataChannelListener methods *******

    public void graphChanged(final GraphEvent e) {
        // process flush
        if (shouldProcessEvent(e) && e.getDiff() != null) {
            runWithEventsDisabled(new Runnable() {

                public void run() {
                    e.getDiff().apply(CayenneContextMergeHandler.this);

                }
            });

            // post event outside of "execute" to make sure it is sent
            repostAfterMerge(e);
        }
    }

    public void graphFlushed(final GraphEvent e) {
        // TODO (Andrus, 10/17/2005) - there are a few problems with commit processing:

        // 1. Event mechanism reliability:
        // - events may come out of order (commit and then preceeding flush)
        // - events may be missing all together (commit arrived, while prior flush did
        // not)
        // Possible solution - an "event_version_id" to be used for optimistic locking

        // 2. We don't know if our own dirty objects were committed or not...
        // For now we will simply merge the changes, and keep the context dirty

        if (shouldProcessEvent(e)) {

            runWithEventsDisabled(new Runnable() {

                public void run() {

                    if (e.getDiff() != null) {
                        e.getDiff().apply(CayenneContextMergeHandler.this);
                    }
                }
            });

            // post event outside of "execute" to make sure it is sent
            repostAfterMerge(e);
        }
    }

    public void graphRolledback(final GraphEvent e) {

        // TODO: andrus, 3/29/2007: per CAY-771, if a LOCAL peer context posted the event,
        // just ignore it, however if the REMOTE peer reverted the parent remote
        // DataContext, we need to invalidate stale committed objects...
    }

    // ******* End DataChannelListener methods *******

    void repostAfterMerge(GraphEvent originalEvent) {
        // though the subject is CHANGE, "merge" events are really lifecycle.
        if (context.isLifecycleEventsEnabled()) {
            context.fireDataChannelChanged(originalEvent.getSource(), originalEvent.getDiff());
        }
    }

    /**
     * Executes merging of the external diff.
     */
    void merge(final GraphDiff diff) {
        runWithEventsDisabled(new Runnable() {

            public void run() {
                diff.apply(CayenneContextMergeHandler.this);
            }
        });
    }

    // ******* GraphChangeHandler methods *********

    public void nodeIdChanged(Object nodeId, Object newId) {
        // do not unregister the node just yet... only put replaced id in deadIds to
        // remove it later. Otherwise stored operations will not work
        Object node = context.internalGraphManager().getNode(nodeId);

        if (node != null) {
            context.internalGraphManager().deadIds().add(nodeId);
            context.internalGraphManager().registerNode(newId, node);

            if (node instanceof Persistent) {
                // inject new id
                ((Persistent) node).setObjectId((ObjectId) newId);
            }
        }
    }

    public void nodeCreated(Object nodeId) {
        // ignore
    }

    public void nodeRemoved(Object nodeId) {
        context.getGraphManager().unregisterNode(nodeId);
    }

    public void nodePropertyChanged(
            Object nodeId,
            String property,
            Object oldValue,
            Object newValue) {

        Object object = context.internalGraphManager().getNode(nodeId);
        if (object != null) {

            // do not override local changes....
            Property p = propertyForId(nodeId, property);
            if (Util.nullSafeEquals(p.readPropertyDirectly(object), oldValue)) {

                p.writePropertyDirectly(object, oldValue, newValue);
            }
        }
    }

    public void arcCreated(Object nodeId, Object targetNodeId, Object arcId) {
        // null source or target likely means the object is not faulted yet... Faults
        // shouldn't get disturbed by adding/removing arcs

        Object source = context.internalGraphManager().getNode(nodeId);
        if (source == null) {
            // no need to connect non-existent object
            return;
        }

        // TODO (Andrus, 10/17/2005) - check for local modifications to avoid
        // overwriting...

        ArcProperty p = (ArcProperty) propertyForId(nodeId, arcId.toString());
        if (p.isFault(source)) {
            return;
        }

        Object target = context.internalGraphManager().getNode(targetNodeId);
        if (target == null) {
            target = context.createFault((ObjectId) targetNodeId);
        }

        context.internalGraphAction().setArcChangeInProcess(true);
        try {
            if (p instanceof ToManyProperty) {
                ((ToManyProperty) p).addTarget(source, target, false);
            }
            else {
                p.writePropertyDirectly(source, null, target);
            }
        }
        finally {
            context.internalGraphAction().setArcChangeInProcess(false);
        }
    }

    public void arcDeleted(Object nodeId, Object targetNodeId, Object arcId) {

        // null source or target likely means the object is not faulted yet... Faults
        // shouldn't get disturbed by adding/removing arcs

        Object source = context.internalGraphManager().getNode(nodeId);
        if (source == null) {
            // no need to disconnect non-existent object
            return;
        }

        // (see "TODO" in 'arcCreated')
        ArcProperty p = (ArcProperty) propertyForId(nodeId, arcId.toString());
        if (p.isFault(source)) {
            return;
        }

        Object target = context.internalGraphManager().getNode(targetNodeId);
        if (target == null) {
            target = context.createFault((ObjectId) targetNodeId);
        }

        context.internalGraphAction().setArcChangeInProcess(true);
        try {
            if (p instanceof ToManyProperty) {
                ((ToManyProperty) p).removeTarget(source, target, false);
            }
            else {
                p.writePropertyDirectly(source, target, null);
            }
        }
        finally {
            context.internalGraphAction().setArcChangeInProcess(false);
        }
    }

    private Property propertyForId(Object nodeId, String propertyName) {
        ClassDescriptor descriptor = context.getEntityResolver().getClassDescriptor(
                ((ObjectId) nodeId).getEntityName());
        return descriptor.getProperty(propertyName);
    }

    // Returns true if this object is active; an event came from our channel, but did not
    // originate in it.
    boolean shouldProcessEvent(GraphEvent e) {
        // only process events that came from our channel, but did not originate in it
        // (i.e. likely posted by EventBridge)
        return active
                && e.getSource() == context.getChannel()
                && e.getPostedBy() != context
                && e.getPostedBy() != context.getChannel();
    }

    // executes a closure, disabling ObjectContext events for the duration of the
    // execution.

    private void runWithEventsDisabled(Runnable closure) {

        synchronized (context.internalGraphManager()) {
            boolean changeEventsEnabled = context.internalGraphManager().changeEventsEnabled;
            context.internalGraphManager().changeEventsEnabled = false;

            boolean lifecycleEventsEnabled = context.internalGraphManager().lifecycleEventsEnabled;
            context.internalGraphManager().lifecycleEventsEnabled = false;

            try {
                closure.run();
            }
            finally {
                context.internalGraphManager().changeEventsEnabled = changeEventsEnabled;
                context.internalGraphManager().lifecycleEventsEnabled = lifecycleEventsEnabled;
            }
        }
    }
}
