Package org.openquark.gems.client

Source Code of org.openquark.gems.client.GemGraphArgumentManager

/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*     * Redistributions of source code must retain the above copyright notice,
*       this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of Business Objects nor the names of its contributors
*       may be used to endorse or promote products derived from this software
*       without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/


/*
* GemGraphArgumentManager.java
* Creation date: Jun 22, 2004.
* By: Edward Lam
*/
package org.openquark.gems.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.openquark.cal.compiler.CALSourceGenerator;
import org.openquark.cal.compiler.CompositionNode;
import org.openquark.gems.client.Gem.PartInput;
import org.openquark.gems.client.GemGraph.InputCollectMode;
import org.openquark.gems.client.GemGraph.TraversalScope;
import org.openquark.util.Pair;


/**
* This class is responsible for managing argument targets and retargeting inputs in response to gem model events.
* @author Edward Lam
*/
public class GemGraphArgumentManager {
   
    /** The target collector.  Arguments will often be retargeted at this collector gem. */
    private final CollectorGem targetCollector;
   
    /** A flag to disable argument updating. */
    private boolean disableArgumentUpdating = false;

    /**
     * Constructor for an GemGraphArgumentManager.
     * @param targetCollector the target collector.
     */
    GemGraphArgumentManager(CollectorGem targetCollector) {
        this.targetCollector = targetCollector;
    }
   
    /**
     * Sets the disableArgumentUpdating flag.
     *   This causes automatic argument updating by the retargeter to be disabled.
     * @param newValue The new value.  If false, argument updating is enabled.  However, affected reflectors will not have been updated.
     * @return the old value.
     */
    boolean setArgumentUpdatingDisabled(boolean newValue) {
        boolean oldSetting = this.disableArgumentUpdating;
        this.disableArgumentUpdating = newValue;

        return oldSetting;
    }
   
    /**
     * Retarget arguments after a connection has taken place.
     * @param conn the connection which was made.
     */
    public void retargetForConnect(Connection conn) {
       
        if (disableArgumentUpdating) {
            return;
        }
       
        PartInput destInput = conn.getDestination();
        Gem destGem = destInput.getGem();
       
        // Get the input argument which is disappearing.  Note the affected collector.
        CollectorGem replacedArgumentTarget = GemGraph.getInputArgumentTarget(destInput);

        // Make sure inputs from the connected subtree make sense.
        reconcileArgumentTargetsOnConnect(conn);
       
        // Update the corresponding collectors.
        Set<CollectorGem> affectedCollectorSet = new HashSet<CollectorGem>();
       
        // Add the collector targeted by the input being connected.
        if (replacedArgumentTarget != null) {
            affectedCollectorSet.add(replacedArgumentTarget);
        }

        // Find collectors targeted by free inputs in the descendant gem forest
        // (when considering the gem graph after connection)
        List<PartInput> freeInputsInDescendantForestList =
            GemGraph.obtainUnboundDescendantInputs(destGem, TraversalScope.FOREST, InputCollectMode.UNBURNT_ONLY);

        for (final PartInput partInput : freeInputsInDescendantForestList) {
            CollectorGem inputArgumentTarget = GemGraph.getInputArgumentTarget(partInput);

            if (inputArgumentTarget != null) {
                affectedCollectorSet.add(inputArgumentTarget);
            }
        }

        // Now update the gem graph for affected collectors.
        updateForArgumentChange(affectedCollectorSet);
    }
   
    /**
     * Retarget arguments after a disconnection has taken place.
     * @param conn the connection which was disconnected.
     */
    public void retargetForDisconnect(Connection conn) {

        if (disableArgumentUpdating) {
            return;
        }
       
        // Make sure inputs from the disconnected subtree make sense.
        Set<CollectorGem> affectedCollectorSet = new HashSet<CollectorGem>(reconcileArgumentTargetsOnDisconnect(conn));
       
        {
            PartInput freedInput = conn.getDestination();
            Gem.PartOutput disconnectedOutput = conn.getSource();

            // (HACK!?) Temporarily re-bind a connection so that argument target resolution can take place for the connected tree.
            // Collectors targeted by arguments on trees rooted at collectors at inner scopes of the current tree will be affected.
            freedInput.bindConnection(conn);
            disconnectedOutput.bindConnection(conn);

            // Find collectors targeted by free inputs in the descendant gem forest
            // (when considering the gem graph after connection)
            List<PartInput> freeInputsInDescendantForestList =
                GemGraph.obtainUnboundDescendantInputs(freedInput.getGem(), TraversalScope.FOREST, InputCollectMode.UNBURNT_ONLY);

            for (final PartInput partInput : freeInputsInDescendantForestList) {
                CollectorGem inputArgumentTarget = GemGraph.getInputArgumentTarget(partInput);

                if (inputArgumentTarget != null) {
                    affectedCollectorSet.add(inputArgumentTarget);
                }
            }

            // Unbind the connection.
            freedInput.bindConnection(null);
            disconnectedOutput.bindConnection(null);
        }
       
        // Update the corresponding collectors.
        updateForArgumentChange(affectedCollectorSet);
    }
   
