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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A utility class that removes redundant and mutually exclusive graph changes from the
 * graph diff.
 * 
 * @author Andrus Adamchik
 * @since 3.0
 */
public class GraphDiffCompressor {

    public GraphDiff compress(GraphDiff diff) {

        if (diff.isNoop()) {
            return diff;
        }

        CompressAction action = new CompressAction();
        diff.apply(action);
        return action.getCompressedDiff();
    }

    final class CompressAction implements GraphChangeHandler {

        private List<GraphDiff> compressed = new ArrayList<GraphDiff>();
        private Map<Object, List<NodeDiff>> diffsByNode = new HashMap<Object, List<NodeDiff>>();
        private Set<Object> deletedNodes;
        private Set<Object> createdNodes;

        GraphDiff getCompressedDiff() {

            // remove deleted nodes...
            if (deletedNodes != null) {

                for (Object nodeId : deletedNodes) {

                    Iterator<GraphDiff> it = compressed.iterator();

                    // if the node was inserted in the same transaction and later deleted,
                    // remove all its ops. Otherwise preserve arc ops (since delete rules
                    // depend on them), and delete operation itself.

                    // TODO: andrus 2008/02/04 - this doesn't take into account a
                    // possibility that a deleted node was re-inserted... Although I don't
                    // see how this could possibly happen with the present Cayenne API.
                    if (createdNodes != null && createdNodes.contains(nodeId)) {
                        while (it.hasNext()) {
                            NodeDiff diff = (NodeDiff) it.next();
                            if (nodeId.equals(diff.getNodeId())) {
                                it.remove();
                            }
                        }
                    }
                    else {
                        while (it.hasNext()) {
                            NodeDiff diff = (NodeDiff) it.next();
                            if (nodeId.equals(diff.getNodeId())) {
                                if (diff instanceof NodePropertyChangeOperation) {
                                    it.remove();
                                }
                            }
                        }
                    }

                }
            }

            return new CompoundDiff(compressed);
        }

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

            if (targetNodeId != null) {
                List<NodeDiff> diffs = diffsByNode.get(nodeId);
                if (diffs != null) {
                    for (int i = diffs.size() - 1; i >= 0; i--) {
                        NodeDiff diff = diffs.get(i);
                        if (diff instanceof ArcDeleteOperation) {
                            ArcDeleteOperation arcDiff = (ArcDeleteOperation) diff;
                            if (arcId.equals(arcDiff.getArcId())
                                    && targetNodeId.equals(arcDiff.targetNodeId)) {
                                diffs.remove(i);
                                compressed.remove(arcDiff);
                                return;
                            }
                        }
                    }
                }
            }

            registerDiff(new ArcCreateOperation(nodeId, targetNodeId, arcId));
        }

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

            if (targetNodeId != null) {
                List<NodeDiff> diffs = diffsByNode.get(nodeId);
                if (diffs != null) {
                    for (int i = diffs.size() - 1; i >= 0; i--) {
                        NodeDiff diff = diffs.get(i);
                        if (diff instanceof ArcCreateOperation) {
                            ArcCreateOperation arcDiff = (ArcCreateOperation) diff;
                            if (arcId.equals(arcDiff.getArcId())
                                    && targetNodeId.equals(arcDiff.targetNodeId)) {
                                diffs.remove(i);
                                compressed.remove(arcDiff);
                                return;
                            }
                        }
                    }
                }
            }

            registerDiff(new ArcDeleteOperation(nodeId, targetNodeId, arcId));
        }

        public void nodeCreated(Object nodeId) {
            registerDiff(new NodeCreateOperation(nodeId));

            if (createdNodes == null) {
                createdNodes = new HashSet<Object>();
            }

            createdNodes.add(nodeId);
        }

        public void nodeIdChanged(Object nodeId, Object newId) {
            registerDiff(new NodeIdChangeOperation(nodeId, newId));
        }

        public void nodeRemoved(Object nodeId) {

            registerDiff(new NodeDeleteOperation(nodeId));

            if (deletedNodes == null) {
                deletedNodes = new HashSet<Object>();
            }

            deletedNodes.add(nodeId);
        }

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

            List<NodeDiff> diffs = diffsByNode.get(nodeId);
            if (diffs != null) {

                for (int i = diffs.size() - 1; i >= 0; i--) {
                    NodeDiff diff = diffs.get(i);
                    if (diff instanceof NodePropertyChangeOperation) {
                        NodePropertyChangeOperation propertyDiff = (NodePropertyChangeOperation) diff;
                        if (property.equals(propertyDiff.getProperty())) {
                            propertyDiff.setNewValue(newValue);
                            return;
                        }
                    }
                }
            }

            registerDiff(new NodePropertyChangeOperation(
                    nodeId,
                    property,
                    oldValue,
                    newValue));
        }

        private void registerDiff(NodeDiff diff) {

            compressed.add(diff);
            List<NodeDiff> diffs = diffsByNode.get(diff.getNodeId());
            if (diffs == null) {
                diffs = new ArrayList<NodeDiff>();
                diffsByNode.put(diff.getNodeId(), diffs);
            }

            diffs.add(diff);
        }
    }
}
