const EventEmitter = require('events');

const Blocks = require('./blocks');
const Variable = require('../engine/variable');
const Comment = require('../engine/comment');
const uid = require('../util/uid');
const {Map} = require('immutable');
const log = require('../util/log');
const StringUtil = require('../util/string-util');
const VariableUtil = require('../util/variable-util');

/**
 * @fileoverview
 * A Target is an abstract "code-running" object for the Scratch VM.
 * Examples include sprites/clones or potentially physical-world devices.
 */

class Target extends EventEmitter {

    /**
     * @param {Runtime} runtime Reference to the runtime.
     * @param {?Blocks} blocks Blocks instance for the blocks owned by this target.
     * @constructor
     */
    constructor (runtime, blocks) {
        super();

        if (!blocks) {
            blocks = new Blocks(runtime);
        }

        /**
         * Reference to the runtime.
         * @type {Runtime}
         */
        this.runtime = runtime;
        /**
         * A unique ID for this target.
         * @type {string}
         */
        this.id = uid();
        /**
         * Blocks run as code for this target.
         * @type {!Blocks}
         */
        this.blocks = blocks;
        /**
         * Dictionary of variables and their values for this target.
         * Key is the variable id.
         * @type {Object.<string,*>}
         */
        this.variables = {};
        /**
         * Dictionary of comments for this target.
         * Key is the comment id.
         * @type {Object.<string,*>}
         */
        this.comments = {};
        /**
         * Dictionary of custom state for this target.
         * This can be used to store target-specific custom state for blocks which need it.
         * TODO: do we want to persist this in SB3 files?
         * @type {Object.<string,*>}
         */
        this._customState = {};

        /**
         * Currently known values for edge-activated hats.
         * Keys are block ID for the hat; values are the currently known values.
         * @type {Object.<string, *>}
         */
        this._edgeActivatedHatValues = {};
    }

    /**
     * Called when the project receives a "green flag."
     * @abstract
     */
    onGreenFlag () {}

    /**
     * Return a human-readable name for this target.
     * Target implementations should override this.
     * @abstract
     * @returns {string} Human-readable name for the target.
     */
    getName () {
        return this.id;
    }

    /**
     * Update an edge-activated hat block value.
     * @param {!string} blockId ID of hat to store value for.
     * @param {*} newValue Value to store for edge-activated hat.
     * @return {*} The old value for the edge-activated hat.
     */
    updateEdgeActivatedValue (blockId, newValue) {
        const oldValue = this._edgeActivatedHatValues[blockId];
        this._edgeActivatedHatValues[blockId] = newValue;
        return oldValue;
    }

    hasEdgeActivatedValue (blockId) {
        return this._edgeActivatedHatValues.hasOwnProperty(blockId);
    }

    /**
     * Clear all edge-activaed hat values.
     */
    clearEdgeActivatedValues () {
        this._edgeActivatedHatValues = {};
    }