    /**
     * Retarget an input argument from one collector to another.
     * @param collectorArgument the argument to retarget.
     * @param newTarget the collector to which the input will be retargeted.
     * @param addIndex the index at which the retargeted argument will be placed, or -1 to add to the end.
     * @return the old argument index, or -1 if there was no old target.
     */
    public int retargetInputArgument(PartInput collectorArgument, CollectorGem newTarget, int addIndex) {
       
        CollectorGem oldTarget = GemGraph.getInputArgumentTarget(collectorArgument);
       
        Set<CollectorGem> affectedCollectors = new HashSet<CollectorGem>();
       
        // Remove the argument from its old target if any.
        int oldArgIndex = -1;
        if (oldTarget != null) {
            oldArgIndex = oldTarget.getTargetArguments().indexOf(collectorArgument);
            oldTarget.removeArgument(collectorArgument);
           
            affectedCollectors.add(oldTarget);
        }
       
        // Add the argument to its new target.
        if (addIndex < 0) {
            newTarget.addArgument(collectorArgument);
        } else {
            newTarget.addArguments(addIndex, Collections.singleton(collectorArgument));
        }
        affectedCollectors.add(newTarget);

        // Update collectors and reflectors..
        updateForArgumentChange(affectedCollectors);

        return oldArgIndex;
    }
   
    /**
     * Retarget the inputs on the given gem for a definition change.
     * @param changedGem the gem whose definition changed.
     * @param oldInputs the inputs from before the change.
     */
    public void retargetArgumentsForDefinitionChange(Gem changedGem, PartInput[] oldInputs) {
        if (disableArgumentUpdating) {
            return;
        }

        Set<CollectorGem> affectedCollectorSet = reconcileArgumentsOnDefinitionChange(changedGem, oldInputs);
        updateForArgumentChange(affectedCollectorSet);
    }
   
    /**
     * Ensure that a collector's input is targeted when it is added to the gem graph.
     * @param addedCollector the collector which was added.
     */
    public void retargetArgumentsOnAdd(CollectorGem addedCollector) {

        if (disableArgumentUpdating) {
            return;
        }
       
        // get the input argument.
        PartInput newPartInput = addedCollector.getCollectingPart();

        // Add the arg to the target collector, if it's not already targeted.
        // TODOEL: this assumes that the target is (or will be) in the graph.
        CollectorGem inputTarget = GemGraph.getInputArgumentTarget(newPartInput);
        if (inputTarget == null) {
            addTargetArgsInSourceOrder(targetCollector, Collections.singleton(newPartInput));
        }
       
        // Update its reflected inputs.
        updateForArgumentChange(Collections.singleton(addedCollector));
    }
   
    /**
     * Ensure that a collector's input and targeting inputs are properly (un)targeted when it is removed from the gem graph.
     * @param removedCollector the collector which was removed.
     * @param retargetCollector the collector to which any arguments targeting the removed collector will be retargeted.
     */
    public void retargetArgumentsOnRemove(CollectorGem removedCollector, CollectorGem retargetCollector) {
        if (disableArgumentUpdating) {
            return;
        }
       
        PartInput collectingPart = removedCollector.getCollectingPart();
        CollectorGem collectingPartArgTarget = GemGraph.getInputArgumentTarget(collectingPart);

        // Remove the arg from its target, if any.
        if (collectingPartArgTarget != null) {
            collectingPartArgTarget.removeArgument(collectingPart);
        }
       
        // Update its reflected inputs.
        updateForArgumentChange(Collections.singleton(removedCollector));
       
        // Reassign all other arguments targeting the collector to the retarget collector.
        List<PartInput> targetArguments = removedCollector.getTargetArguments();
        removedCollector.removeArguments(targetArguments);
        retargetCollector.addArguments(targetArguments);
    }
   
    /**
     * When a connection takes place, call this method to reconcile the arguments on the connected subtree
     *   to make sure that their targets still make sense.
     * The old argument will disappear from its target, and new arguments will appear in appropriate target(s).
     * @param conn the connection which was made.
     */
    private void reconcileArgumentTargetsOnConnect(Connection conn) {
       
        // Get the arguments on the connecting subtree.
        Gem.PartOutput outputPart = conn.getSource();
        List<PartInput> connectedInputList = GemGraph.obtainUnboundDescendantInputs(outputPart.getGem(), TraversalScope.TREE, InputCollectMode.UNBURNT_ONLY);
       
        PartInput replacedInputArgument = conn.getDestination();
       
        // Get the input argument which is disappearing.  Note the affected collector.
        CollectorGem replacedArgumentTarget = GemGraph.getInputArgumentTarget(replacedInputArgument);
       
        // The best collector for the new input arguments to target is whatever collector was targeted by the argument being connected.
        // The exception is where any of the new inputs belong to reflectors, in which case the best collector to target is the target itself.
        boolean reflectorInputsPresent = false;
        for (final PartInput input : connectedInputList) {
            if (input.getGem() instanceof ReflectorGem) {
                reflectorInputsPresent = true;
                break;
            }
        }
        CollectorGem collectorToTarget = reflectorInputsPresent ? targetCollector : GemGraph.getInputArgumentTarget(replacedInputArgument);
       
        // Gather the new args, and affected collectors.
        Set<PartInput> newArgSet = new LinkedHashSet<PartInput>();

        for (final PartInput newInput: connectedInputList) {
            newArgSet.add(newInput);
        }
       
        // Try to put the arguments in the right place.
        if (collectorToTarget == null) {
            // Remove the replaced argument (if any) from its target.
            if (replacedArgumentTarget != null) {
                replacedArgumentTarget.removeArgument(replacedInputArgument);
            }
           
        } else if (replacedArgumentTarget == collectorToTarget) {
            // Replace the argument with the new arguments.
            replacedArgumentTarget.replaceArgument(replacedInputArgument, newArgSet);
           
        } else {
            // Remove the replaced argument (if any) from its target.
            if (replacedArgumentTarget != null) {
                replacedArgumentTarget.removeArgument(replacedInputArgument);
            }

            // Retarget any new arguments.
            if (!newArgSet.isEmpty()) {
                // Get the inputs on the tree when connected.
                List<PartInput> joinedInputList = GemGraph.obtainUnboundDescendantInputs(replacedInputArgument.getGem().getRootGem(), TraversalScope.TREE,
                                                                              InputCollectMode.UNBURNT_ONLY);
               
                // Get the first and last indices of the new inputs in the list.
                int firstInputIndex = joinedInputList.indexOf(connectedInputList.get(0));    // if empty, should have been caught by the "if".
                int lastInputIndex = firstInputIndex + connectedInputList.size() - 1;
               
                if (firstInputIndex < 0) {
                    throw new IllegalStateException("Programming error.");
                }
   
                // retarget according to the connected tree.
                retargetInputArgumentsForTree(joinedInputList, firstInputIndex, lastInputIndex)// TODOEL: this doesn't use "collectorToTarget".
            }
        }
    }
   
