meyfa/structogram2byob

View on GitHub
src/main/java/structogram2byob/program/ProgramUnit.java

Summary

Maintainability
A
35 mins
Test Coverage
A
98%
package structogram2byob.program;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import nsdlib.elements.NSDRoot;
import scratchlib.objects.ScratchObject;
import scratchlib.objects.fixed.collections.ScratchObjectAbstractCollection;
import scratchlib.objects.fixed.collections.ScratchObjectArray;
import scratchlib.objects.fixed.data.ScratchObjectString;
import scratchlib.objects.fixed.data.ScratchObjectSymbol;
import scratchlib.objects.fixed.data.ScratchObjectUtf8;
import scratchlib.objects.user.ScratchObjectVariableFrame;
import structogram2byob.ScratchType;
import structogram2byob.blocks.Block;
import structogram2byob.blocks.BlockDescription;
import structogram2byob.blocks.BlockRegistry;
import structogram2byob.blocks.special.ScriptVariablesBlock;
import structogram2byob.program.expressions.BlockExpression;
import structogram2byob.program.expressions.Expression;


/**
 * A "program unit" is either a script or an invokable method that is part of a
 * {@link Program} and can be converted to a Scratch array of its blocks.
 */
public class ProgramUnit
{
    private final NSDRoot element;
    private final UnitType type;
    private final BlockDescription description;
    private final List<BlockExpression> blocks;
    private final Block unitBlock;

    private final VariableContext thisContext = VariableContext.getForUnit(this);

    /**
     * Constructs a new unit from the given header description and the given blocks.
     *
     * @param element The element this unit was constructed from.
     * @param type The type of this unit.
     * @param description The unit description.
     * @param blocks The blocks that form the unit body.
     */
    public ProgramUnit(NSDRoot element, UnitType type, BlockDescription description,
            Collection<? extends BlockExpression> blocks)
    {
        this.element = element;

        this.type = type;
        this.description = description;
        this.blocks = Collections.unmodifiableList(new ArrayList<>(blocks));

        this.unitBlock = new UnitBlock(description, ScratchType.ANY);
    }

    /**
     * @return The element this unit was constructed from.
     */
    public NSDRoot getElement()
    {
        return element;
    }

    /**
     * @return Whether this unit is a script, command, reporter or predicate.
     */
    public UnitType getType()
    {
        return type;
    }

    /**
     * @return The blocks that form the unit body.
     */
    public List<BlockExpression> getBlocks()
    {
        return blocks;
    }

    /**
     * @return This unit's user spec, for conversion to a custom block.
     */
    public String getUserSpec()
    {
        return description.toUserSpec();
    }

    /**
     * @return A {@link Block} instance for invoking this as a custom block.
     */
    public Block getInvocationBlock()
    {
        return unitBlock;
    }

    /**
     * Converts this unit into an array of its blocks, given a map of variables
     * and a block registry to distinguish ambiguous parts.
     *
     * @param vars A map of variable names to {@link VariableContext}s.
     * @param blocks The available blocks, including all custom blocks.
     * @return A {@link ScratchObject}.
     *
     * @throws ScratchConversionException When the conversion fails.
     */
    public ScratchObjectAbstractCollection toScratch(VariableMap vars, BlockRegistry blocks)
            throws ScratchConversionException
    {
        ScratchObjectArray arr = new ScratchObjectArray();

        // add parameters to available variables
        vars = vars.combine(getParameterVariables());

        if (type == UnitType.SCRIPT) {
            // create hat block
            Block hat = blocks.lookup(description);
            if (hat == null) {
                throw new ScratchConversionException(element, "unknown block: " + description);
            }
            arr.add(hat.toScratch(Collections.emptyList(), vars, blocks));
        }

        // add script body
        for (BlockExpression block : this.blocks) {
            ScratchObject obj = block.toScratch(vars, blocks);
            arr.add(obj);

            // check whether the block is a "script variables" block
            if (ScriptVariablesBlock.instance.getDescription().isAssignableFrom(block.getDescription())) {
                // extend available variables
                vars = vars.combine(retrieveScriptVariables(block, obj));
            }
        }

        return arr;
    }

    /**
     * Obtains this unit's parameters as a variable map for use in script
     * serialization.
     *
     * @return A variable map.
     */
    private VariableMap getParameterVariables()
    {
        Map<String, VariableContext> variables = new HashMap<>();
        for (int i = 0, n = description.countParts(); i < n; ++i) {
            if (description.isParameter(i)) {
                variables.put(description.getLabel(i), thisContext);
            }
        }
        return new VariableMap(variables);
    }

    /**
     * Given a "script variables" block and its object representation, retrieves
     * all the variable definitions.
     *
     * @param block The "script variables" block.
     * @param obj The block's Scratch object conversion result.
     * @return A variable map.
     */
    private VariableMap retrieveScriptVariables(BlockExpression block, ScratchObject obj)
    {
        Map<String, VariableContext> variables = new HashMap<>();

        // retrieve the frames for context construction
        Map<String, ScratchObjectVariableFrame> frames = ScriptVariablesBlock.retrieveFrames((ScratchObjectArray) obj);

        for (Expression param : block.getParameters()) {
            // get the variable name and corresponding frame
            BlockExpression pblock = (BlockExpression) param;
            String varName = pblock.getDescription().getLabel(0);
            ScratchObjectVariableFrame frame = frames.get(varName);

            variables.put(varName, VariableContext.getForScript(frame));
        }

        return new VariableMap(variables);
    }

    /**
     * Specifies a unit invocation block.
     */
    private static class UnitBlock extends Block
    {
        public UnitBlock(BlockDescription desc, ScratchType returnValue)
        {
            super(desc, returnValue);
        }

        @Override
        public ScratchObjectArray toScratch(List<Expression> params, VariableMap vars, BlockRegistry blocks)
                throws ScratchConversionException
        {
            ScratchObjectArray a = new ScratchObjectArray();

            a.add(new ScratchObjectSymbol("byob"));
            a.add(new ScratchObjectString(""));
            a.add(new ScratchObjectSymbol("doCustomBlock"));

            a.add(new ScratchObjectUtf8(getDescription().toUserSpec()));

            for (Expression param : params) {
                a.add(param.toScratch(vars, blocks));
            }

            return a;
        }
    }
}