public class WorkspaceFragment extends Fragment {
    private static final String TAG = "WorkspaceFragment";

    private BlocklyController mController;
    private Workspace mWorkspace;
    private VirtualWorkspaceView mVirtualWorkspaceView;
    private WorkspaceView mWorkspaceView;

    private boolean mDrawGrid = true;
    private int mGridColor;
    private int mGridSpacing;
    private int mGridDotRadius;
    private int mBackgroundColor;

    @Override
    public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
        super.onInflate(context, attrs, savedInstanceState);
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
                R.styleable.WorkspaceFragment, 0, 0);
        try {
            mDrawGrid = a.getBoolean(R.styleable.WorkspaceFragment_drawGrid,
                    mDrawGrid);
            mGridColor = a.getInt(R.styleable.WorkspaceFragment_gridColor,
                    WorkspaceGridRenderer.DEFAULT_GRID_COLOR);
            mGridSpacing = a.getInt(R.styleable.WorkspaceFragment_gridSpacing,
                    WorkspaceGridRenderer.DEFAULT_GRID_SPACING);
            mGridDotRadius = a.getInt(R.styleable.WorkspaceFragment_gridDotRadius,
                    WorkspaceGridRenderer.DEFAULT_GRID_RADIUS);
            mBackgroundColor = a.getInt(R.styleable.WorkspaceFragment_backgroundColor,
                    WorkspaceGridRenderer.DEFAULT_BACKGROUND_COLOR);
        } finally {
            a.recycle();
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        final ViewGroup rootView =
                (ViewGroup) inflater.inflate(R.layout.fragment_workspace, container, false);

        mVirtualWorkspaceView =
                (VirtualWorkspaceView) rootView.findViewById(R.id.virtual_workspace);
        mWorkspaceView = (WorkspaceView) rootView.findViewById(R.id.workspace);
        configureWorkspaceLayout(mVirtualWorkspaceView);

        if (mController != null) {
            mVirtualWorkspaceView.setZoomBehavior(
                    mController.getWorkspaceHelper().getZoomBehavior());
        }
        mVirtualWorkspaceView.setDrawGrid(mDrawGrid);

        return rootView;
    }

    private void configureWorkspaceLayout(VirtualWorkspaceView virtualWorkspaceView) {
        virtualWorkspaceView.setGridColor(mGridColor);
        virtualWorkspaceView.setGridSpacing(mGridSpacing);
        virtualWorkspaceView.setGridDotRadius(mGridDotRadius);
        virtualWorkspaceView.setBackgroundColor(mBackgroundColor);
    }

    /**
     * Sets the controller to use in this fragment for instantiating views. This should be the same
     * controller used for any associated {@link FlyoutFragment FlyoutFragments}.
     *
     * @param controller The controller backing this fragment.
     */
    public void setController(BlocklyController controller) {
        if (controller == mController) {
            return; // no-op
        }

        mController = controller;
        mWorkspace = (controller == null) ? null : mController.getWorkspace();
        mController.initWorkspaceView(mWorkspaceView);

        if (mVirtualWorkspaceView != null) {
            mVirtualWorkspaceView.setZoomBehavior(
                    mController.getWorkspaceHelper().getZoomBehavior());
        }
    }

    /**
     * @return The workspace being used by this fragment.
     */
    public Workspace getWorkspace() {
        return mWorkspace;
    }

    /**
     * @return The {@link WorkspaceView} inside this fragment.
     */
    public WorkspaceView getWorkspaceView() {
        return mWorkspaceView;
    }
} public class Dragger {
    private static final String TAG = "Dragger";
    private static final boolean LOG_TOUCH_EVENTS = false;
    private static final boolean LOG_DRAG_EVENTS = false;

    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();

    // Modes of finishDragging()
    private static final int FINISH_BEHAVIOR_DROP = 1;
    private static final int FINISH_BEHAVIOR_REVERT = 2;
    private static final int FINISH_BEHAVIOR_DELETED = 3;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({FINISH_BEHAVIOR_DROP, FINISH_BEHAVIOR_REVERT, FINISH_BEHAVIOR_DELETED})
    public @interface FinishDragBehavior {}


    /**
     * Interface for processing a drag behavior.
     */
    // TODO(#233): Include clicks. Rename. BlockTouchHandler sounds good, but...
    public interface DragHandler {
        /**
         * This method checks whether the pending drag maps to a valid draggable {@link BlockGroup}
         * on the workspace.  If it does, it should return a {@link Runnable} that will perform (at
         * a later time) the necessary {@link Block} and {@link BlockView} manipulations to
         * construct that drag group, and assign it to the {@link PendingDrag}.  Such manipulations
         * must not occur immediately, because this can result in recursive touch events.  The
         * {@link Dragger} is designed to catch these calls and forcibly crash.  Just don't do it.
         * <p/>
         * When the {@link Runnable} is called, it should proceed with the {@code Block} and
         * {@code BlockView} manipulations, and call {@link PendingDrag#startDrag} to
         * assign the draggable {@link BlockGroup}, which must contain a root block on the
         * {@link Workspace} and be added to the {@link WorkspaceView}.
         * <p/>
         * If pending drag does not map to a draggable, this method should return null.
         *
         * @param pendingDrag The pending drag state in question.
         */
        @Nullable Runnable maybeGetDragGroupCreator(PendingDrag pendingDrag);

        /**
         * Handles click events on blocks.
         *
         * @param pendingDrag The pending drag state in question.
         * @return True if click was processed and event should be captured.
         */
        boolean onBlockClicked(PendingDrag pendingDrag);

        // TODO(#202): onDragCancel(BlockGroup dragGroup) to support invalid drop locations.
        //     For instance, returning a block to the trash. Currently drops at the last move
        //     location.
    }

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({DRAG_MODE_IMMEDIATE, DRAG_MODE_SLOPPY})
    public @interface DragMode {}
    @VisibleForTesting static final int DRAG_MODE_IMMEDIATE = 0;
    @VisibleForTesting static final int DRAG_MODE_SLOPPY = 1;

    private final ArrayList<Connection> mDraggedConnections = new ArrayList<>();
    // For use in bumping neighbours; instance variable only to avoid repeated allocation.
    private final ArrayList<Connection> mTempConnections = new ArrayList<>();
    // For use in getting location on screen.
    private final int[] mTempScreenCoord1 = new int[2];
    private final int[] mTempScreenCoord2 = new int[2];
    private final ViewPoint mTempViewPoint = new ViewPoint();
    private final WorkspacePoint mTempWorkspacePoint = new WorkspacePoint();

    private Handler mMainHandler;
    private final BlocklyController mController;
    private final WorkspaceHelper mViewHelper;
    private final BlockClipDataHelper mClipHelper;
    private final Workspace mWorkspace;
    private final ConnectionManager mConnectionManager;

    /**
     * This flags helps check {@link #onTouchBlockImpl} is not called recursively, which can occur
     * when the view hierarchy is manipulated during event handling.
     */
    private boolean mWithinOnTouchBlockImpl = false;

    private PendingDrag mPendingDrag;
    private Runnable mLogPending = (LOG_TOUCH_EVENTS || LOG_DRAG_EVENTS) ? new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, (mPendingDrag == null ? "\tnot pending" :
                    (mPendingDrag.getDragGroup()==null ?
                            "\tpending: touched = " + mPendingDrag.getTouchedBlockView()
                            : "\tdragging: " + mPendingDrag.getDragGroup())));
        }
    } : null;

    // Which {@link BlockView} was touched, and possibly may be being dragged.
    private WorkspaceView mWorkspaceView;
    private WeakReference<BlockView> mHighlightedBlockViewRef = new WeakReference<>(null);
    //The square of the required touch slop before starting a drag, precomputed to avoid
    // square root operations at runtime.
    private float mTouchSlopSquared = 0.0f;

    private View.OnDragListener mDragEventListener = new View.OnDragListener() {
        @Override
        public boolean onDrag(View workspaceView, DragEvent event) {
            final int action = event.getAction();

            if (LOG_DRAG_EVENTS) {
                String actionName = (action == DragEvent.ACTION_DRAG_STARTED) ? "DRAG_STARTED" :
                        (action == DragEvent.ACTION_DRAG_LOCATION) ? "DRAG_LOCATION" :
                        (action == DragEvent.ACTION_DRAG_ENDED) ? "DRAG_ENDED" :
                        (action == DragEvent.ACTION_DROP) ? "DROP" :
                        "UNKNOWN ACTION #" + action;
                Log.d(TAG, "onDrag: " + actionName + ", " + event);

                mMainHandler.removeCallbacks(mLogPending);  // Only log once per event tick
                mMainHandler.post(mLogPending);
            }

            // ClipDescription is always null on DRAG_ENDED.
            if (action != DragEvent.ACTION_DRAG_ENDED &&
                    !mClipHelper.isBlockData(event.getClipDescription()))
            {
                if (LOG_DRAG_EVENTS) {
                    Log.d(TAG, "onDrag: Not a block.");
                }
                return false;
            }

            if (mPendingDrag != null && mPendingDrag.isDragging()) {
                switch (action) {
                    case DragEvent.ACTION_DRAG_STARTED:
                        // Triggered in maybeStartDrag(..).
                        // The rest of the drag data is already captured in mPendingDrag.
                        // NOTE: This event position does not respect view scale.

                        BlockView rootDraggedBlockView = mPendingDrag.getRootDraggedBlockView();
                        if (rootDraggedBlockView.getBlock().isMovable()) {
                            BlockGroup dragGroup = mPendingDrag.getDragGroup();
                            if (dragGroup.getParent() == mWorkspaceView) {
                                // Hide the view on the workspace
                                mPendingDrag.getDragGroup().setVisibility(View.INVISIBLE);
                            }

                            // TODO(#35): This might be better described as "selected".
                            ((View) rootDraggedBlockView).setPressed(true);
                            return true;    // We want to keep listening for drag events
                        } else {
                            return false;   // We don't want to keep listening for drag events
                        }
                    case DragEvent.ACTION_DRAG_LOCATION:
                        // If we're still finishing up a previous drag we may have missed the
                        // start of the drag, in which case we shouldn't do anything.
                        continueDragging(event);
                        break;
                    case DragEvent.ACTION_DRAG_ENDED:
                        // TODO(#202): Cancel pending drag?
                        if (event.getResult()) {
                            break;
                        }
                        // Otherwise fall through
                    case DragEvent.ACTION_DROP:
                        // Finalize dragging and reset dragging state flags.
                        // These state flags are still used in the initial phase of figuring out if
                        // a drag has started.
                        maybeConnectDragGroup();
                        finishDragging(FINISH_BEHAVIOR_DROP);
                        return true;    // The drop succeeded.
                    default:
                        break;
                }
            }
            return false;   // In every case that gets here, the return value won't be checked.
        }
    };

    /**
     * @param blocklyController The {@link BlocklyController} managing Blocks in this activity.
     */
    public Dragger(BlocklyController blocklyController) {
        mController = blocklyController;
        mWorkspace = blocklyController.getWorkspace();
        mViewHelper = blocklyController.getWorkspaceHelper();
        mClipHelper = blocklyController.getClipDataHelper();
        mConnectionManager = mWorkspace.getConnectionManager();

        mMainHandler = new Handler();
    }

    /**
     * @param slop The required touch slop before starting a drag.
     */
    public void setTouchSlop(float slop) {
        mTouchSlopSquared = slop * slop;
    }

    /**
     * Remove all the connections in a blocks tree from the list of connections being dragged. This
     * is used when removing shadow blocks from a block tree during a drag. If there's no drag
     * in progress this has no effects.
     *
     * @param rootBlock The start of the block tree to remove connections for.
     */
    public void removeFromDraggingConnections(Block rootBlock) {
        if (mPendingDrag == null) {
            return;
        }
        mTempConnections.clear();
        rootBlock.getAllConnectionsRecursive(mTempConnections);
        for (int i = 0; i < mTempConnections.size(); i++) {
            Connection conn = mTempConnections.get(i);
            mDraggedConnections.remove(conn);
            conn.setDragMode(false);
        }
    }

    /**
     * Creates a BlockTouchHandler that will initiate a drag only after the user has dragged
     * beyond some touch threshold.
     *
     * @param dragHandler The {@link DragHandler} to handle gestures for the constructed
     *                    {@link BlockTouchHandler}.
     * @return A newly constructed {@link BlockTouchHandler}.
     */
    public BlockTouchHandler buildSloppyBlockTouchHandler(final DragHandler dragHandler) {
        return new BlockTouchHandler() {
            @Override
            public boolean onTouchBlock(BlockView blockView, MotionEvent motionEvent) {
                return onTouchBlockImpl(DRAG_MODE_SLOPPY, dragHandler, blockView, motionEvent,
                        /* interceptMode */ false);
            }

            @Override
            public boolean onInterceptTouchEvent(BlockView blockView, MotionEvent motionEvent) {
                // Intercepted move events might still be handled but the child view, such as
                // a drop down field.
                return onTouchBlockImpl(DRAG_MODE_SLOPPY, dragHandler, blockView, motionEvent,
                        /* interceptMode */ true);
            }
        };
    };

    /**
     * Creates a BlockTouchHandler that will initiate a drag as soon as the BlockView receives a
     * {@link MotionEvent} directly (not via interception).
     *
     * @param dragHandler The {@link DragHandler} to handle gestures for the constructed
     *                    {@link BlockTouchHandler}.
     * @return A newly constructed {@link BlockTouchHandler}.
     */
    public BlockTouchHandler buildImmediateDragBlockTouchHandler(final DragHandler dragHandler) {
        return new BlockTouchHandler() {
            @Override
            public boolean onTouchBlock(BlockView blockView, MotionEvent motionEvent) {
                return onTouchBlockImpl(DRAG_MODE_IMMEDIATE, dragHandler, blockView, motionEvent,
                        /* interceptMode */ false);
            }

            @Override
            public boolean onInterceptTouchEvent(BlockView blockView, MotionEvent motionEvent) {
                return onTouchBlockImpl(DRAG_MODE_IMMEDIATE, dragHandler, blockView, motionEvent,
                        /* interceptMode */ true);
            }
        };
    };

    /**
     * Continue dragging the currently moving block.  Called during ACTION_DRAG_LOCATION.
     *
     * @param event The next drag event to handle, as received by the {@link WorkspaceView}.
     */
    private void continueDragging(DragEvent event) {
        updateBlockPosition(event);

        // highlight as we go
        BlockView highlightedBlockView = mHighlightedBlockViewRef.get();
        if (highlightedBlockView != null) {
            highlightedBlockView.setHighlightedConnection(null);
        }
        Pair<Connection, Connection> connectionCandidate =
                findBestConnection(mPendingDrag.getRootDraggedBlock());
        if (connectionCandidate != null) {
            highlightedBlockView = mViewHelper.getView(connectionCandidate.second.getBlock());
            mHighlightedBlockViewRef = new WeakReference<>(highlightedBlockView);
            highlightedBlockView.setHighlightedConnection(connectionCandidate.second);
        }

        mPendingDrag.getDragGroup().requestLayout();
    }

    /**
     * Attempts to connect a dropped drag group with nearby connections
     */
    private void maybeConnectDragGroup() {
        Block dragRoot = mPendingDrag.getRootDraggedBlock();

        // Maybe snap to connections.
        Pair<Connection, Connection> connectionCandidate = findBestConnection(dragRoot);
        if (connectionCandidate != null) {
            mController.connect(connectionCandidate.first, connectionCandidate.second);
            // .connect(..) includes bumping block within snap distance of the new location.
        } else {
            // Even if no connection is found, still bump any neighbors within snap distance of the
            // new location.
            mController.bumpNeighbors(dragRoot);
        }
    }

    /**
     * Finish a drag gesture and clear pending drag info.  Called by event handlers for ACTION_UP,
     * ACTION_CANCEL, ACTION_DROP, and ACTION_DRAG_ENDED.
     */
    // TODO(305): Revert actions when behavior == FINISH_BEHAVIOR_REVERT
    private void finishDragging(@FinishDragBehavior int behavior) {
        if (behavior == FINISH_BEHAVIOR_DROP || behavior == FINISH_BEHAVIOR_REVERT) {
            // Update the drag group so that everything that has been changed will be properly
            // invalidated. Also, update the positions of all of the connections that were impacted
            // by the move and add them back to the manager. All of the connection locations will be
            // set relative to their block views immediately after this loop.  For now we just want
            // to unset drag mode and add the connections back to the list; 0, 0 is a cheap place to
            // put them.
            // Dragged connections may be empty, especially if the
            for (int i = 0; i < mDraggedConnections.size(); i++) {
                Connection cur = mDraggedConnections.get(i);
                cur.setPosition(0, 0);
                cur.setDragMode(false);
                mConnectionManager.addConnection(cur);
            }
        }
        mDraggedConnections.clear();

        BlockView highlightedBlockView = mHighlightedBlockViewRef.get();
        if (highlightedBlockView != null) {
            highlightedBlockView.setHighlightedConnection(null);
            mHighlightedBlockViewRef = new WeakReference<>(null);
        }

        if (mPendingDrag != null) {
            BlockGroup dragGroup = mPendingDrag.getDragGroup();
            if (dragGroup != null) {
                // Restore visibility at the end of the drag.
                dragGroup.setVisibility(View.VISIBLE);
            }


            BlockView blockView = mPendingDrag.getRootDraggedBlockView();
            if (blockView != null) {
                ((View) blockView).setPressed(false);
            } // else, trashing or similar manipulation made the view disappear.
            mPendingDrag = null;
        }
    }

    public void setWorkspaceView(WorkspaceView view) {
        mWorkspaceView = view;
    }

    /**
     * Let the Dragger know that a block was touched. This will be called when the block in the
     * workspace has been touched, but a drag has not yet been started.
     *
     * This method handles both regular touch events and intercepted touch events, with the latter
     * identified with the {@code interceptMode} parameter.  The only difference is that intercepted
     * events only return true (indicating they are handled) when a drag has been initiated. This
     * allows any underlying View, such as a field to handle the MotionEvent normally.
     *
     * @param dragMode The mode (immediate or sloppy) for handling this touch event.
     * @param dragHandler The {@link DragHandler} attached to this view.
     * @param touchedView The {@link BlockView} that detected a touch event.
     * @param event The touch event.
     * @param interceptMode When true forces all {@link MotionEvent#ACTION_MOVE} events
     *                                   that match {@link #mPendingDrag} to return true / handled.
     *                                   Otherwise, it only returns true if a drag is started.
     *
     * @return True if the event was handled by this touch implementation.
     */
    @VisibleForTesting
    boolean onTouchBlockImpl(@DragMode int dragMode, DragHandler dragHandler, BlockView touchedView,
                             MotionEvent event, boolean interceptMode) {
        if (mWithinOnTouchBlockImpl) {
            throw new IllegalStateException(
                    "onTouchBlockImpl() called recursively. Make sure OnDragHandler." +
                    "maybeGetDragGroupCreator() is not manipulating the View hierarchy.");
        }
        mWithinOnTouchBlockImpl = true;

        final int action = event.getActionMasked();

        boolean matchesPending = false;
        if (mPendingDrag != null) {
            matchesPending = mPendingDrag.isMatchAndProcessed(event, touchedView);
            if (!matchesPending && !mPendingDrag.isAlive()) {
                mPendingDrag = null;  // Was a part of previous gesture. Delete.
            }
        }

        if (LOG_TOUCH_EVENTS) {
            Log.d(TAG, "onTouchBlockImpl: "
                    + (dragMode == DRAG_MODE_IMMEDIATE ? "IMMEDIATE" : "SLOPPY")
                    + (interceptMode ? " intercept" : " direct")
                    + "\n\t" + event
                    + "\n\tMatches pending? " + matchesPending);

            mMainHandler.removeCallbacks(mLogPending);  // Only call once per event 'tick'
            mMainHandler.post(mLogPending);
        }

        final boolean result;
        if (action == MotionEvent.ACTION_DOWN ) {
            if (mPendingDrag == null) {
                mPendingDrag = new PendingDrag(mController, touchedView, event);
                if (interceptMode) {
                    // Do not handle intercepted down events. Allow child views (particularly
                    // fields) to handle the touch normally.
                    result = false;
                } else {
                    // The user touched the block directly.
                    if (dragMode == DRAG_MODE_IMMEDIATE) {
                        result = maybeStartDrag(dragHandler);
                    } else {
                        result = true;
                    }
                }
            } else if (matchesPending && !interceptMode) {
                // The Pending Drag was created during intercept, but the child did not handle it
                // and the event has bubbled down to here.
                if (dragMode == DRAG_MODE_IMMEDIATE) {
                    result = maybeStartDrag(dragHandler);
                } else {
                    result = true;
                }
            } else {
                result = false; // Pending drag already started with a different view / pointer id.
            }
        } else if (matchesPending) {
            // This touch is part of the current PendingDrag.
            if (action == MotionEvent.ACTION_MOVE) {
                if (mPendingDrag.isDragging()) {
                    result = false;  // We've already cancelled or started dragging.
                } else {
                    // Mark all direct move events as handled, but only intercepted events if they
                    // initiate a new drag.
                    boolean isDragGesture =
                            (!interceptMode && dragMode == DRAG_MODE_IMMEDIATE
                                    && event.getDownTime() > TAP_TIMEOUT)
                            || isBeyondSlopThreshold(event);
                    boolean isNewDrag = isDragGesture && maybeStartDrag(dragHandler);
                    result = isNewDrag || !interceptMode;
                }
            }
            // Handle the case when the user releases before moving far enough to start a drag.
            else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
                if (!mPendingDrag.isDragging()) {
                    if (!interceptMode && mPendingDrag.isClick()) {
                        dragHandler.onBlockClicked(mPendingDrag);
                    }
                    finishDragging(FINISH_BEHAVIOR_REVERT);
                }
                result = !interceptMode;
            } else {
                result = false; // Unrecognized event action
            }
        } else {
            result = false; // Doesn't match existing drag.
        }

        if (LOG_TOUCH_EVENTS) Log.d(TAG, "\treturn " + result);
        mWithinOnTouchBlockImpl = false;
        return result;
    }

    /**
     * @return The listener to use for {@link DragEvent}'s in the {@link WorkspaceView}.
     */
    public View.OnDragListener getDragEventListener() {
        return mDragEventListener;
    }

    /**
     * Checks whether {@code actionMove} is beyond the allowed slop (i.e., unintended) drag motion
     * distance.
     *
     * @param actionMove The {@link MotionEvent#ACTION_MOVE} event.
     * @return True if the motion is beyond the allowed slop threshold
     */
    private boolean isBeyondSlopThreshold(MotionEvent actionMove) {
        BlockView touchedView = mPendingDrag.getTouchedBlockView();

        // Not dragging yet - compute distance from Down event and start dragging if far enough.
        @Size(2) int[] touchDownLocation = mTempScreenCoord1;
        mPendingDrag.getTouchDownScreen(touchDownLocation);

        @Size(2) int[] curScreenLocation = mTempScreenCoord2;
        touchedView.getTouchLocationOnScreen(actionMove, curScreenLocation);

        final int deltaX = touchDownLocation[0] - curScreenLocation[0];
        final int deltaY = touchDownLocation[1] - curScreenLocation[1];

        // Dragged far enough to start a drag?
        return (deltaX * deltaX + deltaY * deltaY > mTouchSlopSquared);
    }

    /**
     * Handle motion events while starting to drag a block.  This keeps track of whether the block
     * has been dragged more than {@code mTouchSlop} and starts a drag if necessary. Once the drag
     * has been started, all following events will be handled through the {@link
     * #mDragEventListener}.
     */
    private boolean maybeStartDrag(DragHandler dragHandler) {
        // Check with the pending drag handler to select or create the dragged group.
        final PendingDrag pendingDrag = mPendingDrag;  // Stash for async callback
        final Runnable dragGroupCreator = dragHandler.maybeGetDragGroupCreator(pendingDrag);
        final boolean foundDragGroup = (dragGroupCreator != null);
        if (foundDragGroup) {
            mMainHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (mPendingDrag != null && mPendingDrag.isDragging()) {
                        return; // Ignore.  Probably being handled by a child view.
                    }

                    dragGroupCreator.run();
                    boolean dragStarted = pendingDrag.isDragging();
                    if (dragStarted) {
                        mPendingDrag = pendingDrag;
                        final BlockGroup dragGroup = mPendingDrag.getDragGroup();
                        ViewParent parent = dragGroup.getParent();
                        // TODO(#): Allow unparented BlockGroups (new blocks from Toolbox, etc.)
                        if (parent != mWorkspaceView) {
                            throw new IllegalStateException("dragGroup is root in WorkspaceView");
                        }

                        try {
                            ClipData clipData = mClipHelper.buildDragClipData(pendingDrag);

                            Block rootBlock = dragGroup.getFirstBlock();
                            removeDraggedConnectionsFromConnectionManager(rootBlock);
                            int flags = mViewHelper.getBlockViewFactory().getDragAndDropFlags();

                            ViewCompat.startDragAndDrop(
                                    dragGroup,
                                    clipData,
                                    new DragShadowBuilder(pendingDrag, mViewHelper),
                                    pendingDrag,
                                    flags);
                        } catch (IOException e) {
                            Log.w(TAG, "Serialization failed in ClipDataHelper.");
                            mPendingDrag = null;
                        }
                    } else {
                        mPendingDrag = null;
                    }
                }
            });
        }

        return foundDragGroup;
    }

    /**
     * Removes all connections of block and its descendants from the {@link }ConnectionManager}, so
     * these connections are not considered as potential connections when looking from connections
     * during dragging.
     *
     * @param block The root block of connections that should be removed.
     */
    private void removeDraggedConnectionsFromConnectionManager(Block block) {
        mDraggedConnections.clear();
        // Don't track any of the connections that we're dragging around.
        block.getAllConnectionsRecursive(mDraggedConnections);
        for (int i = 0; i < mDraggedConnections.size(); i++) {
            Connection conn = mDraggedConnections.get(i);
            mConnectionManager.removeConnection(conn);
            conn.setDragMode(true);
        }
    }

    /**
     * Move the currently dragged block in response to a new {@link MotionEvent}.
     * <p/>
     * All of the child blocks move with the root block based on its position during layout.
     *
     * @param event The {@link MotionEvent} to react to.
     */
    private void updateBlockPosition(DragEvent event) {
        // The event is relative to the WorkspaceView. Grab the pixel offset within.
        ViewPoint curDragLocationPixels = mTempViewPoint;
        curDragLocationPixels.set((int) event.getX(), (int) event.getY());
        WorkspacePoint curDragPositionWorkspace = mTempWorkspacePoint;
        mViewHelper.virtualViewToWorkspaceCoordinates(
                curDragLocationPixels, curDragPositionWorkspace);

        WorkspacePoint touchDownWorkspace = mPendingDrag.getTouchDownWorkspaceCoordinates();
        // Subtract original drag location from current location to get delta
        float workspaceDeltaX = curDragPositionWorkspace.x - touchDownWorkspace.x;
        float workspaceDeltaY = curDragPositionWorkspace.y - touchDownWorkspace.y;

        WorkspacePoint blockOrigPosition = mPendingDrag.getOriginalBlockPosition();
        mPendingDrag.getRootDraggedBlock().setPosition(blockOrigPosition.x + workspaceDeltaX,
                                                blockOrigPosition.y + workspaceDeltaY);
        mPendingDrag.getDragGroup().requestLayout();
    }

    private Pair<Connection, Connection> findBestConnection(Block block) {
        return mConnectionManager.findBestConnection(block, mViewHelper.getMaxSnapDistance());
    }

    private static class DragShadowBuilder extends View.DragShadowBuilder {
        private PendingDrag mPendingDrag;
        private int mSizeX, mSizeY;
        private float mZoomScale;

        DragShadowBuilder(PendingDrag pendingDrag, WorkspaceHelper helper) {
            super(pendingDrag.getDragGroup());
            mPendingDrag = pendingDrag;
            mZoomScale = helper.getWorkspaceZoomScale();

            BlockGroup dragGroup = pendingDrag.getDragGroup();
            mSizeX = dragGroup.getWidth();
            if (mSizeX == 0) {
                dragGroup.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
                dragGroup.layout(0, 0, dragGroup.getMeasuredWidth(), dragGroup.getMeasuredHeight());
                mSizeX = dragGroup.getWidth();
            }
            mSizeY = dragGroup.getHeight();
        }

        @Override
        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
            shadowSize.set(
                    (int) Math.ceil(mSizeX * mZoomScale),
                    (int) Math.ceil(mSizeY * mZoomScale));
            ViewPoint dragTouchOffset = mPendingDrag.getDragTouchOffset();
            shadowTouchPoint.set(
                    (int) Math.ceil(dragTouchOffset.x * mZoomScale),
                    (int) Math.ceil(dragTouchOffset.y * mZoomScale));
        }

        @Override
        public void onDrawShadow(Canvas canvas) {
            canvas.scale(mZoomScale, mZoomScale);
            super.onDrawShadow(canvas);
        }
    }
} public abstract class BlockViewFactory<BlockView extends com.google.blockly.android.ui.BlockView,
                                       InputView extends com.google.blockly.android.ui.InputView> {
    private static final String TAG = "BlockViewFactory";

    /**
     * Context for creating or loading views.
     */
    protected Context mContext;
    /**
     * Helper for doing conversions and style lookups.
     */
    protected WorkspaceHelper mHelper;
    /**
     * Name manager for the list of variables in this instance of Blockly.
     */
    protected NameManager mVariableNameManager;
    /**
     * The callback to use for views that can request changes to the list of variables.
     */
    protected VariableRequestCallback mVariableCallback;
    /**
     * The callback to use when something triggers showing or hiding a block's mutator.
     */
    protected MutatorToggleListener mMutatorListener;

    private SpinnerAdapter mVariableAdapter;
    private final Map<String, MutatorFragment.Factory> mMutatorUiFactories = new HashMap<>();

    // TODO(#137): Move to ViewPool class.
    protected final Map<String,WeakReference<BlockView>> mBlockIdToView
            = Collections.synchronizedMap(new HashMap<String, WeakReference<BlockView>>());

    protected BlockViewFactory(Context context, WorkspaceHelper helper) {
        mContext = context;
        mHelper = helper;
        helper.setBlockViewFactory(this);
    }

    /**
     * Sets the callback to use for variable view events, such as the user selected delete/rename.
     *
     * @param callback The callback to set on variable field views.
     */
    public void setVariableRequestCallback(VariableRequestCallback callback) {
        mVariableCallback = callback;
    }

    /**
     * Sets the listener to call when the user toggles a mutator. This is typically in response to
     * a mutator button being tapped on a block.
     *
     * @param listener The listener to call when a user toggles a mutator.
     */
    public void setMutatorToggleListener(MutatorToggleListener listener) {
        mMutatorListener = listener;
    }

    public WorkspaceHelper getWorkspaceHelper() {
        return mHelper;
    }

    /**
     * Set the {@link NameManager} being used to track variables in the workspace.
     *
     * @param variableNameManager The name manager for the variables in the associated workspace.
     */
    public void setVariableNameManager(NameManager variableNameManager) {
        mVariableNameManager = variableNameManager;
    }

    /**
     * Registers a {@link MutatorFragment.Factory} for the named mutator type. Mutators that have a
     * UI registered will display a user affordance for opening and closing the UI.
     *
     * @param mutatorId The name / id of this mutator type.
     * @param mutatorUiFactory The factory that builds fragments for this mutator type.
     */
    public void registerMutatorUi(String mutatorId, MutatorFragment.Factory mutatorUiFactory) {
        MutatorFragment.Factory old = mMutatorUiFactories.get(mutatorId);
        if (mutatorUiFactory == old) {
            return;
        }
        mMutatorUiFactories.put(mutatorId, mutatorUiFactory);
    }

    /**
     * @return True if there is a UI registered for the given mutator id.
     */
    public boolean hasUiForMutator(String mutatorId) {
        return mMutatorUiFactories.get(mutatorId) != null;
    }

    /**
     * Retrieves a {@link MutatorFragment} for the given mutator. They fragment may be displayed to
     * the user. If a fragment could not be found null is returned instead.
     *
     * @param mutator The mutator to return a fragment for.
     * @return A {@link MutatorFragment} that can be shown to the user.
     */
    @Nullable
    public MutatorFragment getMutatorFragment(Mutator mutator) {
        MutatorFragment.Factory factory = mMutatorUiFactories.get(mutator.getMutatorId());
        if (factory == null) {
            return null;
        }
        return factory.newMutatorFragment(mutator);
    }

    /**
     * Creates a {@link BlockGroup} for the given block and its children using the workspace's
     * default style.
     *
     * @param rootBlock The root block to generate a view for.
     * @param connectionManager The {@link ConnectionManager} to update when moving connections.
     * @param touchHandler The {@link BlockTouchHandler} to manage all touches.
     *
     * @return A view for the block.
     */
    public final BlockGroup buildBlockGroupTree(Block rootBlock,
                                          ConnectionManager connectionManager,
                                          BlockTouchHandler touchHandler) {
        BlockGroup bg = buildBlockGroup();
        buildBlockViewTree(rootBlock, bg, connectionManager, touchHandler);
        return bg;
    }

    /**
     * Called to construct the complete hierarchy of views representing a {@link Block} and its
     * subcomponents, added to {@code parentGroup}.
     *
     * @param block The root block to generate a view for.
     * @param parentGroup T
     * @param connectionManager The {@link ConnectionManager} to update when moving connections.
     * @param touchHandler The {@link BlockTouchHandler} to manage all touches.
     *
     * @return A view for the block and all its descendants.
     */
    public final BlockView buildBlockViewTree(Block block, BlockGroup parentGroup,
                                        ConnectionManager connectionManager,
                                        BlockTouchHandler touchHandler) {
        BlockView blockView = getView(block);
        if (blockView != null) {
            throw new IllegalStateException("BlockView already created.");
        }

        List<InputView> inputViews = buildInputViews(block, connectionManager, touchHandler);
        blockView = buildBlockView(block, inputViews, connectionManager, touchHandler);

        // TODO(#137): Move to ViewPool class.
        mBlockIdToView.put(block.getId(), new WeakReference<>(blockView));

        parentGroup.addView((View) blockView);

        Block next = block.getNextBlock();
        if (next != null) {
            // Recursively calls buildBlockViewTree(..) for the rest of the sequence.
            buildBlockViewTree(next, parentGroup, connectionManager, touchHandler);
        }

        return blockView;
    }

    /**
     * This returns the view constructed to represent a {@link Block}.  Each block is only allowed
     * one view instance among the views managed by this factory (including
     * {@link WorkspaceFragment} and any {@link FlyoutFragment FlyoutFragments}. Views are
     * constructed in {@link #buildBlockViewTree}, either directly or via recursion.  If the block
     * view has not been constructed, this method will return null.
     * <p/>
     * Calling {@link BlockView#unlinkModel()} (possibly via {@link BlockGroup#unlinkModel()}) will
     * disconnect this view from its model, and will it no longer be returned from this method.
     *
     * @param block The {@link Block} to get the view for.
     * @return The previously constructed and active view of {@code block}. Otherwise null.
     */
    @Nullable
    public final BlockView getView(Block block) {
        WeakReference<BlockView> viewRef = mBlockIdToView.get(block.getId());
        return viewRef == null ? null : viewRef.get();
    }

    /**
     * @return A new, empty {@link BlockGroup} container view for a sequence of blocks.
     */
    public BlockGroup buildBlockGroup() {
        return new BlockGroup(mContext, mHelper);
    }

    /**
     * Rebuilds a BlockView based on the latest model state, replacing it in-place in the view tree.
     * Some minor view state is not propagated (pressed, highlight, text selection, etc.) during the
     * replacement.
     *
     * @param original The original BlockView to be rebuilt and replaced.
     * @return The newly reconstructed BlockView.
     * @throws ClassCastException If original is connected to a parent that is not a ViewGroup.
     */
    // TODO(#588): Replace with in-place view shaping, to preserve view state (open dropdowns, text
    //             selection, etc.) and minimize risk of breaking object references.
    // TODO(#589): Testing
    @NonNull
    public BlockView rebuildBlockView(BlockView original) {
        if (Looper.getMainLooper() != Looper.myLooper()) {
            throw new IllegalStateException("rebuildBlockView() must run on the main thread.");
        }

        Block model = original.getBlock();
        if (getView(model) != original) {
            throw new IllegalStateException(
                    "Refusing to rebuild a BlockView that is not the block's current/active view.");
        }

        // Get parent as ViewGroup first, so any failure to cast to ViewGroup fails early.
        ViewGroup parent = (ViewGroup) original.getParent();


        ConnectionManager connectionManager = original.getConnectionManager();
        BlockTouchHandler touchHandler = original.getTouchHandler();

        original.unlinkModel();

        for (Input input : model.getInputs()) {
            Block childBlock = input.getConnectedBlock();
            BlockView childBlockView = childBlock == null ? null : getView(childBlock);
            ViewGroup inputView = childBlockView == null ? null :
                    (ViewGroup)childBlockView.getParent();
            if (inputView != null) {
                inputView.removeView((View) childBlockView);
            }
        }

        List<InputView> inputViews = buildInputViews(model, connectionManager, touchHandler);
        BlockView newBlockView = buildBlockView(model, inputViews, connectionManager, touchHandler);

        if (parent != null) {
            View originalView = (View) original;
            int viewIndex = getIndexOfChild(parent, originalView);
            ViewGroup.LayoutParams lp = ((View) original).getLayoutParams();

            parent.removeView(originalView);
            parent.addView((View) newBlockView, viewIndex, lp);
        }

        mBlockIdToView.put(model.getId(), new WeakReference<>(newBlockView));
        return newBlockView;
    }

    /**
     * Build and populate the {@link BlockView} for {@code block}, using the provided
     * {@link InputView}s.
     * <p/>
     * This method should not recurse the model to generate more than one view.
     * {@link #buildBlockViewTree} will traverse the model and call this method for each
     * {@link Block}.
     *
     * @param block The {@link Block} to build a view for.
     * @param inputViews The list of {@link com.google.blockly.android.ui.InputView}s in this block.
     * @param connectionManager The {@link ConnectionManager} for the {@link Workspace}.
     * @param touchHandler The {@link BlockTouchHandler} this view should start with.
     * @return The new {@link com.google.blockly.android.ui.BlockView}.
     */
    protected abstract BlockView buildBlockView(Block block, List<InputView> inputViews,
                                                ConnectionManager connectionManager,
                                                BlockTouchHandler touchHandler);

    /**
     * Build and populate the {@link InputView} for {@code input}.
     * <p/>
     * This method should not recurse the model to generate more than one view.
     * {@link #buildBlockViewTree} will traverse the model and call this method for each
     * {@link Input}.
     *
     * @param input The {@link Input} to build a view for.
     * @param fieldViews The list of {@link FieldView}s in the constructed view.
     * @return The new {@link com.google.blockly.android.ui.InputView}.
     */
    protected abstract InputView buildInputView(Input input, List<FieldView> fieldViews);

    /**
     * Build and populate the {@link FieldView} for {@code field}.
     * <p>
     * Note: Variables need some extra setup when they are created by a custom
     * ViewFactory.
     * <ul>
     *     <li>If they use an adapter to display the list of variables it must be set.</li>
     *     <li>If they have delete/rename/create options they must have a
     *     {@link VariableRequestCallback} set on them. {@link #mVariableCallback} may be used for
     *     this purpose.</li>
     * </ul>
     *
     *
     * @param field The {@link Field} to build a view for.
     * @return The new {@link FieldView}.
     */
    protected FieldView buildFieldView(Field field) {
        @Field.FieldType int type = field.getType();
        switch (type) {
            case Field.TYPE_ANGLE: {
                BasicFieldAngleView fieldAngleView = new BasicFieldAngleView(mContext);
                fieldAngleView.setField(field);
                return fieldAngleView;
            }
            case Field.TYPE_CHECKBOX: {
                BasicFieldCheckboxView fieldCheckboxView = new BasicFieldCheckboxView(mContext);
                fieldCheckboxView.setField(field);
                return fieldCheckboxView;
            }
            case Field.TYPE_COLOR: {
                BasicFieldColorView fieldColorView = new BasicFieldColorView(mContext);
                fieldColorView.setField(field);
                return fieldColorView;
            }
            case Field.TYPE_DATE: {
                BasicFieldDateView fieldDateView = new BasicFieldDateView(mContext);
                fieldDateView.setField(field);
                return fieldDateView;
            }
            case Field.TYPE_DROPDOWN: {
                BasicFieldDropdownView fieldDropdownView = new BasicFieldDropdownView(mContext);
                fieldDropdownView.setField(field);
                return fieldDropdownView;
            }
            case Field.TYPE_IMAGE: {
                BasicFieldImageView fieldImageView = new BasicFieldImageView(mContext);
                fieldImageView.setField(field);
                return fieldImageView;
            }
            case Field.TYPE_INPUT: {
                BasicFieldInputView fieldInputView = new BasicFieldInputView(mContext);
                fieldInputView.setField(field);
                return fieldInputView;
            }
            case Field.TYPE_LABEL: {
                BasicFieldLabelView fieldLabelView = new BasicFieldLabelView(mContext);
                fieldLabelView.setField(field);
                return fieldLabelView;
            }
            case Field.TYPE_VARIABLE: {
                BasicFieldVariableView fieldVariableView = new BasicFieldVariableView(mContext);
                fieldVariableView.setAdapter(getVariableAdapter(fieldVariableView));
                fieldVariableView.setField(field);
                fieldVariableView.setVariableRequestCallback(mVariableCallback);
                return fieldVariableView;
            }
            case Field.TYPE_NUMBER: {
                BasicFieldNumberView fieldNumberView = new BasicFieldNumberView(mContext);
                fieldNumberView.setField(field);
                return fieldNumberView;
            }

            case Field.TYPE_UNKNOWN:
            default:
                throw new IllegalArgumentException("Unknown Field type: " + type);
        }
    }

    protected SpinnerAdapter getVariableAdapter(AppCompatSpinner mSpinner) {
        if (mVariableNameManager == null) {
            throw new IllegalStateException("NameManager must be set before variable field is "
                    + "instantiated.");
        }
        if (mVariableAdapter == null) {
            mVariableAdapter = new BasicFieldVariableView.VariableViewAdapter(
                    mContext, mVariableNameManager, android.R.layout.simple_spinner_item, mSpinner);
        }
        return mVariableAdapter;
    }

    @NonNull
    protected List<InputView> buildInputViews(
            Block block, ConnectionManager connectionManager, BlockTouchHandler touchHandler) {
        List<Input> inputs = block.getInputs();
        final int inputCount = inputs.size();
        List<InputView> inputViews = new ArrayList<>(inputCount);
        for (int i = 0; i < inputCount; i++) {
            Input input = inputs.get(i);
            List<Field> fields = input.getFields();
            List<FieldView> fieldViews = new ArrayList<>(fields.size());
            for (int  j = 0; j < fields.size(); j++) {
                fieldViews.add(buildFieldView(fields.get(j)));
            }
            InputView inputView = buildInputView(input, fieldViews);

            if (input.getType() != Input.TYPE_DUMMY) {
                Block targetBlock = input.getConnection().getTargetBlock();
                if (targetBlock != null) {
                    // Blocks connected to inputs live in their own BlockGroups.
                    BlockGroup subgroup;
                    BlockView targetBlockView = getView(targetBlock);
                    if (targetBlockView == null) {
                        subgroup =
                                buildBlockGroupTree(targetBlock, connectionManager, touchHandler);
                    } else {
                        // BlockViews might already exist, especially in the case of children of
                        // mutated blocks.
                        ViewParent targetBlockViewParent = targetBlockView.getParent();
                        if (targetBlockViewParent == null) {
                            subgroup = buildBlockGroup();
                            subgroup.addView((View) targetBlockView);
                        } else if (targetBlockViewParent instanceof BlockGroup) {
                            if (targetBlockViewParent.getParent() != null) {
                                throw new IllegalStateException(
                                        "BlockView parent is already attached.");
                            }
                            if (((ViewGroup) targetBlockViewParent).getChildAt(0)
                                    != targetBlockView) {
                                throw new IllegalStateException(
                                        "Input BlockView is not first in parent BlockGroup.");
                            }
                            subgroup = (BlockGroup) targetBlockViewParent;
                        } else {
                            throw new IllegalStateException(
                                    "Unexpected BlockView parent is not a BlockGroup.");
                        }
                    }
                    inputView.setConnectedBlockGroup(subgroup);
                }
            }
            inputViews.add(inputView);
        }
        return inputViews;
    }

    /**
     * Removes the mapping to this view from its block.  This should only be called from
     * {@link BlockView#unlinkModel()}, which is already handled in {@link AbstractBlockView}.
     *
     *  @param blockView The BlockView to disassociate from its Block model.
     */
    // TODO(#137): Move to ViewPool class.
    protected final void unregisterView(BlockView blockView) {
        Block block = blockView.getBlock();
        mBlockIdToView.remove(block.getId());
    }

    /**
     * Constructs the drag and drop flags used by {@link ViewCompat#startDragAndDrop}.
     */
    public int getDragAndDropFlags() {
        int flags = 0;
        if (android.os.Build.VERSION.SDK_INT >= 24) {
            flags |= 0x00000100;  // View.DRAG_FLAG_GLOBAL
        }
        return flags;
    }

    /**
     * @return The index of the provided child view.
     */
    private static int getIndexOfChild(ViewGroup parent, View child) {
        int childCount = parent.getChildCount();
        for (int viewIndex = 0; viewIndex < childCount; ++viewIndex) {
            if (parent.getChildAt(viewIndex) == child) {
                return viewIndex;
            }
        }
        throw new IllegalStateException("Child View not found.");
    }

    /**
     * Handles a user toggling the UI for a mutator on a block. This is typically done through a
     * mutator button on a block, but other UIs may trigger it in other ways.
     */
    public interface MutatorToggleListener {
        void onMutatorToggled(Block block);
    }
} public class VirtualWorkspaceView extends NonPropagatingViewGroup {
    private static final String TAG = "VirtualWorkspaceView";
    private static final boolean DEBUG = false;

    // TODO(#87): Replace with configuration. Use dp.
    // Default desired width of the view in pixels.
    private static final int DESIRED_WIDTH = 2048;
    // Default desired height of the view in pixels.
    private static final int DESIRED_HEIGHT = 2048;

    private static final float MIN_SCALE_TO_DRAW_GRID = 0.5f;

    // Allowed zoom scales.
    private static final float[] ZOOM_SCALES = new float[]{0.25f, 0.5f, 1.0f, 2.0f};
    private static final int INIT_ZOOM_SCALES_INDEX = 2;

    protected boolean mScrollable = true;
    protected boolean mScalable = true;

    private final ViewPoint mPanningStart = new ViewPoint();

    private final WorkspaceGridRenderer mGridRenderer = new WorkspaceGridRenderer();

    // Fields for workspace panning.
    private int mPanningPointerId = MotionEvent.INVALID_POINTER_ID;
    // Coordinates at the beginning of scrolling the workspace.
    private int mOriginalScrollX;
    private int mOriginalScrollY;
    // Scale and zoom in/out factor.
    private int mCurrentZoomScaleIndex = INIT_ZOOM_SCALES_INDEX;
    private float mViewScale = ZOOM_SCALES[INIT_ZOOM_SCALES_INDEX];

    private boolean mDrawGrid = true;

    // The workspace view that backs this virtual view.
    private WorkspaceView mWorkspaceView;
    // Flag indicating whether view should be reset before redrawing. This is set upon construction
    // to force an initial reset in the first call to onLayout. Call postResetView() to set this.
    private boolean mResetViewPending = true;

    private ScaleGestureDetector mScaleGestureDetector;
    private GestureDetector mTapGestureDetector;
    private InputMethodManager mImeManager;

    private Rect mTempRect = new Rect();  // Used when calculating view-scaled block bounds.

    public VirtualWorkspaceView(Context context) {
        this(context, null);
    }

    public VirtualWorkspaceView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public VirtualWorkspaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public void onFinishInflate() {
        super.onFinishInflate();

        mWorkspaceView = (WorkspaceView) findViewById(R.id.workspace);
        // Setting the child view's pivot point to (0,0) means scaling leaves top-left corner in
        // place means there is no need to adjust view translation.
        mWorkspaceView.setPivotX(0);
        mWorkspaceView.setPivotY(0);

        setWillNotDraw(false);
        setHorizontalScrollBarEnabled(mScrollable);
        setVerticalScrollBarEnabled(mScrollable);

        mScaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleGestureListener());
        mTapGestureDetector = new GestureDetector(getContext(), new TapGestureListener());
        mGridRenderer.updateGridBitmap(mViewScale);
        mImeManager = (InputMethodManager) getContext()
                .getSystemService(Context.INPUT_METHOD_SERVICE);
    }

    /**
     * Post a view reset to occur upon next call to {@link #onLayout(boolean, int, int, int, int)}.
     * <p/>
     * This method can be called when workspace content has been completely replaced, to provide the
     * user with a good initial view of the new workspace content.
     */
    public void postResetView() {
        mResetViewPending = true;
    }

    /**
     * Reset the view to the top-left corner of the virtual workspace (with a small margin), and
     * reset zoom to unit scale.
     * <p/>
     * This is called when the "reset view" button is clicked, or when
     * {@link #mResetViewPending} is set.
     */
    public void resetView() {
        // Reset scrolling state.
        mPanningPointerId = MotionEvent.INVALID_POINTER_ID;
        mPanningStart.set(0,0);
        mOriginalScrollX = 0;
        mOriginalScrollY = 0;

        updateScaleStep(INIT_ZOOM_SCALES_INDEX);

        final Rect blocksBoundingBox = getViewScaledBlockBounds();
        final boolean useRtl = mWorkspaceView.getWorkspaceHelper().useRtl();
        if (mScrollable) {
            final int margin = mGridRenderer.getGridSpacing() / 2;
            final int scrollToY = blocksBoundingBox.top - margin;
            if (useRtl) {
                scrollTo(blocksBoundingBox.right - getMeasuredWidth() + margin, scrollToY);
            } else {
                scrollTo(blocksBoundingBox.left - margin, scrollToY);
            }
        } else {
            // Reset top leading corner to 0,0 when
            scrollTo(useRtl ? -getMeasuredWidth() : 0, 0);
        }
    }

    public boolean isScrollable() {
        return mScrollable;
    }

    public void setDrawGrid(boolean drawGrid){
        mDrawGrid = drawGrid;
    }

    public void setZoomBehavior(ZoomBehavior zoomBehavior){
        setScrollable(zoomBehavior.isScrollEnabled());
        setScalable(zoomBehavior.isPinchZoomEnabled());
    }

    /**
     * Configures whether the user can scroll the workspace by dragging.  If scrolling is disabled,
     * the workspace will reset to 0,0 in the top right hand corner.
     *
     * @param scrollable Allow scrolling if true. Otherwise, disable it.
     */
    protected void setScrollable(boolean scrollable) {
        if (scrollable == mScrollable) {
            return;
        }
        mScrollable = scrollable;
        setHorizontalScrollBarEnabled(mScrollable);
        setVerticalScrollBarEnabled(mScrollable);
        if (!mScrollable) {
            resetView();
        }
    }

    /**
     * Configures whether the user can scale the workspace by touch events.
     *
     * @param scalable Allow scalability if true. Otherwise, disable it.
     */
    protected void setScalable(boolean scalable){
        if(mScalable == scalable){
            return;
        }
        mScalable = scalable;
        if(!scalable) {
            resetView();
        }
    }

    /**
     * Zoom into (i.e., enlarge) the workspace.
     *
     * @return True if a zoom was changed, increased. Otherwise false.
     */
    public boolean zoomIn() {
        if (mCurrentZoomScaleIndex < ZOOM_SCALES.length - 1) {
            updateScaleStep(mCurrentZoomScaleIndex + 1);
            return true;
        }
        return false;
    }

    /**
     * Zoom out from (i.e.,shrink) the workspace.
     *
     * @return True if a zoom was changed, decreased. Otherwise false.
     */
    public boolean zoomOut() {
        if (mScrollable && mCurrentZoomScaleIndex > 0) {
            updateScaleStep(mCurrentZoomScaleIndex - 1);
            return true;
        }
        return false;
    }

    /**
     * @return The current view scale factor. Larger than 1.0 means zoomed in, smaller than 1.0
     * means zoomed out.
     */
    public float getViewScale() {
        return mViewScale;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        mTapGestureDetector.onTouchEvent(event);
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(mScalable && mScaleGestureDetector != null) {
            mScaleGestureDetector.onTouchEvent(event);
            if (mScaleGestureDetector.isInProgress()) {
                // If the scale gesture detector is handling a scale-and-pan gesture, then exit here
                // since otherwise we would also be generating dragging events below.
                return true;
            }
        }

        final int action = event.getActionMasked();

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                clearFocus();
                mImeManager.hideSoftInputFromWindow(getWindowToken(), 0);

                if (mScrollable) {
                    final int pointerIdx = event.getActionIndex();
                    mPanningPointerId = event.getPointerId(pointerIdx);
                    mPanningStart.set(
                            (int) event.getX(pointerIdx),
                            (int) event.getY(pointerIdx));
                    mOriginalScrollX = getScrollX();
                    mOriginalScrollY = getScrollY();
                }
                return true;
            }
            case MotionEvent.ACTION_MOVE: {
                if (mPanningPointerId != MotionEvent.INVALID_POINTER_ID) {
                    final int pointerIdx =
                            event.findPointerIndex(mPanningPointerId);
                    if (pointerIdx == -1) {
                        // TODO: (#319) remove when we clean up multi-touch handling.
                        Log.w(TAG, "Got an invalid pointer idx for the panning pointer.");
                        return false;
                    }
                    scrollTo(
                            mOriginalScrollX + mPanningStart.x -
                                    (int) event.getX(pointerIdx),
                            mOriginalScrollY + mPanningStart.y -
                                    (int) event.getY(pointerIdx));
                    return true;
                } else {
                    return false;
                }
            }
            case MotionEvent.ACTION_POINTER_UP: {
                // Some pointer went up - check whether it was the one used for panning.
                final int pointerIdx = event.getActionIndex();
                final int pointerId = event.getPointerId(pointerIdx);
                if (pointerId != mPanningPointerId) {
                    return false;
                }
                // Pointer that went up was used for panning - treat like ACTION_UP.
                // FALLTHROUGH INTENDED.
            }
            case MotionEvent.ACTION_UP: {
                if (mPanningPointerId != MotionEvent.INVALID_POINTER_ID) {
                    mPanningPointerId = MotionEvent.INVALID_POINTER_ID;
                    return true;
                } else {
                    return false;
                }
            }
            case MotionEvent.ACTION_CANCEL: {
                if (mPanningPointerId != MotionEvent.INVALID_POINTER_ID) {
                    // When cancelled, reset to original scroll position.
                    scrollTo(mOriginalScrollX, mOriginalScrollY);
                    mPanningPointerId = MotionEvent.INVALID_POINTER_ID;
                    return true;
                } else {
                    return false;
                }
            }
            default: {
                break;
            }
        }

        return false;
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
                getMeasuredSize(widthMeasureSpec, DESIRED_WIDTH),
                getMeasuredSize(heightMeasureSpec, DESIRED_HEIGHT));

        mWorkspaceView.measure(
                MeasureSpec.makeMeasureSpec(
                        (int) (getMeasuredWidth() / mViewScale), MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(
                        (int) (getMeasuredHeight() / mViewScale), MeasureSpec.EXACTLY));
    }

    @Override
    public void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mResetViewPending) {
            resetView();
            mResetViewPending = false;
        }

        // Shift the wrapped view's position to follow scrolling along. The scrolling of view
        // content is controlled by setTranslationX() and setTranslationY() in this.scrollTo()
        // below.
        final int offsetX = getScrollX();
        final int offsetY = getScrollY();
        mWorkspaceView.layout((offsetX), (offsetY),
                (int) ((getMeasuredWidth() / mViewScale) + offsetX),
                (int) ((getMeasuredHeight() / mViewScale) + offsetY));
    }

    @Override
    protected void onDraw(Canvas c) {
        if (shouldDrawGrid()) {
            mGridRenderer.drawGrid(c, getWidth(), getHeight(), getScrollX(), getScrollY());
        }
    }

    /**
     * Set scroll position for the {@link WorkspaceView} wrapped by this instance.
     * <p/>
     * The given scroll position specifies the absolute offset of the displayed area within the
     * virtual workspace. Inside this method, the given scroll coordinates are clamped to their
     * valid ranges determined from the bounding box of all blocks in the virtual workspace, and
     * allowing for overscroll of half the width (or height, respectively) of this view.
     *
     * @param x The horizontal scroll position in pixel units of this view.
     * @param y The vertical scroll position in pixel units of this view.
     */
    @Override
    public void scrollTo(int x, int y) {
        if (!mScrollable) {
            return;
        }

        // Clamp x and y to the scroll range that will allow for 1/2 the view (or more, for smaller
        // views) being outside the range use by blocks. This matches the computations in
        // computeHorizontalScrollOffset and computeVerticalScrollOffset, respectively.
        Rect blocksBounds = getViewScaledBlockBounds();
        int blocksWidth = blocksBounds.width();
        int blocksHeight = blocksBounds.height();

        int viewWidth = getMeasuredWidth();
        int halfViewWidth = viewWidth / 2;
        int viewHeight = getMeasuredHeight();
        int halfViewHeight = viewHeight / 2;

        int horzMargin = halfViewWidth; // Default margin is half the scrollable view width.
        if (blocksWidth < halfViewWidth) {
            horzMargin = viewWidth - blocksWidth;
        }

        int vertMargin = halfViewHeight;
        if (blocksHeight < halfViewHeight) {
            vertMargin = viewHeight - blocksHeight;
        }

        final int xMin = blocksBounds.left - horzMargin;
        final int xMax = blocksBounds.right + horzMargin - viewWidth;
        x = clampToRange(x, xMin, xMax);

        final int yMin = blocksBounds.top - vertMargin;
        final int yMax = blocksBounds.bottom + vertMargin - viewHeight;
        y = clampToRange(y, yMin, yMax);

        // Update and show scroll bars.
        super.scrollTo(x, y);

        // Set view offset in the virtual workspace and request layout of the WorkspaceView with the
        // new offset. The view offset is the location of the top-left pixel displayed in this view
        // in virtual workspace coordinates, regardless of RTL vs. LTR mode.
        mWorkspaceView.getWorkspaceHelper()
                .setVirtualWorkspaceViewOffset(
                        (int) (x / mViewScale), /* virtual coords. */
                        (int) (y / mViewScale));
        mWorkspaceView.requestLayout();
    }

    /**
     * Get size for one dimension (width or height) of the view based on measure spec and desired
     * size.
     *
     * @param measureSpec The measure spec provided to {@link #onMeasure(int, int)} by its caller.
     * @param desiredSize The intrinsic desired size for this view.
     *
     * @return The determined size, given measure spec and desired size.
     */
    private static int getMeasuredSize(int measureSpec, int desiredSize) {
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);

        if (mode == MeasureSpec.EXACTLY) {
            return size;
        } else if (mode == MeasureSpec.AT_MOST) {
            return Math.min(size, desiredSize);
        } else {
            return desiredSize;
        }

    }

    /**
     * @return Horizontal scroll range width, which is used to position the horizontal scroll bar.
     * <p/>
     * This value is the upper limit of the permitted horizontal scroll range, which corresponds to
     * the right boundary of the currently-used workspace area, plus has the view width to allow for
     * overscroll.
     * <p/>
     * Together with the return value of {@link #computeHorizontalScrollOffset()}, this determines
     * the relative position of the scroll bar.
     * <p/>
     * Note that the units of the value returned by this method are essentially arbitrary in the
     * sense that scroll bar position and width are invariant under scaling of range, offset, and
     * extent by the same factor (up to truncation of fractions to nearest integers).
     * <p/>
     * We compute this by taking the total width of the available workspace and converting it to
     * screen pixels. The available workspace is the size needed to fully enclose all blocks with
     * half the display's width for padding, or
     * {@code (workspace max - workspace min) in pixels + the visible width}.
     */
    @Override
    protected int computeHorizontalScrollRange() {
        final Rect viewScaledBlockBounds = getViewScaledBlockBounds();
        final int viewScaledWorkspaceRange =
                viewScaledBlockBounds.right - viewScaledBlockBounds.left;
        final int width = getMeasuredWidth();
        int totalMargin = width;  // By default, leave a half screen width on left and right.
        if (viewScaledWorkspaceRange < width / 2) {
            // Make sure blocks can touch each edge.
            totalMargin = 2 * (width - viewScaledWorkspaceRange);
        }
        return viewScaledWorkspaceRange + totalMargin;
    }

    /**
     * @return Width of the horizontal scroll bar within its range.
     * <p/>
     * This value determines how wide the horizontal scroll bar is, relative to its range as
     * returned by {@link #computeHorizontalScrollRange()}. For example, if this value is half the
     * current range, then the scroll bar will be draw half as wide as the view.
     * <p/>
     * Note that the units of the value returned by this method are essentially arbitrary in the
     * sense that scroll bar position and width are invariant under scaling of range, offset, and
     * extent by the same factor (up to truncation of fractions to nearest integers).
     * <p/>
     * We use screen pixels for the unit, so this is just the measured width of the view.
     */
    @Override
    protected int computeHorizontalScrollExtent() {
        return getMeasuredWidth();
    }

    /**
     * @return Offset of the current horizontal scroll position, which determines the relative
     * position of the horizontal scroll bar.
     * <p/>
     * This is a value between 0 and the return value of {@link #computeHorizontalScrollRange()}.
     * Zero corresponds to the left boundary of the currently-used workspace area, minus half the
     * width of this view to allow for overscroll.
     * <p/>
     * Note that the units of the value returned by this method are essentially arbitrary in the
     * sense that scroll bar position and width are invariant under scaling of range, offset, and
     * extent by the same factor (up to truncation of fractions to nearest integers).
     * <p/>
     * The offset is just a translation of {@code getScrollX()} of the left edge of the viewport on
     * the workspace in pixels relative to the farthest left position possible. In other words,
     * offset goes from {@code 0} to {@code (workspace max - workspace min)} in pixels.
     */
    @Override
    protected int computeHorizontalScrollOffset() {
        final Rect viewScaledBlockBounds = getViewScaledBlockBounds();
        return getScrollX() - (viewScaledBlockBounds.left - computeHorizontalScrollExtent()) / 2;
    }

    /**
     * @return Vertical scroll range width. See {@link #computeHorizontalScrollRange()} for details.
     */
    @Override
    protected int computeVerticalScrollRange() {
        final Rect viewScaledBlockBounds = getViewScaledBlockBounds();
        final int viewScaledWorkspaceRange =
                viewScaledBlockBounds.bottom - viewScaledBlockBounds.top;
        final int height = getMeasuredHeight();
        int totalMargin = height;  // By default, leave a half screen height on top and bottom.
        if (viewScaledWorkspaceRange < height / 2) {
            // Make sure blocks can touch each edge.
            totalMargin = 2 * (height - viewScaledWorkspaceRange);
        }
        return viewScaledWorkspaceRange + totalMargin;
    }

    /**
     * @return Width of the vertical scroll bar within its range.
     * See {@link #computeHorizontalScrollExtent()} for details.
     */
    @Override
    protected int computeVerticalScrollExtent() {
        return getMeasuredHeight();
    }

    /**
     * @return Offset of the current horizontal scroll position.
     * See {@link #computeHorizontalScrollOffset()} for details.
     */
    @Override
    protected int computeVerticalScrollOffset() {
        final Rect viewScaledBlockBounds = getViewScaledBlockBounds();
        return getScrollY() - (viewScaledBlockBounds.top - computeVerticalScrollExtent()) / 2;
    }

    private void updateScaleStep(int newScaleIndex) {
        if (newScaleIndex != mCurrentZoomScaleIndex) {
            final float oldViewScale = mViewScale;

            mCurrentZoomScaleIndex = newScaleIndex;
            mViewScale = ZOOM_SCALES[mCurrentZoomScaleIndex];

            // Add offset to current scroll coordinates so the center of the visible workspace area
            // remains in the same place.
            final float scaleDifference = mViewScale - oldViewScale;
            scrollBy((int) (scaleDifference * getMeasuredWidth() / 2),
                    (int) (scaleDifference * getMeasuredHeight() / 2));

            if (shouldDrawGrid()) {
                mGridRenderer.updateGridBitmap(mViewScale);
            }

            mWorkspaceView.setScaleX(mViewScale);
            mWorkspaceView.setScaleY(mViewScale);
            mWorkspaceView.requestLayout();
        }
    }

    private boolean shouldDrawGrid() {
        return mDrawGrid && mViewScale >= MIN_SCALE_TO_DRAW_GRID;
    }

    private static int clampToRange(int y, int min, int max) {
        return Math.min(max, Math.max(min, y));
    }

    public int getGridSpacing() {
        return mGridRenderer.getGridSpacing();
    }

    public void setGridSpacing(int gridSpacing) {
        mGridRenderer.setGridSpacing(gridSpacing);
    }

    public void setGridColor(int gridColor) {
        mGridRenderer.setGridColor(gridColor);
    }

    public void setGridDotRadius(int gridDotRadius) {
        mGridRenderer.setGridDotRadius(gridDotRadius);
    }

    private class TapGestureListener extends GestureDetector.SimpleOnGestureListener {
        public boolean onSingleTapUp(MotionEvent e) {
            return callOnClick();
        }
    }

    /** EventsCallback class for scaling and panning the view using pinch-to-zoom gestures. */
    private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        // Focus point at the start of the pinch gesture. This is used for computing proper scroll
        // offsets during scaling, as well as for simultaneous panning.
        private float mStartFocusX;
        private float mStartFocusY;
        // View scale at the beginning of the gesture. This is used for computing proper scroll
        // offsets during scaling.
        private float mStartScale;
        // View scroll offsets at the beginning of the gesture. These provide the reference point
        // for adjusting scroll in response to scaling and panning.
        private int mStartScrollX;
        private int mStartScrollY;

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            mStartFocusX = detector.getFocusX();
            mStartFocusY = detector.getFocusY();

            mStartScrollX = getScrollX();
            mStartScrollY = getScrollY();

            mStartScale = mViewScale;
            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            final float oldViewScale = mViewScale;

            final float scaleFactor = detector.getScaleFactor();
            mViewScale *= scaleFactor;

            if (mViewScale < ZOOM_SCALES[0]) {
                mCurrentZoomScaleIndex = 0;
                mViewScale = ZOOM_SCALES[mCurrentZoomScaleIndex];
            } else if (mViewScale > ZOOM_SCALES[ZOOM_SCALES.length - 1]) {
                mCurrentZoomScaleIndex = ZOOM_SCALES.length - 1;
                mViewScale = ZOOM_SCALES[mCurrentZoomScaleIndex];
            } else {
                // find nearest zoom scale
                float minDist = Float.MAX_VALUE;
                // If we reach the end the last one was the closest
                int index = ZOOM_SCALES.length - 1;
                for (int i = 0; i < ZOOM_SCALES.length; i++) {
                    float dist = Math.abs(mViewScale - ZOOM_SCALES[i]);
                    if (dist < minDist) {
                        minDist = dist;
                    } else {
                        // When it starts increasing again we've found the closest
                        index = i - 1;
                        break;
                    }
                }
                mCurrentZoomScaleIndex = index;
            }

            if (shouldDrawGrid()) {
                mGridRenderer.updateGridBitmap(mViewScale);
            }

            mWorkspaceView.setScaleX(mViewScale);
            mWorkspaceView.setScaleY(mViewScale);

            // Compute scroll offsets based on difference between original and new scaling factor
            // and the focus point where the gesture started. This makes sure that the scroll offset
            // is adjusted to keep the focus point in place on the screen unless there is also a
            // focus point shift (see next scroll component below).
            final float scaleDifference = mViewScale - mStartScale;
            final int scrollScaleX = (int) (scaleDifference * mStartFocusX);
            final int scrollScaleY = (int) (scaleDifference * mStartFocusY);

            // Compute scroll offset based on shift of the focus point. This makes sure the view
            // pans along with the focus.
            final int scrollPanX = (int) (mStartFocusX - detector.getFocusX());
            final int scrollPanY = (int) (mStartFocusY - detector.getFocusY());

            // Apply the computed scroll components for scale and panning relative to the scroll
            // coordinates at the beginning of the gesture.
            scrollTo(mStartScrollX + scrollScaleX + scrollPanX,
                    mStartScrollY + scrollScaleY + scrollPanY);

            return true;
        }
    }

    @NonNull
    private Rect getViewScaledBlockBounds() {
        mWorkspaceView.getBlocksBoundingBox(mTempRect);
        mTempRect.left = (int) Math.floor(mTempRect.left * mViewScale);
        mTempRect.right = (int) Math.ceil(mTempRect.right * mViewScale);
        mTempRect.top = (int) Math.floor(mTempRect.top * mViewScale);
        mTempRect.bottom = (int) Math.ceil(mTempRect.bottom * mViewScale);
        return mTempRect;
    }
} public class WorkspaceHelper {
    private static final String TAG = "WorkspaceHelper";

    // Blocks "snap" toward each other at the end of drags if they have compatible connections
    // near each other.  This is the farthest they can snap at 1.0 zoom, in workspace units.
    private static final int DEFAULT_MAX_SNAP_DISTANCE = 24;

    private final ViewPoint mVirtualWorkspaceViewOffset = new ViewPoint();
    private final ViewPoint mTempViewPoint = new ViewPoint();
    private final WorkspacePoint mTempWorkspacePoint = new WorkspacePoint();
    private final int[] mTempIntArray2 = new int[2];
    private final Context mContext;
    private final ZoomBehavior mZoomBehavior;

    private WorkspaceView mWorkspaceView;
    private VirtualWorkspaceView mVirtualWorkspaceView;
    private BlockViewFactory mViewFactory;
    private float mDensity;
    private boolean mRtl;

    /**
     * Determine if {@code dragEvent} is a block drag.
     * @param viewContext The context of the view receiving the drag event.
     * @param dragEvent The drag event in question.
     * @return True if the drag represents a block drag. Otherwise false.
     */
    public static boolean isBlockDrag(Context viewContext, DragEvent dragEvent) {
        // Unwrap ContextWrappers until the Activity is found.
        while (viewContext instanceof ContextWrapper && !(viewContext instanceof Activity)) {
            viewContext = ((ContextWrapper) viewContext).getBaseContext();
        }
        BlocklyController controller = (viewContext instanceof AbstractBlocklyActivity) ?
                ((AbstractBlocklyActivity) viewContext).getController() : null;
        BlockClipDataHelper clipHelper =
                (controller == null) ? null : controller.getClipDataHelper();
        return (clipHelper != null && clipHelper.isBlockData(dragEvent.getClipDescription()));
    }

    /**
     * Create a helper for creating and doing calculations for views in the workspace.
     *
     * @param context The {@link Context} of the fragment or activity this lives in.
     */
    public WorkspaceHelper(Context context) {
        mContext = context;
        mZoomBehavior = ZoomBehavior.loadFromTheme(context);

        final Resources res = mContext.getResources();
        mDensity = res.getDisplayMetrics().density;
        if (mDensity == 0) {
            Log.e(TAG, "Density is not defined for this context. Defaulting to 1.");
            mDensity = 1f;
        }

        updateRtl(res);
    }

    /**
     * Pairs a {@code blockViewFactory} with this helper instance.
     *
     * @param blockViewFactory The factory used to construct block views and subviews for this app.
     */
    void setBlockViewFactory(BlockViewFactory blockViewFactory) {
        if (mViewFactory != null && mViewFactory != blockViewFactory) {
            throw new IllegalStateException("BlockViewFactory already set. Only one allowed.");
        }
        mViewFactory = blockViewFactory;
    }

    /**
     * @return The BlockViewFactory for this application.
     */
    public BlockViewFactory getBlockViewFactory() {
        return mViewFactory;
    }

    /**
     * @return The {@link WorkspaceView} for which this is a helper.
     */
    public WorkspaceView getWorkspaceView() {
        return mWorkspaceView;
    }

    /**
     * Sets the workspace view to use when converting between coordinate systems.
     */
    public void setWorkspaceView(WorkspaceView workspaceView) {
        mWorkspaceView = workspaceView;
        mVirtualWorkspaceView = (VirtualWorkspaceView) mWorkspaceView.getParent();
    }

    /**
     * Convenience method for {@link BlockViewFactory#getView(Block)}.
     *
     * @param block The Block to view.
     * @return The view that was constructed for a given Block object, if any.
     */
    @Nullable
    public BlockView getView(Block block) {
        return (mViewFactory == null) ? null : mViewFactory.getView(block);
    }

    /**
     * Gets the first block up the hierarchy that can be dragged by the user. If the starting
     * block can be manipulated it will be returned.
     *
     * @param startingView The original view that was touched.
     * @return The nearest parent block that the user can manipulate.
     */
    public BlockView getNearestActiveView(BlockView startingView) {
        Block block = startingView.getBlock();
        while (block != null) {
            if (block.isDraggable()) {
                return getView(block);
            }
            block = block.getParentBlock();
        }
        return null;
    }

    /**
     * Set the offset of the virtual workspace view.
     * <p/>
     * This is the coordinate of the top-left corner of the area of the workspace shown by a
     * {@link WorkspaceView} inside a {@link VirtualWorkspaceView}. The coordinate is represented
     * in virtual workspace view coordinates, i.e., workspace coordinates adjusted for display
     * density and reversed in RTL mode (this implies, that the coordinate provides here refers
     * to the top-left corner of the view area, even in RTL mode).
     */
    public void setVirtualWorkspaceViewOffset(int x, int y) {
        mVirtualWorkspaceViewOffset.set(x, y);
    }

    /**
     * @return The maximum distance a block can snap to match a connection, in workspace units.
     */
    // TODO(#477): Return floating point.
    public int getMaxSnapDistance() {
        // TODO(#62): Adapt to WorkspaceView zoom, if connected.
        return DEFAULT_MAX_SNAP_DISTANCE;
    }

    /**
     * Scales a value in workspace coordinate units to virtual view pixel units.
     * <p/>
     * This does not account for offsets into the view's space, nor any scale applied by
     * {@link VirtualWorkspaceView}, but only uses screen density to  calculate the result.
     *
     * @param workspaceValue The value in workspace units.
     *
     * @return The value in virtual view units.
     */
    public int workspaceToVirtualViewUnits(float workspaceValue) {
        return (int) (mDensity * workspaceValue);
    }

    /**
     * Scales a value in virtual view units to workspace units.
     * <p/>
     * This does not account for offsets into the view's space, nor any scale applied by
     * {@link VirtualWorkspaceView}, but only uses screen density to  calculate the result.
     *
     * @param viewValue The value in virtual view units.
     *
     * @return The value in workspace units.
     */
    public int virtualViewToWorkspaceUnits(float viewValue) {
        return (int) (viewValue / mDensity);
    }

    public float getWorkspaceViewScale() {
        return mWorkspaceView.getScaleX();
    }

    /**
     * Function that converts x and y components of a delta vector from virtual view to workspace
     * coordinates.
     * <p/>
     * This function respects right-to-left mode by reversing the direction of the x coordinate, but
     * does not apply the workspace offset.
     */
    public void virtualViewToWorkspaceDelta(ViewPoint viewPointIn,
                                            WorkspacePoint workspacePointOut) {
        workspacePointOut.set(
                (mRtl ? -1 : 1) * virtualViewToWorkspaceUnits(viewPointIn.x),
                virtualViewToWorkspaceUnits(viewPointIn.y));
    }

    /**
     * Function that converts x and y components of a delta vector from workspace to virtual view
     * units.
     * <p/>
     * This function respects right-to-left mode by reversing the direction of the x coordinate, but
     * does not apply the workspace offset.
     */
    public void workspaceToVirtualViewDelta(WorkspacePoint workspacePointIn,
                                            ViewPoint viewPointOut) {
        viewPointOut.set(
                workspaceToVirtualViewUnits(mRtl ? -workspacePointIn.x : workspacePointIn.x),
                workspaceToVirtualViewUnits(workspacePointIn.y));
    }

    /**
     * @return True if using Right to Left layout, false otherwise.
     */
    public boolean useRtl() {
        return mRtl;
    }

    /**
     * Get workspace coordinates of a given {@link View}, relative to the nearest WorkspaceView or
     * RecyclerView.
     * <p/>
     * This function always returns the coordinate of the corner of the view that corresponds to the
     * block coordinate in workspace coordinates. In left-to-right (LTR) mode, this is the
     * <em>top-left</em> corner of the view, in right-to-left (RTL) mode, it is the
     * <em>top-right</em> corner of the view.
     *
     * @param view The view to find the position of.
     * @param outCoordinate The Point to store the results in.
     */
    public void getWorkspaceCoordinates(@NonNull View view, WorkspacePoint outCoordinate) {
        getVirtualViewCoordinates(view, mTempViewPoint);
        if (mRtl) {
            // In right-to-left mode, the Block's position is that of its top-RIGHT corner, but
            // Android still refers to the BlockView's layout coordinate by its top-LEFT corner.
            // Adding the view's width to the lhs view coordinate gives us the rhs coordinate.
            mTempViewPoint.x += view.getMeasuredWidth();
        }
        virtualViewToWorkspaceCoordinates(mTempViewPoint, outCoordinate);
    }

    /**
     * Get virtual view coordinates of a given {@link View}.
     * <p/>
     * This function always returns the coordinate of the top-left corner of the given view,
     * regardless of left-to-right (LTR) vs. right-to-left (RTL) mode. Note that in RTL mode, this
     * is not the corner that corresponds to the block's workspace coordinates. Use
     * {@link #getWorkspaceCoordinates(View, WorkspacePoint)} to obtain  the workspace coordinates
     * of a block from its view, adjusted for RTL mode if necessary.
     *
     * @param view The view to find the position of.
     * @param viewPosition The Point to store the results in.
     */
    public void getVirtualViewCoordinates(View view, ViewPoint viewPosition) {
        int leftRelativeToWorkspace = view.getLeft();
        int topRelativeToWorkspace = view.getTop();

        // Move up the parent hierarchy and add parent-relative view coordinates.
        ViewParent viewParent = view.getParent();
        while (viewParent != null) {
            if (viewParent instanceof WorkspaceView || viewParent instanceof RecyclerView) {
                break;
            }

            leftRelativeToWorkspace += ((View) viewParent).getLeft();
            topRelativeToWorkspace += ((View) viewParent).getTop();

            viewParent = viewParent.getParent();
        }

        if (viewParent == null) {
            throw new IllegalStateException(
                    "No WorkspaceView or RecyclerView found among view's parents.");
        }

        viewPosition.x = leftRelativeToWorkspace;
        viewPosition.y = topRelativeToWorkspace;
    }

    /**
     * Find the highest {@link BlockGroup} in the hierarchy that this {@link Block} descends from.
     *
     * @param block The block to start searching from.
     *
     * @return The highest {@link BlockGroup} found.
     */
    @Nullable
    public BlockGroup getRootBlockGroup(Block block) {
        BlockView bv = getView(block.getRootBlock());
        return (bv == null) ? null : (BlockGroup) bv.getParent();
    }


    /**
     * Find the highest {@link BlockGroup} in the hierarchy that contains this {@link BlockView}.
     *
     * @param blockView The BlockView to start searching from.
     *
     * @return The highest {@link BlockGroup} found.
     */
    @Nullable
    public BlockGroup getRootBlockGroup(BlockView blockView) {
        return getRootBlockGroup(blockView.getBlock());
    }

    /**
     * Find the closest {@link BlockGroup} in the hierarchy that this {@link Block} descends from.
     *
     * @param block The block to start searching from.
     *
     * @return The {@link BlockGroup} parent of the block's view, otherwise null.
     * @throws IllegalStateException when a BlockView is found without a parent BlockGroup.
     */
    @Nullable
    public BlockGroup getParentBlockGroup(Block block) {
        BlockView blockView = getView(block);
        if (blockView != null) {
            BlockGroup bg = blockView.getParentBlockGroup();
            if (bg == null) {
                throw new IllegalStateException("Block has a BlockView, but no parent BlockGroup.");
            }
            return bg;
        }
        return null; // Block does not have a view, and thus no parent BlockGroup.
    }

    /**
     * Finds the closest BlockView that contains the {@code descendantView} as a child or further
     * descendant.
     *
     * @param descendantView The contained {@code view}.
     * @return The closest BlockView that contains the {@code descendantView}.
     */
    @Nullable
    public BlockView getClosestAncestorBlockView(View descendantView) {
        ViewParent parent = descendantView.getParent();
        while (parent != null) {
            if (parent instanceof BlockView) {
                return (BlockView) parent;
            }
            parent = parent.getParent();
        }
        return null; // Not found.
    }

    /**
     * Converts a point in virtual view coordinates to screen coordinates.
     *
     * @param viewPositionIn Input coordinates of a location in {@link WorkspaceView}, expressed
     * with respect to the virtual view coordinate system.
     * @param screenPositionOut Output coordinates of the same location in absolute coordinates on
     * the screen.
     */
    public void virtualViewToScreenCoordinates(ViewPoint viewPositionIn, Point screenPositionOut) {
        mWorkspaceView.getLocationOnScreen(mTempIntArray2);
        screenPositionOut.x = mTempIntArray2[0] +
                (int) (viewPositionIn.x * mWorkspaceView.getScaleX());
        screenPositionOut.y = mTempIntArray2[1] +
                (int) (viewPositionIn.y * mWorkspaceView.getScaleY());
    }

    /**
     * Converts a point in screen coordinates to virtual view coordinates.
     *
     * @param screenPositionIn Input coordinates of a location in absolute coordinates on the
     * screen.
     * @param viewPositionOut Output coordinates of the same location in {@link WorkspaceView},
     * expressed with respect to the virtual view coordinate system.
     */
    public void screenToVirtualViewCoordinates(@Size(2) int[] screenPositionIn,
                                               ViewPoint viewPositionOut) {
        mWorkspaceView.getLocationOnScreen(mTempIntArray2);
        viewPositionOut.x =
                (int) ((screenPositionIn[0] - mTempIntArray2[0]) / mWorkspaceView.getScaleX());
        viewPositionOut.y =
                (int) ((screenPositionIn[1] - mTempIntArray2[1]) / mWorkspaceView.getScaleY());
    }

    /**
     * Convenience method for direct mapping of screen to workspace coordinates.
     * <p/>
     * This method applies {@link #screenToVirtualViewCoordinates} followed by
     * {@link #virtualViewToWorkspaceCoordinates(ViewPoint, WorkspacePoint)} using an existing
     * temporary {@link ViewPoint} instance as intermediate.
     *
     * @param screenPositionIn Input coordinates of a location in absolute coordinates on the
     * screen.
     * @param workspacePositionOut Output coordinates of the same location in workspace coordinates.
     */
    // TODO(#248): Support floating point queries.
    public void screenToWorkspaceCoordinates(@Size(2) int[] screenPositionIn,
                                             WorkspacePoint workspacePositionOut) {
        screenToVirtualViewCoordinates(screenPositionIn, mTempViewPoint);
        virtualViewToWorkspaceCoordinates(mTempViewPoint, workspacePositionOut);
    }

    /**
     * Converts a point in workspace coordinates to virtual view coordinates, storing the result in
     * the second parameter. The resulting coordinates will be in the
     * {@link WorkspaceView WorkspaceView's} coordinates, relative to the current
     * workspace view offset.
     *
     * @param workspacePosition The position to convert to view coordinates.
     * @param viewPosition The Point to store the results in.
     */
    public void workspaceToVirtualViewCoordinates(WorkspacePoint workspacePosition,
                                                  ViewPoint viewPosition) {
        float workspaceX = workspacePosition.x;
        if (mRtl) {
            workspaceX *= -1;
        }
        viewPosition.x = workspaceToVirtualViewUnits(workspaceX) - mVirtualWorkspaceViewOffset.x;
        viewPosition.y = workspaceToVirtualViewUnits(workspacePosition.y) -
                mVirtualWorkspaceViewOffset.y;
    }

    /**
     * Assigns {@code rect} the given bounds, possibly flipping horizontal bounds in RTL mode.
     *
     * @param ltrStart The left coordinate in LTR mode.
     * @param top The top coordinate.
     * @param ltrEnd The right coordinate in LTR mode.
     * @param bottom The bottom coordinate.
     */
    public void setRtlAwareBounds(Rect rect, int parentWidth,
                                  int ltrStart, int top, int ltrEnd, int bottom) {
        boolean isRtl = useRtl();
        rect.left = isRtl ? parentWidth - ltrEnd : ltrStart;
        rect.top = top;
        rect.right = isRtl ? parentWidth - ltrStart : ltrEnd;
        rect.bottom = bottom;
    }

    /**
     * Set a {@link ViewPoint} and flip x coordinate in RTL mode.
     *
     * @param viewPoint The point in view coordinates to set.
     * @param x The new x coordinate in LTR mode.
     * @param y The  new y coordinate.
     */
    public void setPointMaybeFlip(ViewPoint viewPoint, int x, int y) {
        viewPoint.set(useRtl() ? -x : x, y);
    }

    /**
     * Converts a point in virtual view coordinates to workspace coordinates, storing the result in
     * the second parameter. The view position should be in the {@link WorkspaceView} coordinates in
     * pixels.
     *
     * @param viewPosition The position to convert to workspace coordinates.
     * @param outCoordinate The Point to store the results in.
     */
    public void virtualViewToWorkspaceCoordinates(ViewPoint viewPosition,
                                                  WorkspacePoint outCoordinate) {
        int workspaceX =
                virtualViewToWorkspaceUnits(viewPosition.x + mVirtualWorkspaceViewOffset.x);
        if (mRtl) {
            workspaceX *= -1;
        }
        outCoordinate.x = workspaceX;
        outCoordinate.y =
                virtualViewToWorkspaceUnits(viewPosition.y + mVirtualWorkspaceViewOffset.y);
    }

    /**
     * Returns true if the view is a child or deeper descendant of the {@link WorkspaceView}
     * associated with this WorkspaceHelper.
     *
     * @param view The potential child view in question.
     * @return True if {@code touchedView} is a descendant.
     */
    public boolean isInWorkspaceView(BlockView view) {
        return view.getWorkspaceView() == mWorkspaceView;
    }

    /**
     * @return The zoom scale of the workspace, where > 1.0 is enlarged ("zoomed in").
     */
    public float getWorkspaceZoomScale() {
        // Workspace scale is simply the scale of the WorkspaceView, equal in both directions.
        return (mWorkspaceView == null) ? 1.0f : mWorkspaceView.getScaleX();
    }

    /**
     * Gets the visible bounds of the workspace, in workspace units.
     *
     * @param outRect The {@link RectF} in which to store the bounds values.
     * @return {@code outRect}
     */
    public RectF getViewableWorkspaceBounds(RectF outRect) {
        mTempViewPoint.set(0, 0);
        virtualViewToWorkspaceCoordinates(mTempViewPoint, mTempWorkspacePoint);
        outRect.left = (int) mTempWorkspacePoint.x;
        outRect.top = (int) mTempWorkspacePoint.y;

        mTempViewPoint.set(mWorkspaceView.getWidth(), mWorkspaceView.getHeight());
        virtualViewToWorkspaceCoordinates(mTempViewPoint, mTempWorkspacePoint);
        outRect.right = (int) mTempWorkspacePoint.x;
        outRect.bottom = (int) mTempWorkspacePoint.y;
        return outRect;
    }

    /**
     * @return The ZoomBehavior for workspaces in this context.
     */
    public ZoomBehavior getZoomBehavior() {
        return mZoomBehavior;
    }

    /**
     * Updates the current RTL state for the app.
     *
     * Lint Suppressed as plan is to support down to API 16.
     *
     * @param resources The context resources to get the RTL setting from.
     */
    @SuppressLint("ObsoleteSdkInt")
    private void updateRtl(Resources resources) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            mRtl = resources.getConfiguration().getLayoutDirection()
                    == View.LAYOUT_DIRECTION_RTL;
        } else {
            mRtl = false;  // RTL not supported.
        }
    }
} public class Block extends Observable<Block.Observer> {
    private static final String TAG = "Block";

    // Observable attributes of possible concern to BlockViews.
    @Retention(SOURCE)
    @IntDef({
            UPDATE_INPUTS_FIELDS_CONNECTIONS, UPDATE_COLOR, UPDATE_COMMENT, UPDATE_IS_SHADOW,
            UPDATE_IS_DISABLED, UPDATE_IS_COLLAPSED, UPDATE_IS_EDITABLE, UPDATE_IS_DELETABLE,
            UPDATE_TOOLTIP, UPDATE_CONTEXT_MENU, UPDATE_INPUTS_INLINE, UPDATE_IS_MOVEABLE
    })
    public @interface UpdateState {}
    public static final int UPDATE_INPUTS_FIELDS_CONNECTIONS = 1 << 0;
    public static final int UPDATE_COLOR = 1 << 1;  // TODO: Not implemented/emitted
    public static final int UPDATE_COMMENT = 1 << 2;
    public static final int UPDATE_IS_SHADOW = 1 << 3;
    public static final int UPDATE_IS_DISABLED = 1 << 4;
    public static final int UPDATE_IS_COLLAPSED = 1 << 5;
    public static final int UPDATE_IS_EDITABLE = 1 << 6;
    public static final int UPDATE_IS_DELETABLE = 1 << 7;
    public static final int UPDATE_TOOLTIP = 1 << 8;  // TODO: Not implemented/emitted
    public static final int UPDATE_CONTEXT_MENU = 1 << 9;  // TODO: Not implemented/emitted
    public static final int UPDATE_INPUTS_INLINE = 1 << 10;
    public static final int UPDATE_IS_MOVEABLE = 1 << 11;
    public static final int UPDATE_WARNING = 1 << 12; // TODO: Not implemented/emitted

    public interface Observer {
        /**
         * Called when any of the following block elements have changed, possibly triggering a
         * change in the BlockView.
         * <ul>
         *     <li>Inputs</li>
         *     <li>Fields</li>
         *     <li>Mutator</li>
         *     <li>Comment</li>
         *     <li>Shadow state</li>
         *     <li>Disabled state</li>
         *     <li>Collapsed state</li>
         *     <li>Editable state</li>
         *     <li>Deletable state</li>
         * </ul>
         * @param block The block updated.
         * @param updateStateMask A bit mask of {@link UpdateState} bits for the updated parts.
         */
        void onBlockUpdated(Block block, @UpdateState int updateStateMask);
    }

    // These values are immutable once a block is created
    private final BlocklyController mController;
    private final BlockFactory mFactory;
    private final String mId;
    private final String mType;
    private boolean mIsShadow;
    private JSONObject mStyle = null;  // WARNING: Often a mutable object shared by all blocks.

    // Set by BlockFactory.applyMutator(). May only be set once.
    private Mutator mMutator = null;
    private String mMutation = null;

    // These values can be changed after creating the block
    private int mColor = ColorUtils.DEFAULT_BLOCK_COLOR;
    private List<Input> mInputList = Collections.<Input>emptyList();
    private Connection mOutputConnection;
    private Connection mNextConnection;
    private Connection mPreviousConnection;
    private List<Connection> mConnectionList = Collections.<Connection>emptyList();
    private String mTooltip = null;
    private String mComment = null;
    private boolean mHasContextMenu = true;
    private boolean mDeletable = true;
    private boolean mMovable = true;
    private boolean mEditable = true;
    private boolean mCollapsed = false;
    private boolean mDisabled = false;
    private boolean mInputsInline = false;

    // Keep track of whether inputsInline has ever been changed.
    private boolean mInputsInlineModified = false;

    private String mEventWorkspaceId = null;
    private BlocklyController.EventsCallback mEventCallback = null;
    private BlocklyController.EventsCallback mMemSafeCallback = null;

    /** Position of the block in the workspace. Only serialized for the root block. */
    private WorkspacePoint mPosition;

    /**
     * @param controller The controller for this Blockly instance.
     * @param factory The factory creating this block.
     * @param definition The definition this block instantiates.
     * @param id The globally unique identifier for this block.
     * @param isShadow Whether the block should be a shadow block (default input value block).
     * @throws BlockLoadingException When the {@link BlockDefinition} throws errors.
     */
    Block(@Nullable BlocklyController controller, @NonNull BlockFactory factory,
          @NonNull BlockDefinition definition, @NonNull String id, boolean isShadow)
            throws BlockLoadingException {
        if (controller == null || factory == null || definition == null || id == null) {
            throw new IllegalArgumentException(
                    "Tried to instantiate a block but controller, factory, definition, or id was "
                    + "null.");
        }
        mController = controller;
        mFactory = factory;
        mId = id;

        mType = definition.getTypeName();
        mStyle = definition.getStyleJson();
        mColor = definition.getColor();

        reshape(definition.createInputList(factory),
                definition.createOutputConnection(),
                definition.createPreviousConnection(),
                definition.createNextConnection());
        if (isShadow && containsVariableField()) {
            throw new BlockLoadingException("Shadow blocks may not contain variable fields.");
        }

        mInputsInline = definition.isInputsInlineDefault();
        mInputsInlineModified = false;

        mPosition = new WorkspacePoint(0, 0);
        setShadow(isShadow);

        String mutatorId = definition.getMutatorId();
        if (mutatorId != null) {
            factory.applyMutator(mutatorId, this);
        }
        List<String> extensionNames = definition.getExtensionNames();
        for (String name : extensionNames) {
            factory.applyExtension(name, this);
        }
    }

    /**
     * @return The name of the block. Not for display.
     */
    public String getType() {
        return mType;
    }

    /**
     * @return The unique identifier of the block. Not for display.
     */
    public String getId() {
        return mId;
    }

    /**
     * @return The controller for this Blockly instance.
     */
    @NonNull
    public BlocklyController getController() {
        return mController;
    }

    /**
     * {@code getEventWorkspaceId} returns the id of the "workspace", as seen by the event
     * framework. This might be the {@link Workspace#getId() workspace id}, if attached to a
     * {@link Workspace}. It may also be {@link BlocklyEvent#WORKSPACE_ID_TOOLBOX} or
     * {@link BlocklyEvent#WORKSPACE_ID_TRASH}. If the block is not attached to to any of these
     * (directly or indirectly), the event workspace should be {@code null}.
     *
     * @return The id of the "workspace", as seen by the event framework. Null if not attached.
     */
    // TODO(#567) & WARNING: This value is not set appropriately in all cases.
    @Nullable
    public String getEventWorkspaceId() {
        return mEventWorkspaceId;
    }

    /**
     * Sets the block's "workspace" id, for the purposes of events. If the block is on a proper
     * {@link Workspace}, it should be the {@link Workspace#getId()}. If the block is in the
     * toolbox or trash, the value should be {@link BlocklyEvent#WORKSPACE_ID_TOOLBOX} or
     * {@link BlocklyEvent#WORKSPACE_ID_TRASH}, respectively. When the block is detached, the event
     * workspace should be set to {@code null}.
     *
     * Setting this values recursively sets the value on all child blocks.
     *
     * @param eventWorkspaceId The workspace id, as defined by event framework.
     */
    public void setEventWorkspaceId(String eventWorkspaceId) {
        if (eventWorkspaceId == mEventWorkspaceId
                || (mEventWorkspaceId != null && mEventWorkspaceId.equals(eventWorkspaceId))) {
            return; // No-op
        }
        if (Looper.getMainLooper() != Looper.myLooper()) {
            throw new IllegalStateException(
                    "setEventWorkspaceId(..) must be called from main thread.");
        }

        mEventWorkspaceId = eventWorkspaceId;

        for (Input input : mInputList) {
            Block child = input.getConnectedBlock();
            if (child != null) {
                child.setEventWorkspaceId(eventWorkspaceId);
            }
        }
        if (mNextConnection != null) {
            Block child = mNextConnection.getTargetBlock();
            if (child != null) {
                child.setEventWorkspaceId(eventWorkspaceId);
            }
        }
    }

    /**
     * Adds to <code>outList</code> all block ids for this block and all child blocks.
     * This will not include occluded shadow block ids.
     *
     * @param outList List of ids to add to.
     */
    public void addAllBlockIds(List<String> outList) {
        outList.add(getId());
        int inputCount = mInputList.size();
        for (int i = 0; i < inputCount; ++i) {
            Input input = mInputList.get(i);
            Block connectedBlock = input.getConnectedBlock();
            if (connectedBlock != null) {
                connectedBlock.addAllBlockIds(outList);
            }
        }
        if (mNextConnection != null) {
            Block next = mNextConnection.getTargetBlock();
            if (next != null) {
                next.addAllBlockIds(outList);
            }
        }
    }

    /**
     * @return The style definition for this block.
     */
    public JSONObject getStyle() {
        return mStyle;
    }

    /**
     * @return The color this block should be drawn in.
     */
    public int getColor() {
        return mColor;
    }

    /**
     * @return Whether the user can edit field values.
     * @see #setEditable(boolean)
     */
    public boolean isEditable() {
        return mEditable;
    }

    /**
     * If a block is editable, the user can edit the values of the block's fields.  The editable
     * state does not affect whether child blocks can be connected or disconnected.
     *
     * Blocks are editable by default.
     *
     * Warning: FieldViews do not yet respect block disabled state (#303).
     *
     * @param editable
     */
    public void setEditable(boolean editable) {
        if (editable == mEditable) {
            return;
        }
        // TODO: Event support? No current spec for changes to "editable".
        mEditable = editable;
        fireUpdate(UPDATE_IS_EDITABLE);
    }

    /**
     * @return Whether the block can be moved.
     * @see #setMovable(boolean)
     */
    public boolean isMovable() {
        return mMovable;
    }

    /**
     * If a block is movable, users can drag it as a root block on the workspace, or disconnect it
     * from its parent block.
     *
     * Blocks are movable by default.
     *
     * @param movable
     */
    public void setMovable(boolean movable) {
        if (mMovable == movable) {
            return;
        }
        // TODO: Event support? No current spec for changes to "moveable".
        mMovable = movable;
        fireUpdate(UPDATE_IS_MOVEABLE);
    }

    /**
     * @return Whether the user can delete / trash this block.
     */
    public boolean isDeletable() {
        return mDeletable;
    }

    /**
     * Users can delete (i.e., drag to trash) blocks that are deletable.
     *
     * Blocks are deletable by default.
     *
     * Warning: The Android Dragger implementation does not respect deletable false state (#305).
     *
     * @param deletable
     */
    public void setDeletable(boolean deletable) {
        if (mDeletable == deletable) {
            return;
        }
        // TODO: Event support? No current spec for changes to "deletable".
        mDeletable = deletable;
        fireUpdate(UPDATE_IS_DELETABLE);
    }

    /**
     * @return Whether the block is disabled.  Does not check if parent or ancestor is disabled.
     * @see #setDisabled(boolean)
     */
    public boolean isDisabledBlock() {
        return mDisabled;
    }

    /**
     * @return Whether the block is disabled, possibly via a disabled parent or ancestor block.
     * @see #setDisabled(boolean)
     */
    public boolean isDisabled() {
        if (mDisabled) {
            return true;
        }
        Block ancestor = getParentBlock();
        while (ancestor != null) {
            if (ancestor.mDisabled) {
                return true;
            }
            ancestor = ancestor.getParentBlock();
        }
        return false;
    }

    /**
     * Disabling a block is effectively a way to comment out a block. The block is skipped during
     * code generation, including all connection value input and statement input children.  Next
     * statement children are not effected.
     *
     * By default, blocks are not disabled.
     *
     * Warning: BlockViews do not yet render disabled state. (#306).
     *
     * @param disabled
     */
    public void setDisabled(final boolean disabled) {
        if (mDisabled == disabled) {
            return;
        }
        runAsPossibleEventGroup(new Runnable() {
            @Override
            public void run() {
                mDisabled = disabled;

                // Add change event before notifying observers that might add their own events.
                maybeAddPendingChangeEvent(BlocklyEvent.ELEMENT_DISABLED, mDisabled);
                fireUpdate(UPDATE_IS_DISABLED); // Block.Observers
            }
        });
    }

    /**
     * @return Whether the block is collapsed.
     * @see #setCollapsed(boolean)
     */
    public boolean isCollapsed() {
        return mCollapsed;
    }

    /**
     * Users can collapse blocks, to render them in a condensed, text-only form.
     *
     * Warning: Not yet supported by Android BlockViews (#307).
     *
     * @param collapsed Whether the block should be collapsed.
     */
    public void setCollapsed(final boolean collapsed) {
        if (collapsed == mCollapsed) {
            return;
        }
        runAsPossibleEventGroup(new Runnable() {
            @Override
            public void run() {
                mCollapsed = collapsed;

                // Add change event before notifying observers that might add their own events.
                maybeAddPendingChangeEvent(BlocklyEvent.ELEMENT_COLLAPSED, mCollapsed);
                fireUpdate(UPDATE_IS_COLLAPSED);
            }
        });
    }

    /**
     * Gets the position of this block. The position is the top left for LTR configurations and the
     * top right for RTL configurations. This position is only meaningful for top level blocks. All
     * other blocks will have a position that is dependent on the block rendering relative to the
     * top level block.
     *
     * @return The coordinates of the start corner of this block in Workspace coordinates.
     */
    public WorkspacePoint getPosition() {
        return mPosition;
    }

    /**
     * @return A list of connections (including inputs, next, previous and outputs) on this block.
     */
    public List<Connection> getAllConnections() {
        return mConnectionList;
    }

    /**
     * Add all connections on the block to the given list.
     *
     * @param addTo The list to update.
     */
    public void getAllConnections(List<Connection> addTo) {
        addTo.addAll(mConnectionList);
    }

    /**
     * Add all connections on the block and its descendants to the given list.
     *
     * @param addTo The list to update.
     */
    public void getAllConnectionsRecursive(List<Connection> addTo) {
        getAllConnections(addTo);
        for (int i = 0; i < mConnectionList.size(); i++) {
            Connection conn = mConnectionList.get(i);
            int type = conn.getType();
            Block target = conn.getTargetBlock();
            if (type != Connection.CONNECTION_TYPE_OUTPUT
                    && type != Connection.CONNECTION_TYPE_PREVIOUS
                    && target != null) {
                target.getAllConnectionsRecursive(addTo);
            }
        }
    }

    /**
     * Set the position of this block in the workspace.
     *
     * @param x The workspace x position.
     * @param y The workspace y position.
     */
    public void setPosition(float x, float y) {
        if (Float.isNaN(x) || Float.isInfinite(x) || Float.isNaN(y) || Float.isInfinite(y)) {
            throw new IllegalArgumentException("Position must be a real, finite number.");
        }
        mPosition.x = x;
        mPosition.y = y;
    }

    /**
     * @return The comment on this block.
     */
    public String getComment() {
        return mComment;
    }

    /**
     * Set the comment on this block.
     *
     * @param comment The text of the comment.
     */
    public void setComment(@Nullable final String comment) {
        if (comment == mComment || (comment != null && comment.equals(mComment))) {
            return;
        }
        runAsPossibleEventGroup(new Runnable() {
            @Override
            public void run() {
                String oldValue = mComment;
                mComment = comment;

                // Add change event before notifying observers that might add their own events.
                maybeAddPendingChangeEvent(
                        BlocklyEvent.ELEMENT_COMMENT, /* field */ null, oldValue, mComment);
                fireUpdate(UPDATE_COMMENT);
            }
        });
    }

    /**
     * @return The set of inputs on this block.
     */
    public List<Input> getInputs() {
        return mInputList;
    }

    /**
     * @return True if and only if this block has inputs, i.e., {#link mInputList} is non-empty.
     */
    public boolean hasInputs() {
        return !mInputList.isEmpty();
    }

    /**
     * @return Whether the flag for displaying inputs in-line has been explicitly set.
     */
    public boolean getInputsInlineModified() {
        return mInputsInlineModified;
    }

    /**
     * @return The current state of the flag for displaying inputs in-line.
     */
    public boolean getInputsInline() {
        return mInputsInline;
    }

    /**
     * Set whether value inputs should be displayed inline (true), or on separate rows (false).
     * @param inputsInline True if value inputs should be show in a single line. Otherwise, false.
     */
    public void setInputsInline(final boolean inputsInline) {
        // Mark modified state for next serialization, even if the value didn't actually change.
        mInputsInlineModified = true;

        if (inputsInline == mInputsInline) {
            return;
        }
        runAsPossibleEventGroup(new Runnable() {
            @Override
            public void run() {
                mInputsInline = inputsInline;

                // Add change event before notifying observers that might add their own events.
                maybeAddPendingChangeEvent(BlocklyEvent.ELEMENT_INLINE, mInputsInline);
                fireUpdate(UPDATE_INPUTS_INLINE);
            }
        });
    }

    /**
     * Searches through the block's list of inputs and returns the first one with the given name.
     *
     * @param targetName The name of the input to search for.
     *
     * @return The first Input with that name.
     */
    public Input getInputByName(String targetName) {
        for (int i = 0; i < mInputList.size(); i++) {
            if (mInputList.get(i).getName() != null
                    && mInputList.get(i).getName().equalsIgnoreCase(targetName)) {
                return mInputList.get(i);
            }
        }
        return null;
    }

    /**
     * @return The only value input on the block, or null if there are zero or more than one.
     */
    public Input getOnlyValueInput() {
        Input result = null;
        for (int i = 0; i < mInputList.size(); i++) {
            if (mInputList.get(i).getType() == Input.TYPE_VALUE) {
                if (result != null) {
                    return null;    // Found more than one value input
                }
                result = mInputList.get(i);
            }
        }
        return result;
    }

    /**
     * Searches through all of the fields on all of the block's inputs.  Returns the first field
     * with the given name.
     *
     * @param targetName The name of the field to search for.
     *
     * @return The first Field with that name.
     */
    public Field getFieldByName(String targetName) {
        Input input;
        Field field;
        for (int i = 0; i < mInputList.size(); i++) {
            input = mInputList.get(i);
            for (int j = 0; j < input.getFields().size(); j++) {
                field = input.getFields().get(j);
                if (field.getName() != null && field.getName().equalsIgnoreCase(targetName)) {
                    return field;
                }
            }
        }
        return null;
    }

    /**
     * @return The block connected to this block's previous connection or null.
     */
    public Block getPreviousBlock() {
        return mPreviousConnection == null ? null : mPreviousConnection.getTargetBlock();
    }

    /**
     * @return The block connected to this block's next connection or null.
     */
    public Block getNextBlock() {
        return mNextConnection == null ? null : mNextConnection.getTargetBlock();
    }

    /**
     * @return The block's output Connection.
     */
    @Nullable
    public Connection getOutputConnection() {
        return mOutputConnection;
    }

    /**
     * @return The block's previous Connection.
     */
    @Nullable
    public Connection getPreviousConnection() {
        return mPreviousConnection;
    }

    /**
     * @return The block's next Connection.
     */
    @Nullable
    public Connection getNextConnection() {
        return mNextConnection;
    }

    /**
     * @return True if this is a shadow block, false otherwise.
     */
    public boolean isShadow() {
        return mIsShadow;
    }

    /**
     * @return True if this block can be dragged by the user, false otherwise.
     */
    public boolean isDraggable() {
        return !mIsShadow && mMovable;
    }

    /**
     * Creates a copy of this block and all inferior blocks connected to it.
     *
     * @return A new block tree with a copy of this block as the root.
     */
    @NonNull
    public Block deepCopy() {
        try {
            String xml = BlocklyXmlHelper.writeBlockToXml(this,
                    IOOptions.WRITE_ALL_BLOCKS_WITHOUT_ID);
            return BlocklyXmlHelper.loadOneBlockFromXml(xml, mFactory);
        } catch (BlocklySerializerException | BlockLoadingException e) {
            // This error indicates something is very wrong with the serialization / deserialization
            // framework.  Allow this to bubble up as a RuntimeException.
            throw new IllegalStateException("Failed to copy blocks.", e);
        }
    }

    /**
     * Writes information about the editable parts of the block as XML.
     *
     * @param serializer The XmlSerializer to write to.
     * @param options I/O options.
     *
     * @throws IOException
     */
    public void serialize(XmlSerializer serializer, boolean rootBlock, IOOptions options)
            throws IOException {
        serializer.startTag(null, mIsShadow ? "shadow" : "block")
                .attribute(null, "type", mType);
        if (options.isBlockIdWritten()) {
            serializer.attribute(null, "id", mId);
        }

        // The position of the block only needs to be saved if it is a top level block.
        if (rootBlock) {
            serializer.attribute(null, "x", Float.toString(mPosition.x))
                    .attribute(null, "y", Float.toString(mPosition.y));
        }

        if (isCollapsed()) {
            serializer.attribute(null, "collapsed", "true");
        }
        if (!isDeletable() && !isShadow()) {
            serializer.attribute(null, "deletable", "false");
        }
        if (isDisabled()) {
            serializer.attribute(null, "disabled", "true");
        }
        if (!isEditable()) {
            serializer.attribute(null, "editable", "false");
        }
        if (!isMovable() && !isShadow()) {
            serializer.attribute(null, "movable", "false");
        }

        // Only serialize whether the inputs are internal or external if it has been explicitly
        // modified.  This can happen if it's set explicitly in json, if it's set in xml, or if
        // it was changed by the user in the program.
        if (mInputsInlineModified) {
            serializer.attribute(null, "inline", Boolean.toString(mInputsInline));
        }

        // Serialize the mutator state before the inputs. The web code used by the code generator
        // loads XML elements in order, and the mutation may update the available inputs or fields
        // before the values are assigned.
        if (mMutator != null) {
            mMutator.serialize(serializer);
        }

        for (int i = 0; i < mInputList.size(); i++) {
            if (mInputList.get(i) != null) {
                mInputList.get(i).serialize(serializer, options);
            }
        }

        if (options.isBlockChildWritten() && getNextBlock() != null) {
            serializer.startTag(null, "next");
            getNextBlock().serialize(serializer, false, options);
            serializer.endTag(null, "next");
        }

        serializer.endTag(null, mIsShadow ? "shadow" : "block");
    }

    /**
     * @return The {@link Block} for the last non-shadow child in this sequence, possibly itself.
     */
    @NonNull
    public Block getLastBlockInSequence() {
        Block last = this;
        Block next = this.getNextBlock();
        // Protect against loops by checking for dupes?
        while (next != null && !next.isShadow()) {
            last = next;
            next = last.getNextBlock();
        }
        return last;
    }

    /**
     * Walks the chain of blocks in this block, at each stage checking if there are multiple
     * value inputs.  If there is only one value input at each block, follows that input to the
     * next block.  If at any point there is more than one value input on a block or there are no
     * value inputs on a block, returns null. If the next block in the sequence is a shadow block
     * this returns the connection before the shadow.
     *
     * @return the {@link Connection} on the only input on the last block in the chain.
     */
    @Nullable
    public Connection getLastUnconnectedInputConnection() {
        Block block = this;

        // Loop until there are no more singular, connected inputs.
        while (true) {
            Input onlyValueInput = block.getOnlyValueInput();
            if (onlyValueInput == null) {
                return null;
            }
            Connection conn = onlyValueInput.getConnection();
            if (conn == null) {
                return null;
            }
            if (!conn.isConnected()) {
                return conn;
            }
            block = conn.getTargetBlock();
            if (block.isShadow()) {
                return conn;
            }
        }
    }

    /**
     * Find the highest block in the hierarchy that this {@link Block} descends from.
     *
     * @return The highest block found.
     */
    @NonNull
    public Block getRootBlock() {
        Block block = this;
        Block parent = block.getParentBlock();
        // Go up and left as far as possible.
        while (parent != null) {
            block = parent;
            parent = block.getParentBlock();
        }
        return block;
    }

    /**
     * @return The previous or output {@link Connection} on this block, or null.
     */
    @Nullable
    public Connection getUpwardsConnection() {
        if (mPreviousConnection != null) {
            return mPreviousConnection;
        }
        if (mOutputConnection != null) {
            return mOutputConnection;
        }
        return null;
    }

    /**
     * @return The next or input {@link Connection} this block is connected to, or null.
     */
    @Nullable
    public Connection getParentConnection() {
        Connection upwards = getUpwardsConnection();
        return upwards == null ? null : upwards.getTargetConnection();
    }

    /**
     * @return The block connected to the output or previous {@link Connection}, if present.
     *         Otherwise null.
     */
    @Nullable
    public Block getParentBlock() {
        Connection parentConnection = getParentConnection();
        return parentConnection == null ? null : parentConnection.getBlock();
    }

    /**
     * Sets an event callback that will receive {@link BlocklyEvent}s for the lifetime of the block.
     * @param callback The block's callback, or null to unset.
     */
    public void setEventCallback(@Nullable BlocklyController.EventsCallback callback) {
        if (mMemSafeCallback != null) {
            mController.removeCallback(mMemSafeCallback);
            mMemSafeCallback = null;
        }
        mEventCallback = callback;
        if (mEventCallback != null) {
            mMemSafeCallback = new MemSafeEventsCallback(this);
            mController.addCallback(mMemSafeCallback);
        }
    }

    /**
     * Configures whether this block should be a shadow block. This should only be called during
     * block initialization.
     *
     * @param isShadow If true the block will act as a shadow.
     */
    public void setShadow(boolean isShadow) {
        if (mIsShadow == isShadow) {
            return;
        }

        // Unsupported: Changing while connected to the parent (Connection target type, etc.)
        Connection upConnection = getUpwardsConnection();
        if (upConnection != null && upConnection.getTargetConnection() != null) {
            throw new IllegalStateException(
                    "Cannot change block shadow state while connected to parent.");
        }
        if (isShadow) {
            if (containsVariableField(mInputList)) {
                throw new IllegalStateException("Shadow blocks cannot contain variable fields.");
            }

            // Shadow blocks cannot have non-shadow children.
            int inputCount = mInputList == null ? 0 : mInputList.size();
            for (int i = 0; i < inputCount; ++i) {
                Input input = mInputList.get(i);
                Block child = input.getConnectedBlock();  // Not the shadow connection.
                if (child != null && !child.isShadow()) {
                    throw new IllegalStateException(
                            "Cannot change block to shadow while non-shadow children are connected."
                    );
                }
            }
        }

        // State change is valid. Proceed.
        mIsShadow = isShadow;
        fireUpdate(UPDATE_IS_SHADOW);
    }

    /**
     * @return The block's {@link Mutator} id, if any. Otherwise null.
     * @see BlockFactory#applyMutator(String, Block)
     */
    @Nullable
    public final String getMutatorId() {
        return mMutator == null ? null : mMutator.getMutatorId();
    }

    /**
     * @return The block's {@link Mutator}, if any. Otherwise null.
     * @see BlockFactory#applyMutator(String, Block)
     */
    @Nullable
    public final Mutator getMutator() {
        return mMutator;
    }

    /**
     * Updates the mutation value of the block. It requires the block to have an assigned
     * {@link Mutator}, and the mutator must be able to parse the assigned string, or a
     * {@link BlockLoadingException} will be thrown. The mutation string is a XML &lt;mutation&gt;
     * tag in its serialized string form.
     *
     * @param newValue The new mutation value, a &lt;mutation&gt; tag in string form.
     * @throws BlockLoadingException If the mutator is not able to parse the mutation.
     */
    public final void setMutation(@Nullable final String newValue) throws BlockLoadingException {
        if (mMutator == null) {
            throw new IllegalStateException("No mutator attached.");
        }
        final String oldValue = mMutation;
        if (oldValue == newValue || (oldValue != null && oldValue.equals(newValue))) {
            return;
        }
        final BlockLoadingException[] loadingException = {null};
        mController.groupAndFireEvents(new Runnable() {
            @Override
            public void run() {
                try {
                    BlocklyXmlHelper.updateMutator(Block.this, mMutator, newValue);
                    mMutation = newValue;
                    mController.addPendingEvent(new BlocklyEvent.ChangeEvent(
                            BlocklyEvent.ELEMENT_MUTATE, Block.this, /* field */ null,
                            oldValue, newValue));
                } catch (BlockLoadingException e) {
                    loadingException[0] = e; // Runnable interface does not support exceptions
                }
            }
        });
        if (loadingException[0] != null) {
            throw loadingException[0];
        }
    }

    /**
     * @return The string form of the mutation.
     */
    @Nullable
    public final String getMutation() {
        return mMutation;
    }

    /**
     * {@code reshape()} updates the inputs and all connections with potentially new values,
     * changing the shape of the block. This method should only be called by the constructor, or
     * {@link Mutator}s.
     * <p/>
     * Changes to {@link Input} and their {@link Field} change by updating the whole input list.
     * Inputs can be reused, and must be reused if blocks are to remain connected to a block.
     * Removed inputs must be previously disconnected (moved or deleted), and added inputs must also
     * be empty (i.e., not connect to child blocks).
     * <p/>
     * Similarly, {@link Connection}s that should remain connected should be reused, and
     * added/removed connections must not be connected to another block. All connections must be
     * constructed with the correct connection type for their position.
     *
     * @param newInputList The new list of inputs.
     * @param updatedOutput The updated output connection, if any.
     * @param updatedPrev The updated previous connection, if any.
     * @param updatedNext The updated next connection, if any.
     */
    // TODO: Needs lots of tests.
    public void reshape(@Nullable List<Input> newInputList,
                        @Nullable Connection updatedOutput,
                        @Nullable Connection updatedPrev,
                        @Nullable Connection updatedNext) {
        if (updatedOutput != null) {
            if (updatedPrev != null) {
                throw new IllegalArgumentException(
                        "A block cannot have both an output connection and a previous connection.");
            }
            if (updatedOutput.getType() != Connection.CONNECTION_TYPE_OUTPUT) {
                throw new IllegalArgumentException(
                        "updatedOutput Connection type is not CONNECTION_TYPE_OUTPUT");
            }
        }
        if (updatedPrev != null && updatedPrev.getType() != Connection.CONNECTION_TYPE_PREVIOUS) {
            throw new IllegalArgumentException(
                    "updatedPrev Connection type is not CONNECTION_TYPE_PREVIOUS");
        }
        if (updatedNext != null && updatedNext.getType() != Connection.CONNECTION_TYPE_NEXT) {
            throw new IllegalArgumentException(
                    "updatedNext Connection type is not CONNECTION_TYPE_NEXT");
        }

        if (newInputList == null) {
            newInputList = Collections.<Input>emptyList();
        }

        List<Connection> connectionList = new ArrayList<>();
        List<Input> oldInputs = mInputList;

        for (Input in : oldInputs) {
            if (!newInputList.contains(in)) {
                if (in.getConnectedBlock() != null) {
                    // This is a critical failure, as it may leave inputs in an invalid state if
                    // another input was already removed (i.e., setBlock(null) was already called).
                    throw new IllegalStateException(
                            "Cannot remove input \"" + in.getName() + "\" while connected.");       // TODO: Make test for this
                }
                in.setBlock(null); // Reset the block reference in removed Inputs and Fields.
            }
        }
        for (Input in : newInputList) {
            if (!oldInputs.contains(in)) {
                if (in.getConnectedBlock() != null) {
                    // This is a critical failure, as it may leave inputs in an invalid state if
                    // an old input was removed above (i.e., setBlock(null) was already called).
                    throw new IllegalStateException(
                            "Cannot add input \"" + in.getName() + "\" while connected.");          // TODO: Make test for this
                }
                in.setBlock(this);
            }
            Connection inputConn = in.getConnection();
            if (inputConn != null) {
                connectionList.add(inputConn);
            }
        }

        if (updatedOutput != null) {
            updatedOutput.setBlock(this);
            connectionList.add(updatedOutput);
        }
        if (updatedPrev != null) {
            updatedPrev.setBlock(this);
            connectionList.add(updatedPrev);
        }
        if (updatedNext != null) {
            updatedNext.setBlock(this);
            connectionList.add(updatedNext);
        }

        mInputList = Collections.unmodifiableList(newInputList);
        mOutputConnection = updatedOutput;
        mPreviousConnection = updatedPrev;
        mNextConnection = updatedNext;
        mConnectionList = Collections.unmodifiableList(connectionList);

        fireUpdate(UPDATE_INPUTS_FIELDS_CONNECTIONS);
    }

    /**
     * Convenience form of {@link #reshape(List, Connection, Connection, Connection)} that preserves
     * output, previous, and next connections. This method should only be called by
     * {@link Mutator}s.
     * @param newInputList The new list of inputs.
     */
    public void reshape(@Nullable List<Input> newInputList) {
        reshape(newInputList, mOutputConnection, mPreviousConnection, mNextConnection);
    }

    /**
     * This method returns a string describing this Block in developer terms (type
     * name and ID; English only). Intended to on be used in console logs and errors.
     * @return The description.
     */
    public String toString() {
        String description = (mIsShadow ? "shadow" : "block");
        if (mType != null) {
            description = "\"" + mType + "\" " + description;  // Prefix
        }
        if (mId != null) {
            description += " (id=\"" + mId + "\")"; // Postfix
        }
        return description;
    };

    /**
     * Sets the mutator for this block.  Called from BlockFractory, and can only be called once (for
     * now).
     * @param mutator The mutator implementation.
     *
     */
    /*package private*/ void setMutator(@NonNull Mutator mutator) {
        if (mMutator != null) {
            throw new IllegalStateException("Cannot change mutators on a block.");
        }
        mMutator = mutator;
        mutator.attachToBlock(this);
    }

    /**
     * Connects to given child and shadow (even if occluded), or throws a descriptive
     * BlockLoadingException for an invalid connection.
     * @param tagName The string name of the connection, as seen in XML.
     * @param thisConn The connection point on this block.
     * @param child The child block to connect.
     * @param shadow The child shadow to connect.
     * @throws BlockLoadingException If any connection fails.
     */
    void connectOrThrow(String tagName, Connection thisConn, Block child, Block shadow)
            throws BlockLoadingException {
        if (child != null) {
            if (mIsShadow) {
                throw new BlockLoadingException(
                        this + " cannot be a parent to non-shadow " + child);
            }
            Connection childConn = child.getUpwardsConnection();
            try {
                thisConn.connect(childConn);
            } catch (IllegalArgumentException e) {
                throw new BlockLoadingException(
                        this + ": Invalid " + tagName + " connection to " + child, e);
            }
        }
        if (shadow != null) {
            Connection shadowConn = shadow.getUpwardsConnection();
            try {
                thisConn.setShadowConnection(shadowConn);
                if (!thisConn.isConnected()) {
                    // If there is no standard child block, so connect the shadow
                    thisConn.connect(shadowConn);
                }
            } catch (IllegalArgumentException e) {
                throw new BlockLoadingException(
                        this + ": Invalid " + tagName + " shadow connection to " + child, e);
            }
        }
    }

    /**
     * Makes a copy of any blocks connected to the source connection and adds the copies to the
     * destination connection. The source and destination Connections must be of the same type and
     * must be either a next or input connection.
     *
     * @param sourceConnection The connection to copy blocks from.
     * @param destConnection The connection to add copied blocks to.
     */
    private void copyConnection(Connection sourceConnection, Connection destConnection) {
        if (sourceConnection.getType() != destConnection.getType() ||
                (sourceConnection.getType() != Connection.CONNECTION_TYPE_NEXT &&
                        sourceConnection.getType() != Connection.CONNECTION_TYPE_INPUT)) {
            throw new IllegalArgumentException(
                    "Connection types must match and must be a superior connection.");
        }
        Block copy = null;
        if (sourceConnection.getShadowBlock() != null) {
            // Make a copy of the shadow if we have one and set it on the connection
            copy = sourceConnection.getShadowBlock().deepCopy();
            if (destConnection.getType() == Connection.CONNECTION_TYPE_NEXT) {
                destConnection.setShadowConnection(copy.getPreviousConnection());
            } else if (destConnection.getType() == Connection.CONNECTION_TYPE_INPUT) {
                destConnection.setShadowConnection(copy.getOutputConnection());
            }
        }

        if (sourceConnection.getTargetBlock() != null) {
            // If a block other than the shadow was connected make a copy of that
            if (sourceConnection.getTargetBlock() != sourceConnection.getShadowBlock()) {
                copy = sourceConnection.getTargetBlock().deepCopy();
            }
            // Connect a copy of whichever block was connected to the source
            if (destConnection.getType() == Connection.CONNECTION_TYPE_NEXT) {
                destConnection.connect(copy.getPreviousConnection());
            } else if (destConnection.getType() == Connection.CONNECTION_TYPE_INPUT) {
                destConnection.connect(copy.getOutputConnection());
            }
        }
    }

    /**
     * Breaks a block message up into args and text. The returned Strings should all either
     * exactly match "^%\\d+$" if they are an arg or else are just text for a label. %[0-9]+ will
     * always be treated as an argument regardless of where in the string it appears, unless the
     * % is escaped (eg "Escaped %%5 has no args")
     *
     * @param message The message to tokenize.
     * @return A list of Strings that are either an arg or plain text.
     */
    // package scoped for testing
    static List<String> tokenizeMessage(String message) {
        ArrayList<String> result = new ArrayList<>();
        if (TextUtils.isEmpty(message)) {
            return result;
        }
        boolean foundPercent = false;
        int lastSplit = 0;
        int lastPercent = -1;

        for (int i = 0, length = message.length(); i < length; i++) {
            char currChar = message.charAt(i);
            if (currChar == '%') {
                if (i + 1 < length) {
                    char nextChar = message.charAt(i + 1);
                    if (nextChar == '%' || !Character.isDigit(nextChar)) {
                        // If we have %% or this is not an arg don't pull it out of the string.
                        i++;
                        continue;
                    }
                } else {
                    // Done processing the string. Final % will be included in the last token.
                    continue;
                }
                foundPercent = true;
                lastPercent = i;
            } else if (foundPercent) {
                if (Character.isDigit(currChar)) {
                    continue;
                } else {
                    String potentialText = message.substring(lastSplit, lastPercent).trim();
                    if (!TextUtils.isEmpty(potentialText)) {
                        result.add(potentialText);
                    }
                    result.add(message.substring(lastPercent, i));
                    lastSplit = i;
                    foundPercent = false;
                }
            }
        }
        if (lastSplit != message.length() - 1) {
            // We have remaining pieces to split
            if (lastPercent > lastSplit) {
                String potentialText = message.substring(lastSplit, lastPercent).trim();
                if (!TextUtils.isEmpty(potentialText)) {
                    result.add(potentialText);
                }
                result.add(message.substring(lastPercent, message.length()));
            } else {
                String potentialText = message.substring(lastSplit, message.length()).trim();
                if (!TextUtils.isEmpty(potentialText)) {
                    result.add(potentialText);
                }
            }
        }

        return result;
    }

    /**
     * @return True if any input in this block includes a variable field. Otherwise false.
     */
    private boolean containsVariableField() {
        return containsVariableField(mInputList);
    }

    /**
     * Notifies Observers of a structure change.
     * @param updateStateMask A bit mask of {@link UpdateState} bits for the updated parts.
     */
    private void fireUpdate(@UpdateState int updateStateMask) {
        // Allow mObservers to update while notifying prior observers.
        ArrayList<Observer> observers = new ArrayList<>(mObservers);
        for (Observer observer: observers) {
            observer.onBlockUpdated(this, updateStateMask);
        }
    }

    /**
     * Runs the provided closure. If {@link #mEventWorkspaceId} is set, it will run it through
     * {@link BlocklyController#groupAndFireEvents(Runnable)}.
     * @see Field#runAsPossibleEventGroup(Runnable)
     *
     * @param runnable Code to run.
     */
    /* package private */ void runAsPossibleEventGroup(Runnable runnable) {
        if (mEventWorkspaceId == null) {
            runnable.run();
        } else {
            mController.groupAndFireEvents(runnable);
        }
    }

    /**
     * Creates and emits a {@link BlocklyEvent.ChangeEvent} only if {@link #mEventWorkspaceId} is
     * set.
     * @param element The identifying element type string.
     * @param field The field changed, if any.
     * @param oldValue The prior value, in serialized string form.
     * @param newValue The new value, in serialized string form.
     */
    /* package private */ void maybeAddPendingChangeEvent(
            String element, Field field, String oldValue, String newValue) {
        if (mEventWorkspaceId != null) {
            mController.addPendingEvent(
                    new BlocklyEvent.ChangeEvent(element, this, field, oldValue, newValue));
        }
    }

    /**
     * Creates and emits a {@link BlocklyEvent.ChangeEvent} only if {@link #mEventWorkspaceId} is
     * set.
     */
    private void maybeAddPendingChangeEvent(String element, boolean newValue) {
        if (mEventWorkspaceId != null) {
            mController.addPendingEvent(new BlocklyEvent.ChangeEvent(
                    element, this, null, Boolean.toString(!newValue), Boolean.toString(newValue)));
        }
    }

    /**
     * @param inputs {@link Input}s to check for variable fields.
     * @return True if any input in {@code inputs} includes a variable field. Otherwise false.
     */
    private static boolean containsVariableField(List<Input> inputs) {
        // Verify there's not a variable field
        int inputCount = inputs.size();
        for (int i = 0; i < inputCount; i++) {
            Input in = inputs.get(i);
            List<Field> fields = in.getFields();
            int fieldCount = fields.size();
            for (int j = 0; j < fieldCount; j++) {
                Field f = fields.get(j);
                if (f.getType() == Field.TYPE_VARIABLE) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    protected void finalize() throws Throwable {
        if (mMutator != null) {
            mMutator.detachFromBlock();
            mMutator = null;
        }
        super.finalize();
    }

    /**
     * This {@link BlocklyController.EventsCallback} implementation is designed to prevent
     * {@link #setEventCallback the block's EventCallback} from creating a reference from the
     * {@link BlocklyController controller} that prevents garbage collection. If the block or the
     * block's callback disappear, this callback will disappear.
     */
    private static class MemSafeEventsCallback implements BlocklyController.EventsCallback {
        private final BlocklyController mController;
        private final WeakReference<Block> mBlockRef;
        // Do not refer to the inner callback directly.

        MemSafeEventsCallback(@NonNull Block block) {
            mController = block.getController();
            mBlockRef = new WeakReference<Block>(block);
        }

        @Override
        public int getTypesBitmask() {
            Block block = mBlockRef.get();
            if (block != null && block.mEventCallback != null) {
                // I feel fantastic and I'm.... still alive.
                return block.mEventCallback.getTypesBitmask();
            } else {
                removeSelf();
                return 0;
            }
        }

        @Override
        public void onEventGroup(List<BlocklyEvent> events) {
            Block block = mBlockRef.get();
            if (block != null && block.mEventCallback != null) {
                // And believe me I am... still alive.
                block.mEventCallback.onEventGroup(events);
            } else {
                removeSelf();
            }
        }

        private void removeSelf() {
            // Don't change the listener while the listener list is being processed.
            new Handler().post(new Runnable() {
                @Override
                public void run() {
                    mController.removeCallback(MemSafeEventsCallback.this);
                }
            });
        }
    }
} public class BlocklyCategory {
    private static final String TAG = "BlocklyCategory";

    public static final SimpleArrayMap<String, CustomCategory> CUSTOM_CATEGORIES
            = new SimpleArrayMap<>();

    /** Array used for by {@link ColorUtils#parseColor(String, float[], int)} during I/O. **/
    private static final float[] TEMP_IO_THREAD_FLOAT_ARRAY = new float[3];

    protected final List<BlocklyCategory> mSubcategories = new ArrayList<>();
    protected final List<CategoryItem> mItems = new ArrayList<>();
    // As displayed in the toolbox.
    private String mCategoryName;
    private String mCustomType;
    private Integer mColor = null;
    private Callback mCallback;

    public void setCallback(Callback callback) {
        mCallback = callback;
    }

    /**
     * @return The user visible name of this category.
     */
    public String getCategoryName() {
        return mCategoryName;
    }

    /**
     * @return The custom type of this category.
     */
    public String getCustomType() {
        return mCustomType;
    }

    /**
     * Gets the list of blocks in this category. The list should not be modified directly, instead
     * {@link #addItem(CategoryItem)} and {@link #removeItem(CategoryItem)} should be used.
     *
     * @return The list of blocks in this category.
     */
    public List<CategoryItem> getItems() {
        return mItems;
    }

    public List<BlocklyCategory> getSubcategories() {
        return mSubcategories;
    }

    public Integer getColor() {
        return mColor;
    }

    /**
     * Add a {@link Block} to the blocks displayed in this category.
     *
     * @param item The {@link Block} to add.
     */
    public void addItem(CategoryItem item) {
        mItems.add(item);
        if (mCallback != null) {
            mCallback.onItemAdded(mItems.size() - 1, item);
        }
    }

    /**
     * Add a {@link Block} to the blocks displayed in this category at the specified index.
     *
     * @param index The index to insert the block at.
     * @param item The {@link Block} to add.
     */
    public void addItem(int index, CategoryItem item) {
        mItems.add(index, item);
        if (mCallback != null) {
            mCallback.onItemAdded(index, item);
        }
    }

    /**
     * Removes an item from this category.
     *
     * @param item The item to remove.
     * @return true if the item was found and removed, false otherwise.
     */
    public boolean removeItem(CategoryItem item) {
        int i = mItems.indexOf(item);
        if (i != -1) {
            return removeItem(i);
        }
        return false;
    }

    /**
     * Removes an item from this category.
     *
     * @param index The position of the item to remove.
     * @return true if the item was removed, otherwise an OOBE will be thrown.
     */
    public boolean removeItem(int index) {
        CategoryItem item = mItems.remove(index);
        if (mCallback != null) {
            mCallback.onItemRemoved(index, item);
        }
        return true;
    }

    /**
     * Convenience method for removing a {@link BlockItem} from this category by its block.
     *
     * @param block The block to locate and remove.
     * @return true if an item with that block was found and removed, false otherwise.
     */
    public boolean removeBlock(Block block) {
        int index = indexOf(block);
        if (index != -1) {
            return removeItem(index);
        }
        return false;
    }

    /**
     * @return The index of the given item or -1 if it's not in this category.
     */
    public int indexOf(CategoryItem item) {
        return mItems.indexOf(item);
    }

    /**
     * @return The index of the item containing the given block or -1 if it's not in this category.
     */
    public int indexOf(Block rootBlock) {
        for (int i = 0; i < mItems.size(); i++) {
            CategoryItem item = mItems.get(i);
            if (item.getType() == CategoryItem.TYPE_BLOCK) {
                Block currBlock = ((BlockItem)item).getBlock();
                if (currBlock == rootBlock) {
                    return i;
                }
            }
        }
        return -1;
    }

    /**
     * Clear the contents of this category and all subcategories; remove subcategories.
     */
    public void clear() {
        for (int i = 0; i < mSubcategories.size(); i++) {
            mSubcategories.get(i).clear();
        }
        mItems.clear();
        mSubcategories.clear();
        if (mCallback != null) {
            mCallback.onCategoryCleared();
        }
    }

    /**
     * @return True if this category contains no blocks or subcategories, false otherwise.
     */
    public boolean isEmpty() {
        return mSubcategories.isEmpty() && mItems.isEmpty();
    }

    /**
     * Fill the given list with of the {@link Block} instances in this category and its
     * subcategories.
     *
     * @param blocks The list to add to, which is not cleared before adding blocks.
     */
    public void getAllBlocksRecursive(List<Block> blocks) {
        for (CategoryItem item : mItems) {
            if (item.getType() == CategoryItem.TYPE_BLOCK) {
                blocks.add(((BlockItem) item).getBlock());
            }
        }
        for (int i = 0; i < mSubcategories.size(); i++) {
            mSubcategories.get(i).getAllBlocksRecursive(blocks);
        }
    }

    /**
     * Read the full definition of the category's contents in from XML.
     *
     * @param parser The {@link XmlPullParser} to read from.
     * @param factory The {@link BlockFactory} to use to generate blocks from their names.
     * @param workspaceId The workspaceId to set on all blocks attached to this Category.
     *
     * @return A new {@link BlocklyCategory} with the contents given by the XML.
     * @throws BlockLoadingException If any error occurs with the input. It may wrap an IOException
     *                               or XmlPullParserException as a root cause.
     */
    public static BlocklyCategory fromXml(XmlPullParser parser, BlockFactory factory,
                                          String workspaceId)
            throws BlockLoadingException {
        try {
            BlocklyCategory category = new BlocklyCategory();
            String customType = parser.getAttributeValue("", "custom");
            category.mCategoryName = parser.getAttributeValue("", "name");
            String colourAttr = parser.getAttributeValue("", "colour");
            if (!TextUtils.isEmpty(colourAttr)) {
                try {
                    category.mColor = ColorUtils.parseColor(colourAttr, TEMP_IO_THREAD_FLOAT_ARRAY);
                } catch (ParseException e) {
                    Log.w(TAG, "Invalid toolbox category colour \"" + colourAttr + "\"");
                }
            }

            // Load items and sub categories.
            int eventType = parser.next();
            PARSER_LOOP: while (eventType != XmlPullParser.END_DOCUMENT) {
                String tagname = parser.getName();
                switch (eventType) {
                    case XmlPullParser.START_TAG:
                        if (parser.getName().equalsIgnoreCase("category")) {
                            category.addSubcategory(BlocklyCategory.fromXml(parser, factory,
                                    workspaceId));
                        } else if (parser.getName().equalsIgnoreCase("block")) {
                            BlockItem blockItem = new BlockItem(factory.fromXml(parser));
                            blockItem.getBlock().setEventWorkspaceId(workspaceId);
                            category.addItem(blockItem);
                        } else if (parser.getName().equalsIgnoreCase("shadow")) {
                            throw new BlockLoadingException(
                                    "Shadow blocks may not be top level toolbox blocks. " +
                                    "(Line #" + parser.getLineNumber() + ")");
                        } else if (parser.getName().equalsIgnoreCase("label")) {
                            String text = parser.getAttributeValue(null, "text");
                            if (TextUtils.isEmpty(text)) {
                                throw new BlockLoadingException(
                                        "<label> missing text attribute. " +
                                        "(Line #" + parser.getLineNumber() + ")");
                            }
                            category.addItem(new LabelItem(text));
                        } else if (parser.getName().equalsIgnoreCase("button")) {
                            String text = parser.getAttributeValue(null, "text");
                            String callbackKey = parser.getAttributeValue(null, "callbackKey");
                            if (TextUtils.isEmpty(text) || TextUtils.isEmpty(callbackKey)) {
                                throw new BlockLoadingException(
                                        "<button> missing text and/or callbackKey attributes." +
                                        " (Line #" + parser.getLineNumber() + ")");
                            }
                            category.addItem(new ButtonItem(text, callbackKey));
                        }
                        // TODO: Support <sep> separator
                        break;
                    case XmlPullParser.END_TAG:
                        if (tagname.equalsIgnoreCase("category")) {
                            break PARSER_LOOP;
                        }
                        break;
                    default:
                        break;
                }
                eventType = parser.next();
            }

            // Process custom category.
            if (customType != null && CUSTOM_CATEGORIES.containsKey(customType)) {
                category.mCustomType = customType;
                CUSTOM_CATEGORIES.get(customType).initializeCategory(category);
            }

            return category;

        } catch (IOException | XmlPullParserException e) {
            throw new BlockLoadingException(e);
        }
    }

    /**
     * @param subcategory The category to add under this category.
     */
    public void addSubcategory(BlocklyCategory subcategory) {
        mSubcategories.add(subcategory);
    }

    /**
     * Callback class for listening to changes to this category.
     */
    public abstract static class Callback {
        /**
         * Called when an item is added to this category.
         *
         * @param index The index the item was added at.
         * @param item The item that was added.
         */
        public void onItemAdded(int index, CategoryItem item) {}

        /**
         * Called when an item is removed from this category.
         *
         * @param index The index the item was previously at.
         * @param item The item that was removed.
         */
        public void onItemRemoved(int index, CategoryItem item) {}

        /**
         * Called when the category is cleared, which removes all its subcategories and items.
         */
        public void onCategoryCleared() {}
    }

    /**
     * Wraps items that can be displayed as part of a {@link BlocklyCategory}.
     */
    public abstract static class CategoryItem {
        @Retention(RetentionPolicy.SOURCE)
        @IntDef({TYPE_BLOCK, TYPE_LABEL, TYPE_BUTTON})
        public @interface ItemType {
        }
        public static final int TYPE_BLOCK = 0;
        public static final int TYPE_LABEL = 1;
        public static final int TYPE_BUTTON = 2;

        private final @ItemType int mType;

        public CategoryItem(@ItemType int type) {
            mType = type;
        }

        public @CategoryItem.ItemType int getType() {
            return mType;
        }
    }

    /**
     * Flyout item that contains a stack blocks.
     */
    public static class BlockItem extends CategoryItem {
        private final Block mBlock;

        public BlockItem(Block block) {
            super(TYPE_BLOCK);
            mBlock = block;
        }

        public Block getBlock() {
            return mBlock;
        }
    }

    /**
     * Flyout item representing a clickable button, such as "Add Variable".
     */
    // TODO (#503): Support style and callback spec
    public static class ButtonItem extends CategoryItem {
        private final String mText;
        private final String mAction;
        public ButtonItem(String text, String action) {
            super(TYPE_BUTTON);
            mText = text;
            mAction = action;
        }

        public String getText() {
            return mText;
        }

        public String getAction() {
            return mAction;
        }
    }

    /**
     * Flyout item representing a label between groups of blocks.
     */
    // TODO (#503): Support styling
    public static class LabelItem extends CategoryItem {
        private final String mText;

        public LabelItem(String text) {
            super(TYPE_LABEL);
            mText = text;
        }

        public String getText() {
            return mText;
        }
    }
} public class Connection implements Cloneable {
    private static final String TAG = "Connection";
    /**
     * A previous connection on a block. May only be connected to a next connection. A block may
     * not have both a previous and an output connection. This is only used for the previous link
     * on a block.
     */
    public static final int CONNECTION_TYPE_PREVIOUS = 0;
    /**
     * A next connection on a block. May only be connected to a previous connection. This is used
     * for the next link on a block and for inputs that connect to a set of statement blocks.
     */
    public static final int CONNECTION_TYPE_NEXT = 1;
    /**
     * An input connection on a block. May only be connected to an output connection. This is used
     * by inputs that take a single value block.
     */
    public static final int CONNECTION_TYPE_INPUT = 2;
    /**
     * An output connection on a block. May only be connected to an input connection. A block may
     * not have both an output and a previous connection. This is only used for the output link on
     * a block.
     */
    public static final int CONNECTION_TYPE_OUTPUT = 3;

    public static final int CAN_CONNECT = 0;
    public static final int REASON_SELF_CONNECTION = 1;
    public static final int REASON_WRONG_TYPE = 2;
    public static final int REASON_MUST_DISCONNECT = 3;
    public static final int REASON_TARGET_NULL = 4;
    public static final int REASON_CHECKS_FAILED = 5;

    // If updating this, also update ConnectionManager's matchingLists and oppositeLists arrays.
    @ConnectionType
    private static final int[] OPPOSITE_TYPES = new int[]{
            CONNECTION_TYPE_NEXT, // PREVIOUS -> NEXT
            CONNECTION_TYPE_PREVIOUS, // NEXT -> PREVIOUS
            CONNECTION_TYPE_OUTPUT, // INPUT -> OUTPUT
            CONNECTION_TYPE_INPUT // OUTPUT -> INPUT
    };
    @ConnectionType
    private final int mConnectionType;
    private final String[] mConnectionChecks;

    /**
     * Position of the connection in the workspace, used by the connection manager. The position is
     * not a part of the serialized model, and is only updated when connected to a view.
     */
    private final WorkspacePoint mPosition = new WorkspacePoint();

    private Block mBlock;
    private Input mInput;
    private Connection mTargetConnection;
    // The shadow connection is only valid for next/input connections. It is not a live connection,
    // just a reference to the default shadow (if there is one) that should be connected when there
    // isn't another connection.
    private Connection mTargetShadowConnection;
    private boolean mInDragMode = false;

    public Connection(@ConnectionType int type, @Nullable String[] checks) {
        mConnectionType = type;
        mConnectionChecks = checks;
    }

    /**
     * @return A new Connection with the same type and checks but with null block,
     * input and target connection.
     */
    @Override
    public Connection clone() {
        return new Connection(this.getType(), this.getConnectionChecks());
    }

    /**
     * Check if this can be connected to the target connection.
     *
     * @param target The connection to check.
     *
     * @return True if the target can be connected, false otherwise.
     */
    public boolean canConnect(Connection target) {
        return canConnectWithReason(target) == CAN_CONNECT;
    }

    /**
     * Sets the connection (and shadow block) to use when a normal block isn't connected. This may
     * only be called on connections that belong to an input (value or statement).
     *
     * This method does not connect the shadow to the visible connection. For that, follow this call
     * with connect():
     * <pre>{@code
     * parentConnection.setShadowConnection(childConnection)
     * if (!parentConnection.isConnected()) {
     *     // If there is no standard child block, so connect the shadow
     *     parentConnection.connect(childConnection);
     * }
     * }</pre>
     *
     * @param target The connection on the shadow block to use.
     */
    public void setShadowConnection(Connection target) {
        if (target == null) {
            mTargetShadowConnection = null;
            return;
        }
        if (canConnectWithReason(target, true) != CAN_CONNECT) {
            throw new IllegalArgumentException("The shadow connection can't be connected.");
        }
        if (!target.getBlock().isShadow()) {
            throw new IllegalArgumentException("The connection does not belong to a shadow block");
        }
        mTargetShadowConnection = target;
    }

    /**
     * Connect this to another connection. If the connection is not valid a {@link RuntimeException}
     * will be thrown.
     *
     * @param target The connection to connect to.
     */
    public void connect(Connection target) {
        if (target == mTargetConnection) {
            return;
        }
        checkConnection(target);
        connectInternal(target);
        target.connectInternal(this);
    }

    /**
     * Removes the connection between this and the Connection this is connected to. If this is not
     * connected disconnect() does nothing.
     */
    public void disconnect() {
        if (mTargetConnection == null) {
            return;
        }
        Connection target = mTargetConnection;
        disconnectInternal();
        target.disconnectInternal();
    }

    public void setDragMode(boolean dragMode) {
        mInDragMode = dragMode;
    }

    public boolean inDragMode() {
        return mInDragMode;
    }

    /**
     * @return The {@link Block} this is connected to or null if it is not connected.
     */
    public Block getTargetBlock() {
        return mTargetConnection == null ? null : mTargetConnection.getBlock();
    }

    /**
     * @return The shadow {@link Block} that is used when no other block is connected or null if it
     * has no shadow block.
     */
    public Block getShadowBlock() {
        return mTargetShadowConnection == null ? null : mTargetShadowConnection.getBlock();
    }

    /**
     * @return The shadow block {@link Connection} that is used when no other block is connected or
     * null if it has no shadow block.
     */
    public Connection getShadowConnection() {
        return mTargetShadowConnection;
    }

    /**
     * Gets the set of checks for this connection. Two Connections may be connected if one of them
     * supports any connection (when this is null) or if they share at least one common check
     * value. For example, {"Number", "Integer", "MyValueType"} and {"AnotherType", "Integer"} would
     * be valid since they share "Integer" as a check.
     *
     * @return The set of checks for this connection.
     */
    public String[] getConnectionChecks() {
        return mConnectionChecks;
    }

    /**
     * Gets the type of this connection. Valid types are:
     * <ul>
     * <li>{@link #CONNECTION_TYPE_PREVIOUS}</li>
     * <li>{@link #CONNECTION_TYPE_NEXT}</li>
     * <li>{@link #CONNECTION_TYPE_INPUT}</li>
     * <li>{@link #CONNECTION_TYPE_OUTPUT}</li>
     * </ul>
     *
     * @return The type of this connection.
     */
    @ConnectionType
    public int getType() {
        return mConnectionType;
    }

    /**
     * @return The Block this connection belongs to.
     */
    public Block getBlock() {
        return mBlock;
    }

    /**
     * Sets the block that this connection is part of.
     */
    public void setBlock(Block block) {
        mBlock = block;
    }

    /**
     * @return The Input for this connection or null.
     */
    public Input getInput() {
        return mInput;
    }

    /**
     * Sets the input this connection is part of.
     */
    public void setInput(Input input) {
        mInput = input;
    }

    /**
     * @return The Connection this is connected to.
     */
    public Connection getTargetConnection() {
        return mTargetConnection;
    }

    /**
     * Sets the position of this connection in the workspace. An input position only makes sense as
     * part of a rendered block, but workspace coordinates are used to avoid having to update every
     * connection when the scaling of the view changes.
     *
     * @param x The x position in workspace coordinates.
     * @param y The y position in workspace coordinates.
     */
    public void setPosition(float x, float y) {
        mPosition.x = x;
        mPosition.y = y;
    }

    /**
     * Returns the distance between this connection and another connection.
     *
     * @param other The other {@link Connection} to measure the distance to.
     *
     * @return The distance between connections.
     */
    public double distanceFrom(Connection other) {
        float xDiff = mPosition.x - other.getPosition().x;
        float yDiff = mPosition.y - other.getPosition().y;
        return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
    }

    /**
     * @return The input's position in workspace coordinates.
     */
    public WorkspacePoint getPosition() {
        return mPosition;
    }

    /**
     * @return True if the target connection is non-null, false otherwise.
     */
    public boolean isConnected() {
        return mTargetConnection != null;
    }

    /**
     * @return Whether the connection has high priority in the context of bumping connections away.
     */
    public boolean isHighPriority() {
        return mConnectionType == CONNECTION_TYPE_INPUT || mConnectionType == CONNECTION_TYPE_NEXT;
    }

    /**
     * @param target The {@link Connection} to check compatibility with.
     *
     * @return {@code CAN_CONNECT} if the connection is legal, an error code otherwise.
     */
    @CheckResultType
    public int canConnectWithReason(Connection target) {
        return canConnectWithReason(target, false);
    }

    /**
     * @param target The {@link Connection} to check compatibility with.
     * @param ignoreDisconnect True to skip checking if the connection is already connected.
     *
     * @return {@code CAN_CONNECT} if the connection is legal, an error code otherwise.
     */
    @CheckResultType
    private int canConnectWithReason(Connection target, boolean ignoreDisconnect) {
        if (target == null || target.getBlock() == null) {
            return REASON_TARGET_NULL;
        }
        if (getBlock() != null && target.getBlock() == getBlock()) {
            return REASON_SELF_CONNECTION;
        }
        if (target.getType() != OPPOSITE_TYPES[mConnectionType]) {
            return REASON_WRONG_TYPE;
        }
        if (!ignoreDisconnect && mTargetConnection != null) {
            return REASON_MUST_DISCONNECT;
        }
        if (!checksMatch(target)) {
            return REASON_CHECKS_FAILED;
        }
        return CAN_CONNECT;
    }

    /**
     * Checks if the connection is an input.  If so, returns the input's view.  Otherwise returns
     * null.
     *
     * @return The {@link InputView} of the {@link Input} on this connection, or null if it doesn't
     * have one.
     */
    @Nullable
    public InputView getInputView() {
        return (getInput() == null) ? null : getInput().getView();
    }

    public boolean isStatementInput() {
        return (getInput() != null && getInput().getType() == Input.TYPE_STATEMENT);
    }

    private void connectInternal(Connection target) {
        mTargetConnection = target;
    }

    private void disconnectInternal() {
        mTargetConnection = null;
    }

    @VisibleForTesting
    void checkConnection(Connection target) {
        switch (canConnectWithReason(target)) {
            case CAN_CONNECT:
                break;
            case REASON_SELF_CONNECTION:
                throw new IllegalArgumentException("Cannot connect a block to itself.");
            case REASON_WRONG_TYPE:
                throw new IllegalArgumentException("Cannot connect these types.");
            case REASON_MUST_DISCONNECT:
                throw new IllegalStateException(
                        "Must disconnect from current block before connecting to a new one.");
            case REASON_TARGET_NULL:
                throw new IllegalArgumentException("Cannot connect to a null connection/block");
            case REASON_CHECKS_FAILED:
                throw new IllegalArgumentException("Cannot connect, checks do not match.");
            default:
                throw new IllegalArgumentException(
                        "Unknown connection failure, this should never happen!");
        }
    }

    private boolean checksMatch(Connection target) {
        if (mConnectionChecks == null || target.mConnectionChecks == null) {
            return true;
        }
        // The list of checks is expected to be very small (1 or 2 items usually), so the
        // n^2 approach should be fine.
        for (int i = 0; i < mConnectionChecks.length; i++) {
            for (int j = 0; j < target.mConnectionChecks.length; j++) {
                if (TextUtils.equals(mConnectionChecks[i], target.mConnectionChecks[j])) {
                    return true;
                }
            }
        }
        return false;
    }


    /***
     * Checks if the two connection types match,
     * and returns true if the connection is empty or if their type contains the same.
     * Otherwise return false,
     *
     * @param source one of the connections that will be checked.
     * @param target another connection that will be checked.
     * @return check result
     */
    public static boolean checksMatch(Connection source,Connection target){
        if(source.mConnectionChecks == null || target.mConnectionChecks == null) {
            return true;
        }

        for (int i = 0; i < source.mConnectionChecks.length; i++){
            for (int j = 0; j < target.mConnectionChecks.length; j++) {
                if (TextUtils.equals(source.mConnectionChecks[i], target.mConnectionChecks[j])) {
                    return true;
                }
            }

        }

        return false;
    }

    public static Connection cloneConnection(Connection conn) {
        if (conn == null) {
            return null;
        }
        return conn.clone();
    }

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({CONNECTION_TYPE_PREVIOUS, CONNECTION_TYPE_NEXT, CONNECTION_TYPE_INPUT,
            CONNECTION_TYPE_OUTPUT})
    public @interface ConnectionType {
    }

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({CAN_CONNECT, REASON_SELF_CONNECTION, REASON_WRONG_TYPE, REASON_MUST_DISCONNECT,
            REASON_TARGET_NULL, REASON_CHECKS_FAILED})
    public @interface CheckResultType {
    }
} public final class FieldVariable extends Field {
    private String mVariable;

    public FieldVariable(String name, String variable) {
        super(name, TYPE_VARIABLE);
        mVariable = variable;
    }

    public static FieldVariable fromJson(JSONObject json) throws BlockLoadingException {
        String name = json.optString("name");
        if (TextUtils.isEmpty(name)) {
            throw new BlockLoadingException("field_variable \"name\" attribute must not be empty.");
        }
        return new FieldVariable(name, LangUtils.interpolate(json.optString("variable", "item")));
    }

    @Override
    public FieldVariable clone() {
        return new FieldVariable(getName(), mVariable);
    }

    @Override
    public boolean setFromString(String text) {
        if (TextUtils.isEmpty(text)) {
            return false;
        }
        setVariable(text);
        return true;
    }

    /**
     * @return The name of the variable that is set.
     */
    public String getVariable() {
        return mVariable;
    }

    /**
     * Sets the variable in this field. All variables are considered global and must be unique.
     * Two variables with the same name will be considered the same variable at generation.
     */
    public void setVariable(String variable) {
        if ((mVariable == null && variable != null)
                || (mVariable != null && !mVariable.equalsIgnoreCase(variable))) {
            String oldValue = getSerializedValue();
            mVariable = variable;
            String newValue = getSerializedValue();
            fireValueChanged(oldValue, newValue);
        }
    }

    @Override
    public String getSerializedValue() {
        return mVariable;
    }
} public class LangUtils {
    private LangUtils() {
    }

    /**
     * Current Language
     */
    private static String lang = "en";

    /**
     * Get Language
     * @return Language
     */
    public static String getLang() {
        return lang;
    }

    /**
     * Set Language
     * @param newLang New Language
     */
    public static void setLang(String newLang) {
        lang = newLang;
    }

    /**
     * A Map Containing Translations
     */
    private static Map<String, String> langMap = new HashMap<>();

    /**
     * Replace Translation String with localized text using Blockly Web's translation files.
     * @param value Input Value
     * @return Translated String
     */
    public static String interpolate(String value) {
        for (Map.Entry<String, String> entry : langMap.entrySet()) {
            while (value.contains(entry.getKey())) {
                value = value.replace(entry.getKey(), entry.getValue());
            }
        }
        return value;
    }

    /**
     * Generate Map of translations.
     * @param context Context
     */
    public static void generateLang(Context context) {
        generateLang(context, getLang());
    }

    /**
     * Generate Map of translations.
     * @param context Context
     */
    private static void generateLang(Context context, String lang) {
        if (!lang.equals("en")) {
            generateLang(context, "en");
        }

        // Attempt to override language string table. Missing values will default to English loaded above.
        StringBuilder out = new StringBuilder();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(
                    new InputStreamReader(context.getAssets().open("msg/js/" + lang + ".js")));

            String line;
            while ((line = reader.readLine()) != null) {
                out.append(line);
                out.append('\n');
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        String[] lines = out.toString().split("\n");
        for (int i = 0; i < lines.length; i++) {
            lines[i] = lines[i].trim();
            if (lines[i].startsWith("Blockly.Msg[\"")) {
                boolean alias = false;

                lines[i] = lines[i].substring("Blockly.Msg[\"".length());
                String name = lines[i].substring(0, lines[i].indexOf('"'));
                if (lines[i].contains("\"] = \"")) {
                    lines[i] = lines[i].substring(lines[i].indexOf("\"] = \"") + "\"] = \"".length() - 1);
                } else {
                    lines[i] = lines[i].substring(lines[i].indexOf("\"] = Blockly.Msg[\"") + "\"] = Blockly.Msg[\"".length() - 1);
                    alias = true;
                }
                String value = lines[i].substring(1, lines[i].lastIndexOf('"'));
                if (alias) {
                    if (langMap.get("%{BKY_" + value + "}") == null) {
                        throw new RuntimeException(value + " is not defined.");
                    } else {
                        value = langMap.get("%{BKY_" + value + "}");
                    }
                }
                if (value != null) {
                    langMap.put("%{BKY_" + name + "}", value);
                }
            }
        }
    }
}
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.google.blockly.android.ui.BlockListUI;
import com.google.blockly.android.ui.CategorySelectorUI;
import com.google.blockly.android.WorkspaceFragment;
import com.google.blockly.android.clipboard.BlockClipDataHelper;
import com.google.blockly.android.clipboard.SingleMimeTypeClipDataHelper;
import com.google.blockly.android.ui.Dragger;
import com.google.blockly.android.ui.BlockGroup;
import com.google.blockly.android.ui.BlockTouchHandler;
import com.google.blockly.android.ui.BlockView;
import com.google.blockly.android.ui.BlockViewFactory;
import com.google.blockly.android.ui.InputView;
import com.google.blockly.android.ui.PendingDrag;
import com.google.blockly.android.ui.ViewPoint;
import com.google.blockly.android.ui.VirtualWorkspaceView;
import com.google.blockly.android.ui.WorkspaceHelper;
import com.google.blockly.android.ui.WorkspaceView;
import com.google.blockly.android.ui.fieldview.VariableRequestCallback;
import com.google.blockly.model.Block;
import com.google.blockly.model.BlockExtension;
import com.google.blockly.model.BlockFactory;
import com.google.blockly.model.BlocklyCategory;
import com.google.blockly.model.BlocklyEvent;
import com.google.blockly.model.BlocklySerializerException;
import com.google.blockly.model.CustomCategory;
import com.google.blockly.model.Connection;
import com.google.blockly.model.FieldVariable;
import com.google.blockly.model.Input;
import com.google.blockly.model.Mutator;
import com.google.blockly.model.ProcedureInfo;
import com.google.blockly.model.VariableInfo;
import com.google.blockly.model.Workspace;
import com.google.blockly.model.mutator.AbstractProcedureMutator;
import com.google.blockly.utils.BlockLoadingException;
import com.google.blockly.utils.LangUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.blockly.android.control;




/**
 * Controller to coordinate the state among all the major Blockly components: Workspace, Toolbox,
 * Trash, models, and views.
 * <p/>
 * All calls are expected to be called in the main thread/looper, because they create events that
 * are processed immediately. Several methods will throw an IllegalStateExceptions if called on a
 * different thread.
 */
public class BlocklyController {
    private static final String TAG = "BlocklyController";

    private static final String SNAPSHOT_BUNDLE_KEY = "com.google.blockly.snapshot";
    private static final String SERIALIZED_WORKSPACE_KEY = "SERIALIZED_WORKSPACE";

    /**
     * Callback interface for {@link BlocklyEvent}s.
     */
    public interface EventsCallback {
        /**
         * @return The bitmask of event types handled by this callback.  Must not change.
         */
        @BlocklyEvent.EventType int getTypesBitmask();

        /**
         * Called when a group of events are fired and at least one event is of a type specified by
         * {@link #getTypesBitmask}. While events in the group will always include at least one
         * event of the requested type, the group may also contain other events.
         *
         * @param events List of all the events in this group.
         */
        void onEventGroup(List<BlocklyEvent> events);
    }

    private final Context mContext;
    private final Looper mMainLooper;
    private final BlockFactory mModelFactory;
    private final BlockViewFactory mViewFactory;
    private final WorkspaceHelper mHelper;
    private final BlockClipDataHelper mClipHelper;

    private final Workspace mWorkspace;
    private final ConnectionManager mConnectionManager;
    private final ArrayList<EventsCallback> mListeners = new ArrayList<>();

    // Whether the current call stack is actively executing code intended to group and fire events.
    // See groupAndFireEvents(Runnable)
    private boolean mInEventGroup = false;

    private ArrayList<BlocklyEvent> mPendingEvents;
    private int mPendingEventsMask = 0;
    private int mEventCallbackMask = 0;

    private VirtualWorkspaceView mVirtualWorkspaceView;
    private WorkspaceView mWorkspaceView;
    private WorkspaceFragment mWorkspaceFragment = null;
    private Dragger mDragger;
    private VariableCallback mVariableCallback = null;

    private List<Block> mTempBlocks = new ArrayList<>();