    /**
     * When a disconnection takes place, call this method to reconcile the arguments on the connected subtree
     *   to make sure that their targets still make sense.
     * The old arguments (on the disconnected tree) will disappear from their targets,
     *   and the new argument (the connection destination) will appear in an appropriate target.
     * @param conn the connection which was broken.
     * @return  the collectors which were directly affected (ie. had arguments added or removed).
     */
    private Set<CollectorGem> reconcileArgumentTargetsOnDisconnect(Connection conn) {
       
        // The set of affected collectors.
        // Note that in the body of this method we freely add nulls to this set.  Null is removed before returning the set to the caller.
        Set<CollectorGem> affectedCollectorSet = new HashSet<CollectorGem>();
       
        PartInput freedInput = conn.getDestination();
        Gem.PartOutput disconnectedOutput = conn.getSource();

        // Determine whether the disconnected input was originally orphaned (ie. only present because of the connection) when connected.
        // We really only have to check for orphaned reflector inputs, since code gem panels check for orphaned code gem inputs.
        boolean wasOrphanedReflectorInput = false;
        if (freedInput.getGem() instanceof ReflectorGem) {
            PartInput reflectedInput = ((ReflectorGem)freedInput.getGem()).getReflectedInput(freedInput);
            if (reflectedInput == null || reflectedInput.isConnected()) {
                wasOrphanedReflectorInput = true;

            } else {
                Gem reflectedInputGem = reflectedInput.getGem();
                int reflectedInputNum = reflectedInput.getInputNum();
                wasOrphanedReflectorInput = reflectedInputNum >= reflectedInputGem.getNInputs() ||
                                            reflectedInputGem.getInputPart(reflectedInputNum) != reflectedInput;
            }
        }
       
        // Get the inputs on the disconnecting subtree.
        List<PartInput> disconnectedTreeInputList = GemGraph.obtainUnboundDescendantInputs(disconnectedOutput.getGem(), TraversalScope.TREE,
                                                                                InputCollectMode.UNBURNT_ONLY);
       
        // (HACK!?) Temporarily re-bind a connection so that argument target resolution can take place.
        freedInput.bindConnection(conn);
        disconnectedOutput.bindConnection(conn);
       
        Map<PartInput, CollectorGem> disconnectedInputToTargetMap = new HashMap<PartInput, CollectorGem>();
        for (final PartInput disconnectedTreeInput : disconnectedTreeInputList) {
            CollectorGem affectedCollector = GemGraph.getInputArgumentTarget(disconnectedTreeInput);
            disconnectedInputToTargetMap.put(disconnectedTreeInput, affectedCollector);
        }
   
        // Figure out the collector to target.
        CollectorGem collectorToTarget = getCollectorToTarget(disconnectedTreeInputList);
        if (collectorToTarget == null) {
            collectorToTarget = GemGraph.obtainOutermostCollector(freedInput.getGem());
        }
        affectedCollectorSet.add(collectorToTarget);
         
        // Unbind the connection.
        freedInput.bindConnection(null);
        disconnectedOutput.bindConnection(null);

        // First the case of an orphaned input (just remove the input from its target).
        if (wasOrphanedReflectorInput) {
            CollectorGem argumentCollectorTarget = disconnectedInputToTargetMap.get(freedInput);
            if (argumentCollectorTarget != null) {
                argumentCollectorTarget.removeArgument(freedInput);
               
                affectedCollectorSet.add(argumentCollectorTarget);
            }
        }
       
        // Iterate over the inputs of the tree which was disconnected, removing them from their target collector.
        // Also, if necessary retarget the new free argument if any of the disconnecting inputs targeted the desired target.
        boolean needToRetargetFreedArgument = !wasOrphanedReflectorInput;
       
        for (final Map.Entry<PartInput, CollectorGem> mapEntry: disconnectedInputToTargetMap.entrySet()) {
            PartInput disconnectedTreeInput = mapEntry.getKey();
            CollectorGem affectedCollector = mapEntry.getValue();
           
            if (affectedCollector != null) {
               
                // Retarget the new free argument if this argument targets the target collector.
                if (needToRetargetFreedArgument && collectorToTarget != null && collectorToTarget.isTargetedBy(disconnectedTreeInput)) {
                    collectorToTarget.replaceArgument(disconnectedTreeInput, Collections.singleton(freedInput));
                    needToRetargetFreedArgument = false;

                } else {
                    affectedCollector.removeArgument(disconnectedTreeInput);
                }
               
                affectedCollectorSet.add(affectedCollector);
            }
        }
       
        // re-target the new input's argument if necessary, and we haven't already.
        if (needToRetargetFreedArgument && collectorToTarget != null) {

            // Retarget according to inputs from the new input's tree.
            Gem inputRoot = freedInput.getGem().getRootGem();
            List<PartInput> destinationInputList = GemGraph.obtainUnboundDescendantInputs(inputRoot, TraversalScope.TREE, InputCollectMode.UNBURNT_ONLY);

            int newInputIndex = destinationInputList.indexOf(freedInput);
            if (newInputIndex < 0) {
                throw new IllegalStateException("Programming Error.");
            }
   
            CollectorGem collectorTargeted = retargetInputArgumentsForTree(destinationInputList, newInputIndex, newInputIndex);
            affectedCollectorSet.add(collectorTargeted);
        }

        // Remove any nulls from the set (for any args above which didn't have a target).
        affectedCollectorSet.remove(null);
       
        return affectedCollectorSet;
    }