    /**
     * Look up a variable object, first by id, and then by name if the id is not found.
     * Create a new variable if both lookups fail.
     * @param {string} id Id of the variable.
     * @param {string} name Name of the variable.
     * @return {!Variable} Variable object.
     */
    lookupOrCreateVariable (id, name) {
        let variable = this.lookupVariableById(id);
        if (variable) return variable;

        variable = this.lookupVariableByNameAndType(name, Variable.SCALAR_TYPE);
        if (variable) return variable;

        // No variable with this name exists - create it locally.
        const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false);
        this.variables[id] = newVariable;
        return newVariable;
    }

    /**
     * Look up a broadcast message object with the given id and return it
     * if it exists.
     * @param {string} id Id of the variable.
     * @param {string} name Name of the variable.
     * @return {?Variable} Variable object.
     */
    lookupBroadcastMsg (id, name) {
        let broadcastMsg;
        if (id) {
            broadcastMsg = this.lookupVariableById(id);
        } else if (name) {
            broadcastMsg = this.lookupBroadcastByInputValue(name);
        } else {
            log.error('Cannot find broadcast message if neither id nor name are provided.');
        }
        if (broadcastMsg) {
            if (name && (broadcastMsg.name.toLowerCase() !== name.toLowerCase())) {
                log.error(`Found broadcast message with id: ${id}, but` +
                    `its name, ${broadcastMsg.name} did not match expected name ${name}.`);
            }
            if (broadcastMsg.type !== Variable.BROADCAST_MESSAGE_TYPE) {
                log.error(`Found variable with id: ${id}, but its type ${broadcastMsg.type}` +
                    `did not match expected type ${Variable.BROADCAST_MESSAGE_TYPE}`);
            }
            return broadcastMsg;
        }
    }

    /**
     * Look up a broadcast message with the given name and return the variable
     * if it exists. Does not create a new broadcast message variable if
     * it doesn't exist.
     * @param {string} name Name of the variable.
     * @return {?Variable} Variable object.
     */
    lookupBroadcastByInputValue (name) {
        const vars = this.variables;
        for (const propName in vars) {
            if ((vars[propName].type === Variable.BROADCAST_MESSAGE_TYPE) &&
                (vars[propName].name.toLowerCase() === name.toLowerCase())) {
                return vars[propName];
            }
        }
    }

    /**
     * Look up a variable object.
     * Search begins for local variables; then look for globals.
     * @param {string} id Id of the variable.
     * @param {string} name Name of the variable.
     * @return {!Variable} Variable object.
     */
    lookupVariableById (id) {
        // If we have a local copy, return it.
        if (this.variables.hasOwnProperty(id)) {
            return this.variables[id];
        }
        // If the stage has a global copy, return it.
        if (this.runtime && !this.isStage) {
            const stage = this.runtime.getTargetForStage();
            if (stage && stage.variables.hasOwnProperty(id)) {
                return stage.variables[id];
            }
        }
    }

    /**
     * Look up a variable object by its name and variable type.
     * Search begins with local variables; then global variables if a local one
     * was not found.
     * @param {string} name Name of the variable.
     * @param {string} type Type of the variable. Defaults to Variable.SCALAR_TYPE.
     * @param {?bool} skipStage Optional flag to skip checking the stage
     * @return {?Variable} Variable object if found, or null if not.
     */
    lookupVariableByNameAndType (name, type, skipStage) {
        if (typeof name !== 'string') return;
        if (typeof type !== 'string') type = Variable.SCALAR_TYPE;
        skipStage = skipStage || false;

        for (const varId in this.variables) {
            const currVar = this.variables[varId];
            if (currVar.name === name && currVar.type === type) {
                return currVar;
            }
        }

        if (!skipStage && this.runtime && !this.isStage) {
            const stage = this.runtime.getTargetForStage();
            if (stage) {
                for (const varId in stage.variables) {
                    const currVar = stage.variables[varId];
                    if (currVar.name === name && currVar.type === type) {
                        return currVar;
                    }
                }
            }
        }

        return null;
    }

    /**
    * Look up a list object for this target, and create it if one doesn't exist.
    * Search begins for local lists; then look for globals.
    * @param {!string} id Id of the list.
    * @param {!string} name Name of the list.
    * @return {!Varible} Variable object representing the found/created list.
     */
    lookupOrCreateList (id, name) {
        let list = this.lookupVariableById(id);
        if (list) return list;

        list = this.lookupVariableByNameAndType(name, Variable.LIST_TYPE);
        if (list) return list;

        // No variable with this name exists - create it locally.
        const newList = new Variable(id, name, Variable.LIST_TYPE, false);
        this.variables[id] = newList;
        return newList;
    }

    /**
     * Creates a variable with the given id and name and adds it to the
     * dictionary of variables.
     * @param {string} id Id of variable
     * @param {string} name Name of variable.
     * @param {string} type Type of variable, '', 'broadcast_msg', or 'list'
     * @param {boolean} isCloud Whether the variable to create has the isCloud flag set.
     * Additional checks are made that the variable can be created as a cloud variable.
     */
    createVariable (id, name, type, isCloud) {
        if (!this.variables.hasOwnProperty(id)) {
            const newVariable = new Variable(id, name, type, false);
            if (isCloud && this.isStage && this.runtime.canAddCloudVariable()) {
                newVariable.isCloud = true;
                this.runtime.addCloudVariable();
                this.runtime.ioDevices.cloud.requestCreateVariable(newVariable);
            }
            this.variables[id] = newVariable;
        }
    }

    /**
     * Creates a comment with the given properties.
     * @param {string} id Id of the comment.
     * @param {string} blockId Optional id of the block the comment is attached
     * to if it is a block comment.
     * @param {string} text The text the comment contains.
     * @param {number} x The x coordinate of the comment on the workspace.
     * @param {number} y The y coordinate of the comment on the workspace.
     * @param {number} width The width of the comment when it is full size
     * @param {number} height The height of the comment when it is full size
     * @param {boolean} minimized Whether the comment is minimized.
     */
    createComment (id, blockId, text, x, y, width, height, minimized) {
        if (!this.comments.hasOwnProperty(id)) {
            const newComment = new Comment(id, text, x, y,
                width, height, minimized);
            if (blockId) {
                newComment.blockId = blockId;
                const blockWithComment = this.blocks.getBlock(blockId);
                if (blockWithComment) {
                    blockWithComment.comment = id;
                } else {
                    log.warn(`Could not find block with id ${blockId
                    } associated with commentId: ${id}`);
                }
            }
            this.comments[id] = newComment;
        }
    }

    /**
     * Renames the variable with the given id to newName.
     * @param {string} id Id of variable to rename.
     * @param {string} newName New name for the variable.
     */
    renameVariable (id, newName) {
        if (this.variables.hasOwnProperty(id)) {
            const variable = this.variables[id];
            if (variable.id === id) {
                const oldName = variable.name;
                variable.name = newName;

                if (this.runtime) {
                    if (variable.isCloud && this.isStage) {
                        this.runtime.ioDevices.cloud.requestRenameVariable(oldName, newName);
                    }

                    if (variable.type === Variable.SCALAR_TYPE) {
                        // sensing__of may be referencing to this variable.
                        // Change the reference.
                        let blockUpdated = false;
                        this.runtime.targets.forEach(t => {
                            blockUpdated = t.blocks.updateSensingOfReference(
                                oldName,
                                newName,
                                this.isStage ? '_stage_' : this.getName()
                            ) || blockUpdated;
                        });
                        // Request workspace change only if sensing_of blocks were actually updated.
                        if (blockUpdated) this.runtime.requestBlocksUpdate();
                    }

                    const blocks = this.runtime.monitorBlocks;
                    blocks.changeBlock({
                        id: id,
                        element: 'field',
                        name: variable.type === Variable.LIST_TYPE ? 'LIST' : 'VARIABLE',
                        value: id
                    }, this.runtime);
                    const monitorBlock = blocks.getBlock(variable.id);
                    if (monitorBlock) {
                        this.runtime.requestUpdateMonitor(Map({
                            id: id,
                            params: blocks._getBlockParams(monitorBlock)
                        }));
                    }
                }

            }
        }
    }

    /**
     * Removes the variable with the given id from the dictionary of variables.
     * @param {string} id Id of variable to delete.
     */
    deleteVariable (id) {
        if (this.variables.hasOwnProperty(id)) {
            // Get info about the variable before deleting it
            const deletedVariableName = this.variables[id].name;
            const deletedVariableWasCloud = this.variables[id].isCloud;
            delete this.variables[id];
            if (this.runtime) {
                if (deletedVariableWasCloud && this.isStage) {
                    this.runtime.ioDevices.cloud.requestDeleteVariable(deletedVariableName);
                    this.runtime.removeCloudVariable();
                }
                this.runtime.monitorBlocks.deleteBlock(id);
                this.runtime.requestRemoveMonitor(id);
            }
        }
    }

    /**
     * Remove this target's monitors from the runtime state and remove the
     * target-specific monitored blocks (e.g. local variables, global variables for the stage, x-position).
     * NOTE: This does not delete any of the stage monitors like backdrop name.
     */
    deleteMonitors () {
        this.runtime.requestRemoveMonitorByTargetId(this.id);
        let targetSpecificMonitorBlockIds;
        if (this.isStage) {
            // This only deletes global variables and not other stage monitors like backdrop number.
            targetSpecificMonitorBlockIds = Object.keys(this.variables);
        } else {
            targetSpecificMonitorBlockIds = Object.keys(this.runtime.monitorBlocks._blocks)
                .filter(key => this.runtime.monitorBlocks._blocks[key].targetId === this.id);
        }
        for (const blockId of targetSpecificMonitorBlockIds) {
            this.runtime.monitorBlocks.deleteBlock(blockId);
        }
    }

    /**
     * Create a clone of the variable with the given id from the dictionary of
     * this target's variables.
     * @param {string} id Id of variable to duplicate.
     * @param {boolean=} optKeepOriginalId Optional flag to keep the original variable ID
     * for the duplicate variable. This is necessary when cloning a sprite, for example.
     * @return {?Variable} The duplicated variable, or null if
     * the original variable was not found.
     */
    duplicateVariable (id, optKeepOriginalId) {
        if (this.variables.hasOwnProperty(id)) {
            const originalVariable = this.variables[id];
            const newVariable = new Variable(
                optKeepOriginalId ? id : null, // conditionally keep original id or generate a new one
                originalVariable.name,
                originalVariable.type,
                originalVariable.isCloud
            );
            if (newVariable.type === Variable.LIST_TYPE) {
                newVariable.value = originalVariable.value.slice(0);
            } else {
                newVariable.value = originalVariable.value;
            }
            return newVariable;
        }
        return null;
    }

    /**
     * Duplicate the dictionary of this target's variables as part of duplicating.
     * this target or making a clone.
     * @param {object=} optBlocks Optional block container for the target being duplicated.
     * If provided, new variables will be generated with new UIDs and any variable references
     * in this blocks container will be updated to refer to the corresponding new IDs.
     * @return {object} The duplicated dictionary of variables
     */
    duplicateVariables (optBlocks) {
        let allVarRefs;
        if (optBlocks) {
            allVarRefs = optBlocks.getAllVariableAndListReferences();
        }
        return Object.keys(this.variables).reduce((accum, varId) => {
            const newVariable = this.duplicateVariable(varId, !optBlocks);
            accum[newVariable.id] = newVariable;
            if (optBlocks && allVarRefs) {
                const currVarRefs = allVarRefs[varId];
                if (currVarRefs) {
                    this.mergeVariables(varId, newVariable.id, currVarRefs);
                }
            }
            return accum;
        }, {});
    }

    /**
     * Post/edit sprite info.
     * @param {object} data An object with sprite info data to set.
     * @abstract
     */
    postSpriteInfo () {}

    /**
     * Retrieve custom state associated with this target and the provided state ID.
     * @param {string} stateId - specify which piece of state to retrieve.
     * @returns {*} the associated state, if any was found.
     */
    getCustomState (stateId) {
        return this._customState[stateId];
    }

    /**
     * Store custom state associated with this target and the provided state ID.
     * @param {string} stateId - specify which piece of state to store on this target.
     * @param {*} newValue - the state value to store.
     */
    setCustomState (stateId, newValue) {
        this._customState[stateId] = newValue;
    }

    /**
     * Call to destroy a target.
     * @abstract
     */
    dispose () {
        this._customState = {};

        if (this.runtime) {
            this.runtime.removeExecutable(this);
        }
    }

    // Variable Conflict Resolution Helpers

    /**
     * Get the names of all the variables of the given type that are in scope for this target.
     * For targets that are not the stage, this includes any target-specific
     * variables as well as any stage variables unless the skipStage flag is true.
     * For the stage, this is all stage variables.
     * @param {string} type The variable type to search for; defaults to Variable.SCALAR_TYPE
     * @param {?bool} skipStage Optional flag to skip the stage.
     * @return {Array<string>} A list of variable names
     */
    getAllVariableNamesInScopeByType (type, skipStage) {
        if (typeof type !== 'string') type = Variable.SCALAR_TYPE;
        skipStage = skipStage || false;
        const targetVariables = Object.values(this.variables)
            .filter(v => v.type === type)
            .map(variable => variable.name);
        if (skipStage || this.isStage || !this.runtime) {
            return targetVariables;
        }
        const stage = this.runtime.getTargetForStage();
        const stageVariables = stage.getAllVariableNamesInScopeByType(type);
        return targetVariables.concat(stageVariables);
    }

    /**
     * Merge variable references with another variable.
     * @param {string} idToBeMerged ID of the variable whose references need to be updated
     * @param {string} idToMergeWith ID of the variable that the old references should be replaced with
     * @param {?Array<Object>} optReferencesToUpdate Optional context of the change.
     * Defaults to all the blocks in this target.
     * @param {?string} optNewName New variable name to merge with. The old
     * variable name in the references being updated should be replaced with this new name.
     * If this parameter is not provided or is '', no name change occurs.
     */
    mergeVariables (idToBeMerged, idToMergeWith, optReferencesToUpdate, optNewName) {
        const referencesToChange = optReferencesToUpdate ||
            // TODO should there be a separate helper function that traverses the blocks
            // for all references for a given ID instead of doing the below..?
            this.blocks.getAllVariableAndListReferences()[idToBeMerged];

        VariableUtil.updateVariableIdentifiers(referencesToChange, idToMergeWith, optNewName);
    }

    /**
     * Share a local variable (and given references for that variable) to the stage.
     * @param {string} varId The ID of the variable to share.
     * @param {Array<object>} varRefs The list of variable references being shared,
     * that reference the given variable ID. The names and IDs of these variable
     * references will be updated to refer to the new (or pre-existing) global variable.
     */
    shareLocalVariableToStage (varId, varRefs) {
        if (!this.runtime) return;
        const variable = this.variables[varId];
        if (!variable) {
            log.warn(`Cannot share a local variable to the stage if it's not local.`);
            return;
        }
        const stage = this.runtime.getTargetForStage();
        // If a local var is being shared with the stage,
        // sharing will make the variable global, resulting in a conflict
        // with the existing local variable. Preemptively Resolve this conflict
        // by renaming the new global variable.

        // First check if we've already done the local to global transition for this
        // variable. If we have, merge it with the global variable we've already created.
        const varIdForStage = `StageVarFromLocal_${varId}`;
        let stageVar = stage.lookupVariableById(varIdForStage);
        // If a global var doesn't already exist, create a new one with a fresh name.
        // Use the ID we created above so that we can lookup this new variable in the
        // future if we decide to share this same variable again.
        if (!stageVar) {
            const varName = variable.name;
            const varType = variable.type;

            const newStageName = `Stage: ${varName}`;
            stageVar = this.runtime.createNewGlobalVariable(newStageName, varIdForStage, varType);
        }
        // Update all variable references to use the new name and ID
        this.mergeVariables(varId, stageVar.id, varRefs, stageVar.name);
    }

    /**
     * Share a local variable with a sprite, merging with one of the same name and
     * type if it already exists on the sprite, or create a new one.
     * @param {string} varId Id of the variable to share
     * @param {Target} sprite The sprite to share the variable with
     * @param {Array<object>} varRefs A list of all the variable references currently being shared.
     */
    shareLocalVariableToSprite (varId, sprite, varRefs) {
        if (!this.runtime) return;
        if (this.isStage) return;
        const variable = this.variables[varId];
        if (!variable) {
            log.warn(`Tried to call 'shareLocalVariableToSprite' with a non-local variable.`);
            return;
        }
        const varName = variable.name;
        const varType = variable.type;
        // Check if the receiving sprite already has a variable of the same name and type
        // and use the existing variable, otherwise create a new one.
        const existingLocalVar = sprite.lookupVariableByNameAndType(varName, varType);
        let newVarId;
        if (existingLocalVar) {
            newVarId = existingLocalVar.id;
        } else {
            const newVar = new Variable(null, varName, varType);
            newVarId = newVar.id;
            sprite.variables[newVarId] = newVar;
        }

        // Merge with the local variable on the new sprite.
        this.mergeVariables(varId, newVarId, varRefs);
    }

    /**
     * Given a list of variable referencing fields, shares those variables with
     * the target with the provided id, resolving any variable conflicts that arise
     * using the following rules:
     *
     * If this target is the stage, exit. There are no conflicts that arise
     * from sharing variables from the stage to another sprite. The variables
     * already exist globally, so no further action is needed.
     *
     * If a variable being referenced is a global variable, do nothing. The
     * global variable already exists so no further action is needed.
     *
     * If a variable being referenced is local, and
     * 1) The receiving target is a sprite:
     * create a new local variable or merge with an existing local variable
     * of the same name and type. Update all the referencing fields
     * for the original variable to reference the new variable.
     * 2) The receiving target is the stage:
     * Create a new global variable with a fresh name and update all the referencing
     * fields to reference the new variable.
     *
     * @param {Array<object>} blocks The blocks containing
     * potential conflicting references to variables.
     * @param {Target} receivingTarget The target receiving the variables
     */
    resolveVariableSharingConflictsWithTarget (blocks, receivingTarget) {
        if (this.isStage) return;

        // Get all the variable references in the given list of blocks
        const allVarListRefs = this.blocks.getAllVariableAndListReferences(blocks);

        // For all the variables being referenced, check for which ones are local
        // to this target, and resolve conflicts based on whether the receiving target
        // is a sprite (with a conflicting local variable) or whether it is
        // the stage (which cannot have local variables)
        for (const varId in allVarListRefs) {
            const currVar = this.variables[varId];
            if (!currVar) continue; // The current variable is global, there shouldn't be any conflicts here, skip it.

            // Get the list of references for the current variable id
            const currVarListRefs = allVarListRefs[varId];

            if (receivingTarget.isStage) {
                this.shareLocalVariableToStage(varId, currVarListRefs);
            } else {
                this.shareLocalVariableToSprite(varId, receivingTarget, currVarListRefs);
            }
        }
    }

    /**
     * Fixes up variable references in this target avoiding conflicts with
     * pre-existing variables in the same scope.
     * This is used when uploading this target as a new sprite into an existing
     * project, where the new sprite may contain references
     * to variable names that already exist as global variables in the project
     * (and thus are in scope for variable references in the given sprite).
     *
     * If this target has a block that references an existing global variable and that
     * variable *does not* exist in this target (e.g. it was a global variable in the
     * project the sprite was originally exported from), merge the variables. This entails
     * fixing the variable references in this sprite to reference the id of the pre-existing global variable.
     *
     * If this target has a block that references an existing global variable and that
     * variable does exist in the target itself (e.g. it's a local variable in the sprite being uploaded),
     * then the local variable is renamed to distinguish itself from the pre-existing variable.
     * All blocks that reference the local variable will be updated to use the new name.
     */
    // TODO (#1360) This function is too long, add some helpers for the different chunks and cases...
    fixUpVariableReferences () {
        if (!this.runtime) return; // There's no runtime context to conflict with
        if (this.isStage) return; // Stage can't have variable conflicts with itself (and also can't be uploaded)
        const stage = this.runtime.getTargetForStage();
        if (!stage || !stage.variables) return;

        const renameConflictingLocalVar = (id, name, type) => {
            const conflict = stage.lookupVariableByNameAndType(name, type);
            if (conflict) {
                const newName = StringUtil.unusedName(
                    `${this.getName()}: ${name}`,
                    this.getAllVariableNamesInScopeByType(type));
                this.renameVariable(id, newName);
                return newName;
            }
            return null;
        };

        const allReferences = this.blocks.getAllVariableAndListReferences();
        const unreferencedLocalVarIds = [];
        if (Object.keys(this.variables).length > 0) {
            for (const localVarId in this.variables) {
                if (!this.variables.hasOwnProperty(localVarId)) continue;
                if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId);
            }
        }
        const conflictIdsToReplace = Object.create(null);
        const conflictNamesToReplace = Object.create(null);

        // Cache the list of all variable names by type so that we don't need to
        // re-calculate this in every iteration of the following loop.
        const varNamesByType = {};
        const allVarNames = type => {
            const namesOfType = varNamesByType[type];
            if (namesOfType) return namesOfType;
            varNamesByType[type] = this.runtime.getAllVarNamesOfType(type);
            return varNamesByType[type];
        };

        for (const varId in allReferences) {
            // We don't care about which var ref we get, they should all have the same var info
            const varRef = allReferences[varId][0];
            const varName = varRef.referencingField.value;
            const varType = varRef.type;
            if (this.lookupVariableById(varId)) {
                // Found a variable with the id in either the target or the stage,
                // figure out which one.
                if (this.variables.hasOwnProperty(varId)) {
                    // If the target has the variable, then check whether the stage
                    // has one with the same name and type. If it does, then rename
                    // this target specific variable so that there is a distinction.
                    const newVarName = renameConflictingLocalVar(varId, varName, varType);

                    if (newVarName) {
                        // We are not calling this.blocks.updateBlocksAfterVarRename
                        // here because it will search through all the blocks. We already
                        // have access to all the references for this var id.
                        allReferences[varId].map(ref => {
                            ref.referencingField.value = newVarName;
                            return ref;
                        });
                    }
                }
            } else {
                // We didn't find the referenced variable id anywhere,
                // Treat it as a reference to a global variable (from the original
                // project this sprite was exported from).
                // Check for whether a global variable of the same name and type exists,
                // and if so, track it to merge with the existing global in a second pass of the blocks.
                const existingVar = stage.lookupVariableByNameAndType(varName, varType);
                if (existingVar) {
                    if (!conflictIdsToReplace[varId]) {
                        conflictIdsToReplace[varId] = existingVar.id;
                    }
                } else {
                    // A global variable with the same name did not already exist,
                    // create a new one such that it does not conflict with any
                    // names of local variables of the same type.
                    const allNames = allVarNames(varType);
                    const freshName = StringUtil.unusedName(varName, allNames);
                    stage.createVariable(varId, freshName, varType);
                    if (!conflictNamesToReplace[varId]) {
                        conflictNamesToReplace[varId] = freshName;
                    }
                }
            }
        }
        // Rename any local variables that were missed above because they aren't
        // referenced by any blocks
        for (const id in unreferencedLocalVarIds) {
            const varId = unreferencedLocalVarIds[id];
            const name = this.variables[varId].name;
            const type = this.variables[varId].type;
            renameConflictingLocalVar(varId, name, type);
        }
        // Handle global var conflicts with existing global vars (e.g. a sprite is uploaded, and has
        // blocks referencing some variable that the sprite does not own, and this
        // variable conflicts with a global var)
        // In this case, we want to merge the new variable referenes with the
        // existing global variable
        for (const conflictId in conflictIdsToReplace) {
            const existingId = conflictIdsToReplace[conflictId];
            const referencesToUpdate = allReferences[conflictId];
            this.mergeVariables(conflictId, existingId, referencesToUpdate);
        }

        // Handle global var conflicts existing local vars (e.g a sprite is uploaded,
        // and has blocks referencing some variable that the sprite does not own, and this
        // variable conflcits with another sprite's local var).
        // In this case, we want to go through the variable references and update
        // the name of the variable in that reference.
        for (const conflictId in conflictNamesToReplace) {
            const newName = conflictNamesToReplace[conflictId];
            const referencesToUpdate = allReferences[conflictId];
            referencesToUpdate.map(ref => {
                ref.referencingField.value = newName;
                return ref;
            });
        }
    }

}

module.exports = Target;