    /**
     * Remove inputs from their targets.
     * @param inputsToRemove the inputs to remove from their targets.
     * @return the collectors from which the inputs were removed.
     */
    private static Set<CollectorGem> removeFromTargets(Set<PartInput> inputsToRemove) {
        Set<CollectorGem> affectedCollectorSet = new HashSet<CollectorGem>();
       
        // Now remove from their targets any arguments for inputs which are going away.
        for (final PartInput inputToRemove : inputsToRemove) {
            CollectorGem targetedCollector = GemGraph.getInputArgumentTarget(inputToRemove);
           
            if (targetedCollector != null) {
                affectedCollectorSet.add(targetedCollector);
                targetedCollector.removeArgument(inputToRemove);
            }
        }
       
        return affectedCollectorSet;
    }
   
    /**
     * Get the inputs on the tree before a gem's inputs changed.
     * @param changedGem the gem whose inputs changed.
     * @param oldInputs the inputs before the gem changed.
     * @return the inputs on the connected gem tree before the change.
     */
    private static List<PartInput> getOldUnburntTreeInputList(Gem changedGem, PartInput[] oldInputs) {
        // Create a code gem with the right number of inputs.
        CodeGem codeGem = new CodeGem(oldInputs.length);

        // Replace the changed gem connections with the code gem connections.
        Set<Connection> oldConnections = new HashSet<Connection>();

        for (int i = 0, nOldInputs = oldInputs.length; i < nOldInputs; i++) {
            PartInput oldInput = oldInputs[i];
           
            Connection oldInputConnection = oldInput.getConnection();
           
            if (oldInputConnection != null) {
                oldConnections.add(oldInputConnection);
               
                Gem.PartOutput source = oldInputConnection.getSource();
                PartInput dest = codeGem.getInputPart(i);

                Connection newConnection = new Connection(source, dest);
                source.bindConnection(newConnection);
                dest.bindConnection(newConnection);
            }
        }

        Connection oldOutputConnection = changedGem.getOutputPart().getConnection();
        if (oldOutputConnection != null) {
            oldConnections.add(oldOutputConnection);
           
            Gem.PartOutput source = codeGem.getOutputPart();
            PartInput dest = oldOutputConnection.getDestination();

            Connection newConnection = new Connection(source, dest);
            source.bindConnection(newConnection);
            dest.bindConnection(newConnection);
        }       

        // Grab the old inputs.
        List<PartInput> oldUnburntTreeInputList =
            new ArrayList<PartInput>(GemGraph.obtainUnboundDescendantInputs(changedGem.getRootGem(), TraversalScope.TREE, InputCollectMode.UNBURNT_ONLY));
       
        // Replace code gem inputs with old inputs.
        int index = 0;
        for (final PartInput oldUnburntTreeInput : oldUnburntTreeInputList) {
            if (oldUnburntTreeInput.getGem() == codeGem) {
                PartInput oldInput = oldInputs[oldUnburntTreeInput.getInputNum()];
                oldUnburntTreeInputList.set(index, oldInput);
            }
            index++;
        }
       
        // Revert all the connections.
        for (final Connection oldConnection : oldConnections) {
            oldConnection.getSource().bindConnection(oldConnection);
            oldConnection.getDestination().bindConnection(oldConnection);
        }
       
        return oldUnburntTreeInputList;
    }
   
    /**
     * Retarget the inputs on the given gem for a definition change.
     * @param changedGem the gem whose definition changed.
     * @param oldInputs the inputs from before the change.
     * @return the collectors which were affected by argument targeting changes.
     */
    private static Set<CollectorGem> reconcileArgumentsOnDefinitionChange(Gem changedGem, PartInput[] oldInputs) {
       
        // TODOEL: make use of cached input targeting info.
        // TODOEL: break this method up into smaller pieces.
        // TODOEL: handle better the case where the new inputs are in natural order, but tree inputs are not.
        //          (eg. if one of the branch's had inputs reorganized).
       
        Set<CollectorGem> affectedCollectorSet = new HashSet<CollectorGem>();
       
        // Check whether there is a collector to target.
        Set<CollectorGem> enclosingCollectorSet = GemGraph.obtainEnclosingCollectors(changedGem);
        if (enclosingCollectorSet.isEmpty()) {
            return affectedCollectorSet;
        }
       
        // Get the inputs on the tree to which the gem is attached.
        List<PartInput> unburntTreeInputList = GemGraph.obtainUnboundDescendantInputs(changedGem.getRootGem(), TraversalScope.TREE,
                                                                           InputCollectMode.UNBURNT_ONLY);
        Set<PartInput> unburntTreeInputSet = new LinkedHashSet<PartInput>(unburntTreeInputList);

        // Get the old tree inputs.
        List<PartInput> oldUnburntTreeInputList = getOldUnburntTreeInputList(changedGem, oldInputs);

        // Get the old targetable inputs.
        List<PartInput> oldTargetableGemInputs = new ArrayList<PartInput>();
        for (final PartInput oldInput : oldInputs) {
            if (!oldInput.isConnected() && !oldInput.isBurnt()) {
                oldTargetableGemInputs.add(oldInput);
            }
        }
       
        // Get the updated targetable inputs.
        Set<PartInput> updatedTargetableInputSet = new LinkedHashSet<PartInput>();
        int nArgs = changedGem.getNInputs();
        for (int i = 0; i < nArgs; i++) {
            PartInput updatedGemInput = changedGem.getInputPart(i);
            if (!updatedGemInput.isConnected() && !updatedGemInput.isBurnt()) {
                updatedTargetableInputSet.add(updatedGemInput);
            }
        }
       
        // Get the new (untargeted) arguments.
        List<PartInput> untargetedNewArguments = new ArrayList<PartInput>(updatedTargetableInputSet);
        untargetedNewArguments.removeAll(oldTargetableGemInputs);
       
        // Find the best collector to target.
        // This should only be null if there were no targetable inputs on the tree or the gem which changed.
        CollectorGem collectorToTarget = oldTargetableGemInputs.isEmpty() ? getCollectorToTarget(oldUnburntTreeInputList)
                                                                          : getCollectorToTarget(oldTargetableGemInputs);
        if (collectorToTarget == null) {
            collectorToTarget = GemGraph.obtainOutermostCollector(changedGem);
        }

        // Get the collector's old targeting arguments.
        List<PartInput> targetArgs = collectorToTarget.getTargetArguments();
        int nTargetArgs = targetArgs.size();

        // Get the tree arguments which used to target the collector to target, in order.
        List<PartInput> oldTargetingTreeInputs = new ArrayList<PartInput>();
        for (final PartInput unburntTreeInput : oldUnburntTreeInputList) {
            if (GemGraph.getInputArgumentTarget(unburntTreeInput) == collectorToTarget) {
                oldTargetingTreeInputs.add(unburntTreeInput);
            }
        }

        // Calculate characteristics of the previous tree inputs - whether they were together, and/or in natural order.
        Pair<Boolean, Boolean> oldTreeInputsTogetherOrInNaturalOrder = isTogetherOrInNaturalOrder(oldTargetingTreeInputs, targetArgs);
        boolean oldTreeInputsTogether = oldTreeInputsTogetherOrInNaturalOrder.fst().booleanValue();
        boolean oldTreeInputsInNaturalOrder = oldTreeInputsTogetherOrInNaturalOrder.snd().booleanValue();
       
        // Gem the gem inputs which carry over, in order.
        List<PartInput> remainingOldGemInputList = new ArrayList<PartInput>(unburntTreeInputSet);
        remainingOldGemInputList.retainAll(oldTargetableGemInputs);
       
        // Determine if they occur together in natural order (or just together) in the targeted collector's target arguments.
        Pair<Boolean, Boolean> isTogetherOrInNaturalOrder = isTogetherOrInNaturalOrder(oldTargetableGemInputs, targetArgs);
        boolean isTogether = isTogetherOrInNaturalOrder.fst().booleanValue();
        boolean isInNaturalOrder = isTogetherOrInNaturalOrder.snd().booleanValue();
       
        // If the old inputs (on the gem or on the tree) were in natural order, ensure that they remain so.
        if (oldTreeInputsInNaturalOrder) {
            collectorToTarget.removeArguments(targetArgs);
            targetArgs = getTargetArgsInOrder(targetArgs, unburntTreeInputList);
            collectorToTarget.addArguments(targetArgs);

            // The collector to target will also be affected..
            affectedCollectorSet.add(collectorToTarget);

        } else if (isInNaturalOrder) {
            collectorToTarget.removeArguments(targetArgs);
            targetArgs = getTargetArgsInOrder(targetArgs, Arrays.asList(changedGem.getInputParts()));
            collectorToTarget.addArguments(targetArgs);

            // The collector to target will also be affected..
            affectedCollectorSet.add(collectorToTarget);
        }

        // Calculate the inputs which are going away.
        Set<PartInput> departingInputs = new HashSet<PartInput>(oldTargetableGemInputs);
        departingInputs.removeAll(updatedTargetableInputSet);

        // If there aren't any untargeted new arguments, we're done.
        if (untargetedNewArguments.isEmpty()) {
            // Remove from their targets any arguments for inputs which are going away.
            affectedCollectorSet.addAll(removeFromTargets(departingInputs));

            return affectedCollectorSet;
        }
       
        //
        // The rest of this method deals with where to place untargeted new arguments.
        //
       
        // The collector to target will also be affected..
        affectedCollectorSet.add(collectorToTarget);
       
        if (isTogether && isInNaturalOrder) {
            // This is the argument on collectorToTarget before which this gem's args appear.
            //   Null means add to the beginning.
            PartInput argBefore;
           
            // Add the new args to wherever they appear in the tree input set.
            PartInput firstUpdatedInput = updatedTargetableInputSet.iterator().next();
            int firstUpdatedInputIndex = unburntTreeInputList.indexOf(firstUpdatedInput);
           
            if (firstUpdatedInputIndex < 1) {
                argBefore = null;
               
            } else {
                // Get the immediately preceding input on the tree which targets the same collector.
                List<PartInput> precedingTreeInputs = unburntTreeInputList.subList(0, firstUpdatedInputIndex);
                PartInput precedingTargetingInput = getTargetingArg(precedingTreeInputs, collectorToTarget, false);
               
                if (precedingTargetingInput != null) {
                    // Add after the preceding tree input which targets the same collector.
                    argBefore = precedingTargetingInput;

                } else {
                    // No preceding tree inputs target the same collector.

                    // Determine if any inputs which follow on the tree target the same collector.
                    PartInput lastUpdatedInput = (new ArrayList<PartInput>(updatedTargetableInputSet)).get(updatedTargetableInputSet.size() - 1);
                    int lastUpdatedInputIndex = unburntTreeInputList.indexOf(lastUpdatedInput);
                   
                    List<PartInput> followingTreeInputs = unburntTreeInputList.subList(lastUpdatedInputIndex + 1, unburntTreeInputList.size());
                    PartInput followingTargetingInput = getTargetingArg(followingTreeInputs, collectorToTarget, false);
                   
                    if (followingTargetingInput != null) {
                        // Add before the following tree input which targets the same collector.
                        int followingArgTargetIndex = targetArgs.indexOf(followingTargetingInput);
                       
                        // Construct the reverse list of preceding targeting args.
                        List<PartInput> reversedPrecedingTargetArgs = new ArrayList<PartInput>(targetArgs.subList(0, followingArgTargetIndex));
                        Collections.reverse(reversedPrecedingTargetArgs);
                       
                        // Set the arg before to the last preceding arg which isn't an input to the changed gem.
                        // If there isn't any such arg, add to the beginning.
                        argBefore = null;
                        for (final PartInput precedingTargetArg : reversedPrecedingTargetArgs) {
                            if (precedingTargetArg.getGem() != changedGem) {
                                argBefore = precedingTargetArg;
                                break;
                            }
                        }
                       
                    } else {
                        // Add to the end of the collector's args.
                        argBefore = targetArgs.get(nTargetArgs - 1);
                    }
                }
            }

            // remove all the old args..
            collectorToTarget.removeArguments(remainingOldGemInputList);
           
            // add all the updated args back..
            collectorToTarget.addArguments(argBefore, updatedTargetableInputSet, argBefore != null);

        } else if (isInNaturalOrder) {
            // The old inputs appear in natural order in the target arg list.
            // Maintain natural order among new inputs.
           
            // Get the tree arguments which will target the collector to target.
            List<PartInput> newTargetingTreeInputs = new ArrayList<PartInput>();
            for (final PartInput unburntTreeInput : unburntTreeInputList) {
                if (GemGraph.getInputArgumentTarget(unburntTreeInput) == collectorToTarget || unburntTreeInput.getGem() == changedGem) {
                    newTargetingTreeInputs.add(unburntTreeInput);
                }
            }
           
            // If the old tree inputs appeared together in natural order, replace with the new tree inputs.
            if (oldTreeInputsTogether && oldTreeInputsInNaturalOrder) {

                // Remove the arguments, add them back at the appropriate location.
                int addIndex = targetArgs.indexOf(oldTargetingTreeInputs.get(0));
                collectorToTarget.removeArguments(oldTargetingTreeInputs);
                collectorToTarget.addArguments(addIndex, newTargetingTreeInputs);
               

            } else if (oldTreeInputsInNaturalOrder) {
                // If the tree inputs just appear in natural order but not together, insert semi-smartly.
                for (final PartInput newInput : untargetedNewArguments) {
                    // If no preceding tree args, add before the first of the args which targeted the collector.
                    if (newInput == newTargetingTreeInputs.get(0)) {
                        PartInput firstTargetingArg = oldTargetingTreeInputs.get(0);
                        collectorToTarget.addArguments(firstTargetingArg, Collections.singleton(newInput), false);

                    } else {
                        // Add after the arg which precedes it.
                        PartInput precedingTargetingTreeInput = newTargetingTreeInputs.get(newTargetingTreeInputs.indexOf(newInput) - 1);
                        collectorToTarget.addArguments(precedingTargetingTreeInput, Collections.singleton(newInput), true);
                    }
                }
               
            } else {
                // Otherwise, the tree inputs are not in natural order, but the old gem inputs are.
                // Add to the end.
                // Note: it would be better to maintain order where this makes sense
                //   (eg. if intervening gem tree inputs appear together but are reordered wrt each other).
               
                // Now add the arguments to the collector, at the end of the target arg list.
                collectorToTarget.addArguments(untargetedNewArguments);
            }
           
        } else {
            // The remaining args are not in natural order on target args.
           
            // This is the argument on collectorToTarget before which the new args appear.
            //   Null means add to the end.
            PartInput argBefore;   

            if (isTogether) {
                // Add after the last of the target's arguments that belongs to an input on this gem.
                argBefore = null;
                for (final PartInput targetArg : targetArgs) {
                    if (targetArg.getGem() == changedGem) {
                        argBefore = targetArg;
                    }
                }

            } else {
                // Add to the end.
                argBefore = null;
            }
           
            // Now add the untargeted new arguments to the collector, at the appropriate location.
            collectorToTarget.addArguments(argBefore, untargetedNewArguments, true);
        }
       
        // Remove from their targets any arguments for inputs which are going away.
        affectedCollectorSet.addAll(removeFromTargets(departingInputs));
       
        return affectedCollectorSet;
    }

    /**
     * @param oldTargetArgs the old target arguments
     * @param updatedTargetingInputs arguments, in the order in which they appear as updated.
     * @return a list of arguments from targetArgs, reordered such that args also appearing in updatedTargetingInputs
     *   appear in their new order.
     */
    private static List<PartInput> getTargetArgsInOrder(List<PartInput> oldTargetArgs, List<PartInput> updatedTargetingInputs) {
       
        // make a copy of the target args array.
        List<PartInput> newTargetArgs = new ArrayList<PartInput>(oldTargetArgs);
       
        // Get the args which appear in both sets, in their old order.
        List<PartInput> oldIntersection = new ArrayList<PartInput>(oldTargetArgs);
        oldIntersection.retainAll(updatedTargetingInputs);
       
        if (!oldIntersection.isEmpty()) {
            // Get the index of the first of the targeting inputs in the old target args list.
            int firstIndex = oldTargetArgs.indexOf(oldIntersection.get(0));
   
            // Get the intersection, in the new order.
            List<PartInput> newIntersection = new ArrayList<PartInput>(updatedTargetingInputs);
            newIntersection.retainAll(oldTargetArgs);
           
            if (!newIntersection.equals(oldIntersection)) {
                // For now, just put all the intersecting args together, and put anything that might lie among those at the end.
                newTargetArgs.removeAll(newIntersection);
                newTargetArgs.addAll(firstIndex, newIntersection);
            }
        }
       
        return newTargetArgs;
    }

    /**
     * Return whether a given list's items appear together in another list, or appear in order in the other list.
     * @param subList the list in question.
     * @param superList the list which contains the first list.
     * @return Pair:
     *   The first item is whether the items appear together, the second is whether they appear in order.
     *   Both false if subList is not a sublist of superList, or if the intersection of subList and superList is empty.
     */
    private static Pair<Boolean, Boolean> isTogetherOrInNaturalOrder(List<PartInput> subList, List<PartInput> superList) {
        // Copy the superList, and cut it down to only those items in the sublist.
        List<PartInput> cutSuperList = new ArrayList<PartInput>(superList);
        cutSuperList.retainAll(subList);
       
        // Check for list precondition, non-intersecting lists.
        if (cutSuperList.isEmpty() || cutSuperList.size() != subList.size()) {
            return new Pair<Boolean, Boolean>(Boolean.FALSE, Boolean.FALSE);
        }

        // Figure out if the items are in natural order.
        boolean isInNaturalOrder = cutSuperList.equals(new ArrayList<PartInput>(subList));
       
        // Calculate the bounds of a sublist the length of subList, starting from the index of the first item of subList in superList.
        int firstIndex = subList.isEmpty() ? -1 : superList.indexOf(subList.get(0));
        int lastIndex = firstIndex + subList.size();
       
        // Figure out if the items are together.
        boolean isTogether = firstIndex >= 0 && lastIndex < superList.size() && superList.subList(firstIndex, lastIndex).containsAll(subList);

        // Return the result.       
        return new Pair<Boolean, Boolean>(Boolean.valueOf(isTogether), Boolean.valueOf(isInNaturalOrder));
    }
   
    /**
     * Get the first or last arg in the list which targets the given collector.
     * @param argList the inputs to check.
     * @param collectorTarget the target.
     * @param first if true, returns the first arg which targets collectorTarget.  If false, returns the last one.
     * @return the first or last arg in the list which targets the given collector.
     */
    private static PartInput getTargetingArg(List<PartInput> argList, CollectorGem collectorTarget, boolean first) {
        // If we are getting the last arg, reverse the list.
        if (!first) {
            argList = new ArrayList<PartInput>(argList);
            Collections.reverse(argList);
        }
       
        // Iterate over the list, returning the first arg which targets the collector.
        for (final PartInput arg : argList) {
            if (GemGraph.getInputArgumentTarget(arg) == collectorTarget) {
                return arg;
            }
        }
       
        // No args in the list target the collector.
        return null;
    }

    /**
     * Given a set of arguments which target some collector(s), determine the best collector for any new inputs to target.
     * Assumption: all the inputs in the list to consider are in the same gem tree, and have not been disconnected from their targets.
     *
     * @param inputsToConsider the inputs to consider when calculating the best collector
     *   for their arguments to target.  eg. if these all target a given collector, that collector will be returned.
     * @return CollectorGem the best collector for an input to target, considering the given inputs. 
     *   The target gem if the inputs target more than one collector.
     *   Null if the inputs have no enclosing collectors.
     */
    private static CollectorGem getCollectorToTarget(Collection<PartInput> inputsToConsider) {

        // If all the arguments target a collector, use that one.  Otherwise use the outermost collector.
        CollectorGem collectorToTarget = null;

        boolean firstIteration = true;
        for (final PartInput nextOldGemInput : inputsToConsider) {

            CollectorGem inputTargetedCollector = GemGraph.getInputArgumentTarget(nextOldGemInput);

            if (firstIteration) {
                // the first iteration..
                collectorToTarget = inputTargetedCollector;
                firstIteration = false;

            } else if (collectorToTarget != inputTargetedCollector) {
                // Input args target more than one collector.  Return the outermost collector (if any).
                return GemGraph.obtainOutermostCollector(nextOldGemInput.getGem());

            } else {
                // this arg targets the same collector as all the other ones.
                // Do nothing.
            }
        }
       
        return collectorToTarget;
    }
   
    /**
     * Retarget the arguments for a set of inputs according to the tree on which they appear.
     *   When connecting or disconnecting, a gem tree will gain some inputs, which remain to be retargeted.
     *   This method attempts to infer argument information (eg. arg position) from the inputs around the new inputs.
     *   For instance, if an input coming before the set of added inputs (with respect to the tree) targets the target
     *     collector, and the added inputs are determined to also target the target collector, the arguments for the added
     *     inputs will be placed after the argument for the input which precedes it in the tree.
     *
     * @param newInputList the list of inputs on the tree.
     * @param firstNewInputIndex the index of the first new input whose argument should be retargeted.
     * @param lastNewInputIndex the index of the last new input whose argument should be retargeted.
     * @return the CollectorGem to which the arguments were targeted, or null if no argument (re)targeting took place.
     */
    private CollectorGem retargetInputArgumentsForTree(List<PartInput> newInputList, int firstNewInputIndex, int lastNewInputIndex) {

        // Try to infer the position of the new arguments from the other inputs.
        // The new arguments.
        Set<PartInput> newArgSet = new LinkedHashSet<PartInput>();
       
        // Gather the new args.
        for (final PartInput newInput : newInputList.subList(firstNewInputIndex, lastNewInputIndex + 1)) {
            newArgSet.add(newInput);
        }
       
        if (newArgSet.isEmpty()) {
            return null;
        }

        // Determine the new arguments
        // TODOEL: If all the arguments in this subtree are targeting a collector, should that be the collectorToTarget?
        //   We should at least do this in the case where there is only one replacing input.
        Gem rootGem = newArgSet.iterator().next().getGem().getRootGem();
        CollectorGem collectorToTarget =  (!(rootGem instanceof CollectorGem)) ? null : targetCollector;  // Can this (sometimes) be rootGem?

        if (collectorToTarget == null) {
            return null;
        }
       
        // Now add the arguments to the target.

        // Walk backwards over the inputs coming before the replaced input, looking for an input which targets the target collector.
        // If we find one, add all the arguments after that input.
        List<PartInput> beforeInputs = newInputList.subList(0, firstNewInputIndex);
        PartInput beforeInputArray[] = (new ArrayList<PartInput>(beforeInputs)).toArray(new PartInput[firstNewInputIndex]);
        for (int i = firstNewInputIndex - 1; i > -1; i--) {
            PartInput inputArgument = beforeInputArray[i];
            if (GemGraph.getInputArgumentTarget(inputArgument) == collectorToTarget) {
                collectorToTarget.addArguments(inputArgument, newArgSet, true);
                return collectorToTarget;
            }
        }

        // Walk forward over the inputs coming after the replaced input, looking for an input which targets the target collector.
        // If we find one, add all the arguments before that input.
        if (lastNewInputIndex + 1 < newInputList.size()) {
            List<PartInput> afterInputs = newInputList.subList(lastNewInputIndex + 1, newInputList.size());
            for (final PartInput inputArgument : afterInputs) {
                if (GemGraph.getInputArgumentTarget(inputArgument) == collectorToTarget) {
                    collectorToTarget.addArguments(inputArgument, newArgSet, false);
                    return collectorToTarget;
                }
            }
        }
       
        // Just add the args wherever the CALSourceGenerator would naturally put them.
        addTargetArgsInSourceOrder(collectorToTarget, newArgSet);
        return collectorToTarget;
    }
   
    /**
     * Add the given arguments to the target, in the order in which they appear in the source.
     *   If they do not appear in the source, they are simply added to the end of the target's argument list.
     * @param collectorToTarget the target to which the arguments will be added.
     * @param newArgSet the arguments which are retargeted to the target collector.
     *   Must not be empty.
     */
    private void addTargetArgsInSourceOrder(CollectorGem collectorToTarget, Set<PartInput> newArgSet) {

        // Get the arguments in the order returned by the CALSourceGenerator.
        CompositionNode.CompositionArgument[] argsFromSourceGen = CALSourceGenerator.getFunctionArguments(collectorToTarget);
        CompositionNode.CompositionArgument firstArg = newArgSet.iterator().next();
       
        // Find where the first argument appears.
        int firstArgPosition = Arrays.asList(argsFromSourceGen).indexOf(firstArg);
        if (firstArgPosition < 0) {
            // The args don't appear in the source.  Add to the end.
            collectorToTarget.addArguments(newArgSet);

        } else if (firstArgPosition == 0) {
            // Add to the beginning
            collectorToTarget.addArguments(0, newArgSet);

        } else {
            // Add after the argument which comes before it in source gen.
            List<PartInput> targetedCollectorArgList = collectorToTarget.getTargetArguments();
            PartInput argBefore = (PartInput)argsFromSourceGen[firstArgPosition - 1];
           
            if (targetedCollectorArgList.contains(argBefore)) {
                collectorToTarget.addArguments(argBefore, newArgSet, true);
            } else {
                // The "argBefore" doesn't appear in the list if it's an unaccounted-for arg.
                collectorToTarget.addArguments(newArgSet);
            }
        }
    }

    /**
     * Update the gem graph to reflect any changes in the inputs targeting the given collectors.
     * This includes updating their reflected inputs, updating their reflectors, and propagating new targeting changes
     *   which might come about from newly-created reflector inputs.
     * eg. If a reflector update would cause an argument to be added to a collector gem, that collector's reflectors
     *     will be updated as well (and so on..).
     *
     * @param affectedCollectors the collectors for which reflectors will be updated.
     */
    void updateForArgumentChange(Set<CollectorGem> affectedCollectors) {
       
        if (disableArgumentUpdating) {
            return;
        }

        for (final CollectorGem affectedCollector : affectedCollectors) {
            Map<ReflectorGem, PartInput[]> affectedReflectorsToOldInputsMap = affectedCollector.updateReflectedInputs();
           
            // Take care of updating arguments for collectors affected by additional reflector inputs.
            // Note that an infinite loop shouldn't occur, since new reflector inputs would be targeted at the target gem.
            for (final Map.Entry<ReflectorGem, PartInput[]> mapEntry : affectedReflectorsToOldInputsMap.entrySet()) {
                ReflectorGem reflectorGem = mapEntry.getKey();
                retargetArgumentsForDefinitionChange(reflectorGem, mapEntry.getValue());
            }
        }
    }

}
TOP

Related Classes of org.openquark.gems.client.GemGraphArgumentManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.