/*
 * AstOptimizer.java
 *
 * Copyright (c) 2013 Mike Strobel
 *
 * This source code is based on Mono.Cecil from Jb Evain, Copyright (c) Jb Evain;
 * and ILSpy/ICSharpCode from SharpDevelop, Copyright (c) AlphaSierraPapa.
 *
 * This source code is subject to terms and conditions of the Apache License, Version 2.0.
 * A copy of the license can be found in the License.html file at the root of this distribution.
 * By using this source code in any fashion, you are agreeing to be bound by the terms of the
 * Apache License, Version 2.0.
 *
 * You must not remove this notice, or any other, from this software.
 */

package com.strobel.decompiler.ast;

import com.strobel.assembler.metadata.*;
import com.strobel.core.*;
import com.strobel.decompiler.DecompilerContext;
import com.strobel.decompiler.DecompilerSettings;
import com.strobel.functions.Function;
import com.strobel.functions.Supplier;
import com.strobel.functions.Suppliers;
import com.strobel.util.ContractUtils;

import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.strobel.core.CollectionUtilities.*;
import static com.strobel.decompiler.ast.PatternMatching.*;

@SuppressWarnings("ConstantConditions")
public final class AstOptimizer {
    private final static Logger LOG = Logger.getLogger(AstOptimizer.class.getSimpleName());

    private int _nextLabelIndex;

    public static void optimize(final DecompilerContext context, final Block method) {
        optimize(context, method, AstOptimizationStep.None);
    }

    public static void optimize(final DecompilerContext context, final Block method, final AstOptimizationStep abortBeforeStep) {
        VerifyArgument.notNull(context, "context");
        VerifyArgument.notNull(method, "method");

        LOG.fine("Beginning bytecode AST optimization...");

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.RemoveRedundantCode)) {
            return;
        }

        final AstOptimizer optimizer = new AstOptimizer();

        removeRedundantCode(method, context.getSettings());

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.ReduceBranchInstructionSet)) {
            return;
        }

        introducePreIncrementOptimization(context, method);

        for (final Block block : method.getSelfAndChildrenRecursive(Block.class)) {
            reduceBranchInstructionSet(block);
        }

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.InlineVariables)) {
            return;
        }

        final Inlining inliningPhase1 = new Inlining(context, method);

        while (inliningPhase1.inlineAllVariables()) {
            inliningPhase1.analyzeMethod();
        }

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.CopyPropagation)) {
            return;
        }

        inliningPhase1.copyPropagation();

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.RewriteFinallyBlocks)) {
            return;
        }

        rewriteFinallyBlocks(method);

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.SplitToMovableBlocks)) {
            return;
        }

        for (final Block block : method.getSelfAndChildrenRecursive(Block.class)) {
            optimizer.splitToMovableBlocks(block);
        }

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.RemoveUnreachableBlocks)) {
            return;
        }

        removeUnreachableBlocks(method);

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.TypeInference)) {
            return;
        }

        TypeAnalysis.run(context, method);

        boolean done = false;

        LOG.fine("Performing block-level bytecode AST optimizations (enable FINER for more detail)...");

        int blockNumber = 0;

        for (final Block block : method.getSelfAndChildrenRecursive(Block.class)) {
            boolean modified;
            int blockRound = 0;

            ++blockNumber;

            do {
                if (LOG.isLoggable(Level.FINER)) {
                    LOG.finer("Optimizing block #" + blockNumber + ", round " + ++blockRound + "...");
                }

                modified = false;

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.RemoveInnerClassInitSecurityChecks)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new RemoveInnerClassInitSecurityChecksOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.PreProcessShortCircuitAssignments)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new PreProcessShortCircuitAssignmentsOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.SimplifyShortCircuit)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new SimplifyShortCircuitOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.JoinBranchConditions)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new JoinBranchConditionsOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.SimplifyTernaryOperator)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new SimplifyTernaryOperatorOptimization(context, method));
                modified |= runOptimization(block, new SimplifyTernaryOperatorRoundTwoOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.JoinBasicBlocks)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new JoinBasicBlocksOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.SimplifyLogicalNot)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new SimplifyLogicalNotOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.TransformObjectInitializers)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new TransformObjectInitializersOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.TransformArrayInitializers)) {
                    done = true;
                    break;
                }

                modified |= new Inlining(context, method, true).inlineAllInBlock(block);
                modified |= runOptimization(block, new TransformArrayInitializersOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.IntroducePostIncrement)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new IntroducePostIncrementOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.InlineConditionalAssignments)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new InlineConditionalAssignmentsOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.MakeAssignmentExpressions)) {
                    done = true;
                    break;
                }

                modified |= runOptimization(block, new MakeAssignmentExpressionsOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.InlineLambdas)) {
                    return;
                }

                modified |= runOptimization(block, new InlineLambdasOptimization(context, method));

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.InlineVariables2)) {
                    done = true;
                    break;
                }

                modified |= new Inlining(context, method, true).inlineAllInBlock(block);
                new Inlining(context, method).copyPropagation();

                if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.MergeDisparateObjectInitializations)) {
                    done = true;
                    break;
                }

                modified |= mergeDisparateObjectInitializations(context, block);
            }
            while (modified);
        }

        if (done) {
            return;
        }

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.FindLoops)) {
            return;
        }

        for (final Block block : method.getSelfAndChildrenRecursive(Block.class)) {
            new LoopsAndConditions(context).findLoops(block);
        }

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.FindConditions)) {
            return;
        }

        for (final Block block : method.getSelfAndChildrenRecursive(Block.class)) {
            new LoopsAndConditions(context).findConditions(block);
        }

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.FlattenNestedMovableBlocks)) {
            return;
        }

        flattenBasicBlocks(method);

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.RemoveRedundantCode2)) {
            return;
        }

        removeRedundantCode(method, context.getSettings());

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.GotoRemoval)) {
            return;
        }

        new GotoRemoval().removeGotos(method);

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.DuplicateReturns)) {
            return;
        }

        duplicateReturnStatements(method);

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.ReduceIfNesting)) {
            return;
        }

        reduceIfNesting(method);

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.GotoRemoval2)) {
            return;
        }

        new GotoRemoval().removeGotos(method);

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.ReduceComparisonInstructionSet)) {
            return;
        }

        for (final Expression e : method.getChildrenAndSelfRecursive(Expression.class)) {
            reduceComparisonInstructionSet(e);
        }

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.RecombineVariables)) {
            return;
        }

        recombineVariables(method);

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.RemoveRedundantCode3)) {
            return;
        }

        GotoRemoval.removeRedundantCode(
            method,
            GotoRemoval.OPTION_MERGE_ADJACENT_LABELS |
            GotoRemoval.OPTION_REMOVE_REDUNDANT_RETURNS
        );

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.CleanUpTryBlocks)) {
            return;
        }

        cleanUpTryBlocks(method);

        //
        // This final inlining pass is necessary because the DuplicateReturns step and the
        // introduction of ternary operators may open up additional inlining possibilities.
        //

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.InlineVariables3)) {
            return;
        }

        final Inlining inliningPhase3 = new Inlining(context, method, true);

        inliningPhase3.inlineAllVariables();

        if (!shouldPerformStep(abortBeforeStep, AstOptimizationStep.TypeInference2)) {
            return;
        }

        TypeAnalysis.reset(context, method);
        TypeAnalysis.run(context, method);

        LOG.fine("Finished bytecode AST optimization.");
    }

    private static boolean shouldPerformStep(final AstOptimizationStep abortBeforeStep, final AstOptimizationStep nextStep) {
        if (abortBeforeStep == nextStep) {
            return false;
        }

        if (nextStep.isBlockLevelOptimization()) {
            if (LOG.isLoggable(Level.FINER)) {
                LOG.finer("Performing block-level optimization: " + nextStep + ".");
            }
        }
        else {
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Performing optimization: " + nextStep + ".");
            }
        }

        return true;
    }

    // <editor-fold defaultstate="collapsed" desc="RemoveUnreachableBlocks Step">

    private static void removeUnreachableBlocks(final Block method) {
        final BasicBlock entryBlock = first(ofType(method.getBody(), BasicBlock.class));
        final Set<Label> liveLabels = new LinkedHashSet<>();

        @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
        final Map<BasicBlock, List<Label>> embeddedLabels = new DefaultMap<>(
            new Supplier<List<Label>>() {
                @Override
                public List<Label> get() {
                    return new ArrayList<>();
                }
            }
        );

        for (final BasicBlock basicBlock : method.getChildrenAndSelfRecursive(BasicBlock.class)) {
            for (final Label label : basicBlock.getChildrenAndSelfRecursive(Label.class)) {
                embeddedLabels.get(basicBlock).add(label);
            }
        }

        for (final Expression e : method.getChildrenAndSelfRecursive(Expression.class)) {
            if (e.getOperand() instanceof Label) {
                liveLabels.add((Label) e.getOperand());
            }
            else if (e.getOperand() instanceof Label[]) {
                Collections.addAll(liveLabels, (Label[]) e.getOperand());
            }
        }

    outer:
        for (final BasicBlock basicBlock : method.getChildrenAndSelfRecursive(BasicBlock.class)) {
            final List<Node> body = basicBlock.getBody();
            final Label entryLabel = (Label) body.get(0);

            if (basicBlock != entryBlock && !liveLabels.contains(entryLabel)) {
                for (final Label label : embeddedLabels.get(basicBlock)) {
                    if (liveLabels.contains(label)) {
                        continue outer;
                    }
                }
                while (body.size() > 1) {
                    body.remove(body.size() - 1);
                }
            }
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="CleanUpTryBlocks Step">

    private static void cleanUpTryBlocks(final Block method) {
        for (final Block block : method.getChildrenAndSelfRecursive(Block.class)) {
            final List<Node> body = block.getBody();

            for (int i = 0; i < body.size(); i++) {
                if (body.get(i) instanceof TryCatchBlock) {
                    final TryCatchBlock tryCatch = (TryCatchBlock) body.get(i);

                    if (tryCatch.getTryBlock().getBody().isEmpty()) {
                        if (tryCatch.getFinallyBlock() == null || tryCatch.getFinallyBlock().getBody().isEmpty()) {
                            body.remove(i--);
                            continue;
                        }
                    }

                    if (tryCatch.getFinallyBlock() != null &&
                        tryCatch.getCatchBlocks().isEmpty()) {

                        if (tryCatch.getTryBlock().getBody().size() == 1 &&
                            tryCatch.getTryBlock().getBody().get(0) instanceof TryCatchBlock) {

                            final TryCatchBlock innerTryCatch = (TryCatchBlock) tryCatch.getTryBlock().getBody().get(0);

                            if (innerTryCatch.getFinallyBlock() == null) {
                                tryCatch.setTryBlock(innerTryCatch.getTryBlock());
                                tryCatch.getCatchBlocks().addAll(innerTryCatch.getCatchBlocks());
                            }
                        }
//                        else if (tryCatch.getFinallyBlock().getBody().isEmpty()) {
//                            body.remove(i);
//                            body.addAll(i, tryCatch.getTryBlock().getBody());
//                        }
                    }
                }
            }
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="RewriteFinallyBlocks Step">

    private static void rewriteFinallyBlocks(final Block method) {
        rewriteSynchronized(method);

        final List<Expression> a = new ArrayList<>();
        final StrongBox<Variable> v = new StrongBox<>();

        int endFinallyCount = 0;

        for (final TryCatchBlock tryCatch : method.getChildrenAndSelfRecursive(TryCatchBlock.class)) {
            final Block finallyBlock = tryCatch.getFinallyBlock();

            if (finallyBlock == null || finallyBlock.getBody().size() < 2) {
                continue;
            }

            final List<Node> body = finallyBlock.getBody();
            final List<Variable> exceptionCopies = new ArrayList<>();
            final Node lastInFinally = last(finallyBlock.getBody());

            if (matchGetArguments(body.get(0), AstCode.Store, v, a) &&
                match(a.get(0), AstCode.LoadException)) {

                body.remove(0);
                exceptionCopies.add(v.get());

                if (body.isEmpty() || !matchLoadStore(body.get(0), v.get(), v)) {
                    v.set(null);
                }
                else {
                    exceptionCopies.add(v.get());
                }

                final Label endFinallyLabel;

                if (body.size() > 1 && body.get(body.size() - 2) instanceof Label) {
                    endFinallyLabel = (Label) body.get(body.size() - 2);
                }
                else {
                    endFinallyLabel = new Label();
                    endFinallyLabel.setName("EndFinally_" + endFinallyCount++);

                    body.add(body.size() - 1, endFinallyLabel);
                }

                for (final Block b : finallyBlock.getSelfAndChildrenRecursive(Block.class)) {
                    final List<Node> blockBody = b.getBody();

                    for (int i = 0; i < blockBody.size(); i++) {
                        final Node node = blockBody.get(i);

                        if (node instanceof Expression) {
                            final Expression e = (Expression) node;

                            if (matchLoadStoreAny(node, exceptionCopies, v)) {
                                exceptionCopies.add(v.get());
                            }
                            else if (e != lastInFinally &&
                                     matchGetArguments(e, AstCode.AThrow, a) &&
                                     matchLoadAny(a.get(0), exceptionCopies)) {

                                e.setCode(AstCode.Goto);
                                e.setOperand(endFinallyLabel);
                                e.getArguments().clear();
                            }
                        }
                    }
                }

                if (body.size() >= 1 &&
                    matchGetArguments(body.get(body.size() - 1), AstCode.AThrow, a) &&
                    matchLoadAny(a.get(0), exceptionCopies)) {

                    body.set(body.size() - 1, new Expression(AstCode.EndFinally, null, Expression.MYSTERY_OFFSET));
                }
            }
        }
    }

    private static void rewriteSynchronized(final Block method) {
        final StrongBox<LockInfo> lockInfoBox = new StrongBox<>();

        for (final Block block : method.getSelfAndChildrenRecursive(Block.class)) {
            final List<Node> body = block.getBody();

            for (int i = 0; i < body.size() - 1; i++) {
                if (matchLock(body, i, lockInfoBox) &&
                    i + lockInfoBox.get().operationCount < body.size() &&
                    body.get(i + lockInfoBox.get().operationCount) instanceof TryCatchBlock) {

                    final TryCatchBlock tryCatch = (TryCatchBlock) body.get(i + lockInfoBox.get().operationCount);

                    if (tryCatch.isSynchronized()) {
                        continue;
                    }

                    final Block finallyBlock = tryCatch.getFinallyBlock();

                    if (finallyBlock != null) {
                        final List<Node> finallyBody = finallyBlock.getBody();
                        final LockInfo lockInfo = lockInfoBox.get();

                        if (finallyBody.size() == 3 &&
                            matchUnlock(finallyBody.get(1), lockInfo)) {

                            if (rewriteSynchronizedCore(tryCatch, lockInfo.operationCount)) {
                                tryCatch.setSynchronized(true);
                            }
                            else {
                                final StrongBox<Variable> v = new StrongBox<>();
                                final List<Variable> lockCopies = new ArrayList<>();

                                if (lockInfo.lockCopy != null) {
                                    lockCopies.add(lockInfo.lockCopy);
                                }

                                for (final Expression e : tryCatch.getChildrenAndSelfRecursive(Expression.class)) {
                                    if (matchLoadAny(e, lockCopies)) {
                                        e.setOperand(lockInfo.lock);
                                    }
                                    else if (matchLoadStore(e, lockInfo.lock, v) &&
                                             v.get() != lockInfo.lock) {

                                        lockCopies.add(v.get());
                                    }
                                }
                            }

                            inlineLockAccess(tryCatch, body, lockInfo);
                        }
                    }
                }
            }
        }
    }

    private static boolean rewriteSynchronizedCore(final TryCatchBlock tryCatch, final int depth) {
        final Block tryBlock = tryCatch.getTryBlock();
        final List<Node> tryBody = tryBlock.getBody();

        final LockInfo lockInfo;
        final StrongBox<LockInfo> lockInfoBox = new StrongBox<>();

    test:
        {
            switch (tryBody.size()) {
                case 0:
                    return false;
                case 1:
                    lockInfo = null;
                    break test;
            }

            if (matchLock(tryBody, 0, lockInfoBox)) {
                lockInfo = lockInfoBox.get();

                if (lockInfo.operationCount < tryBody.size() &&
                    tryBody.get(lockInfo.operationCount) instanceof TryCatchBlock) {

                    final TryCatchBlock nestedTry = (TryCatchBlock) tryBody.get(lockInfo.operationCount);
                    final Block finallyBlock = nestedTry.getFinallyBlock();

                    if (finallyBlock == null) {
                        return false;
                    }

                    final List<Node> finallyBody = finallyBlock.getBody();

                    if (finallyBody.size() == 3 &&
                        matchUnlock(finallyBody.get(1), lockInfo) &&
                        rewriteSynchronizedCore(nestedTry, depth + 1)) {

                        tryCatch.setSynchronized(true);
                        inlineLockAccess(tryCatch, tryBody, lockInfo);

                        return true;
                    }
                }
            }
            else {
                lockInfo = null;
            }

            break test;
        }

        final boolean skipTrailingBranch = matchUnconditionalBranch(tryBody.get(tryBody.size() - 1));

        if (tryBody.size() < (skipTrailingBranch ? depth + 1 : depth)) {
            return false;
        }

        final int removeTail = tryBody.size() - (skipTrailingBranch ? 1 : 0);
        final List<Node> monitorExitNodes;

        if (removeTail > 0 &&
            tryBody.get(removeTail - 1) instanceof TryCatchBlock) {

            final TryCatchBlock innerTry = (TryCatchBlock) tryBody.get(removeTail - 1);
            final List<Node> innerTryBody = innerTry.getTryBlock().getBody();

            if (matchLock(innerTryBody, 0, lockInfoBox) &&
                rewriteSynchronizedCore(innerTry, depth)) {

                inlineLockAccess(tryCatch, tryBody, lockInfo);
                tryCatch.setSynchronized(true);

                return true;
            }

            final boolean skipInnerTrailingBranch = matchUnconditionalBranch(innerTryBody.get(innerTryBody.size() - 1));

            if (innerTryBody.size() < (skipInnerTrailingBranch ? depth + 1 : depth)) {
                return false;
            }

            final int innerRemoveTail = innerTryBody.size() - (skipInnerTrailingBranch ? 1 : 0);

            monitorExitNodes = innerTryBody.subList(innerRemoveTail - depth, innerRemoveTail);
        }
        else {
            monitorExitNodes = tryBody.subList(removeTail - depth, removeTail);
        }

        final boolean removeAll = all(
            monitorExitNodes,
            new Predicate<Node>() {
                @Override
                public boolean test(final Node node) {
                    return match(node, AstCode.MonitorExit);
                }
            }
        );

        if (removeAll) {
            //
            // Remove the monitorexit instructions that we've already found.  Thank you, SubList.clear().
            //
            monitorExitNodes.clear();

            if (!tryCatch.getCatchBlocks().isEmpty()) {
                final TryCatchBlock newTryCatch = new TryCatchBlock();

                newTryCatch.setTryBlock(tryCatch.getTryBlock());
                newTryCatch.getCatchBlocks().addAll(tryCatch.getCatchBlocks());

                tryCatch.getCatchBlocks().clear();
                tryCatch.setTryBlock(new Block(newTryCatch));
            }

            inlineLockAccess(tryCatch, tryBody, lockInfo);

            tryCatch.setSynchronized(true);

            return true;
        }

        return false;
    }

    private static void inlineLockAccess(final Node owner, final List<Node> body, final LockInfo lockInfo) {
        if (lockInfo == null || lockInfo.lockInit == null) {
            return;
        }

        boolean lockCopyUsed = false;

        final StrongBox<Expression> a = new StrongBox<>();
        final List<Expression> lockAccesses = new ArrayList<>();
        final Set<Expression> lockAccessLoads = new HashSet<>();

        for (final Expression e : owner.getSelfAndChildrenRecursive(Expression.class)) {
            if (matchLoad(e, lockInfo.lock) && !lockAccessLoads.contains(e)) {

                //
                // The lock variable is used elsewhere; we can't remove it.
                //
                return;
            }

            if (lockInfo.lockCopy != null &&
                matchLoad(e, lockInfo.lockCopy) &&
                !lockAccessLoads.contains(e)) {

                lockCopyUsed = true;
            }
            else if ((matchGetArgument(e, AstCode.MonitorEnter, a) || matchGetArgument(e, AstCode.MonitorExit, a)) &&
                     (matchLoad(a.get(), lockInfo.lock) || lockInfo.lockCopy != null && matchLoad(a.get(), lockInfo.lockCopy))) {

                lockAccesses.add(e);
                lockAccessLoads.add(a.get());
            }
        }

        for (final Expression e : lockAccesses) {
            e.getArguments().set(0, lockInfo.lockInit.clone());
        }

        body.remove(lockInfo.lockStore);

        lockInfo.lockAcquire.getArguments().set(0, lockInfo.lockInit.clone());

        if (lockInfo.lockCopy != null && !lockCopyUsed) {
            body.remove(lockInfo.lockStoreCopy);
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="RemoveRedundantCode Step">

    @SuppressWarnings({ "ConstantConditions", "StatementWithEmptyBody" })
    static void removeRedundantCode(final Block method, final DecompilerSettings settings) {
        final Map<Label, MutableInteger> labelReferenceCount = new IdentityHashMap<>();

        final List<Expression> branchExpressions = method.getSelfAndChildrenRecursive(
            Expression.class,
            new Predicate<Expression>() {
                @Override
                public boolean test(final Expression e) {
                    return e.isBranch();
                }
            }
        );

        for (final Expression e : branchExpressions) {
            for (final Label branchTarget : e.getBranchTargets()) {
                final MutableInteger referenceCount = labelReferenceCount.get(branchTarget);

                if (referenceCount == null) {
                    labelReferenceCount.put(branchTarget, new MutableInteger(1));
                }
                else {
                    referenceCount.increment();
                }
            }
        }

        for (final Block block : method.getSelfAndChildrenRecursive(Block.class)) {
            final List<Node> body = block.getBody();
            final List<Node> newBody = new ArrayList<>(body.size());

            for (int i = 0, n = body.size(); i < n; i++) {
                final Node node = body.get(i);
                final StrongBox<Label> target = new StrongBox<>();
                final List<Expression> args = new ArrayList<>();

                if (matchGetOperand(node, AstCode.Goto, target) &&
                    i + 1 < body.size() &&
                    body.get(i + 1) == target.get()) {

                    //
                    // Ignore the branch.
                    //
                    if (labelReferenceCount.get(target.get()).getValue() == 1) {
                        //
                        // Ignore the label as well.
                        //
                        i++;
                    }
                }
                else if (match(node, AstCode.Nop)) {
                    //
                    // Ignore NOP.
                    //
                }
                else if (match(node, AstCode.Load)) {
                    //
                    // Ignore empty load.
                    //
                }
                else if (matchGetArguments(node, AstCode.Pop, args)) {
                    final StrongBox<Variable> variable = new StrongBox<>();

                    if (!matchGetOperand(args.get(0), AstCode.Load, variable)) {
                        throw new IllegalStateException("Pop should just have Load at this stage.");
                    }

                    //
                    // Best effort to move bytecode range to previous statement.
                    //

                    final StrongBox<Variable> previousVariable = new StrongBox<>();
                    final StrongBox<Expression> previousExpression = new StrongBox<>();

                    if (i - 1 >= 0 &&
                        matchGetArgument(body.get(i - 1), AstCode.Store, previousVariable, previousExpression) &&
                        previousVariable.get() == variable.get()) {

                        previousExpression.get().getRanges().addAll(((Expression) node).getRanges());

                        //
                        // Ignore POP.
                        //
                    }
                }
                else if (matchGetArguments(node, AstCode.Pop2, args)) {
                    final StrongBox<Variable> v1 = new StrongBox<>();
                    final StrongBox<Variable> v2 = new StrongBox<>();

                    final StrongBox<Variable> pv1 = new StrongBox<>();
                    final StrongBox<Expression> pe1 = new StrongBox<>();

                    if (args.size() == 1) {
                        if (!matchGetOperand(args.get(0), AstCode.Load, v1)) {
                            throw new IllegalStateException("Pop2 should just have Load arguments at this stage.");
                        }

                        if (!v1.get().getType().getSimpleType().isDoubleWord()) {
                            throw new IllegalStateException("Pop2 instruction has only one single-word operand.");
                        }

                        //
                        // Best effort to move bytecode range to previous statement.
                        //

                        if (i - 1 >= 0 &&
                            matchGetArgument(body.get(i - 1), AstCode.Store, pv1, pe1) &&
                            pv1.get() == v1.get()) {

                            pe1.get().getRanges().addAll(((Expression) node).getRanges());

                            //
                            // Ignore POP2.
                            //
                        }
                    }
                    else {
                        if (!matchGetOperand(args.get(0), AstCode.Load, v1) ||
                            !matchGetOperand(args.get(1), AstCode.Load, v2)) {

                            throw new IllegalStateException("Pop2 should just have Load arguments at this stage.");
                        }

                        //
                        // Best effort to move bytecode range to previous statement.
                        //

                        final StrongBox<Variable> pv2 = new StrongBox<>();
                        final StrongBox<Expression> pe2 = new StrongBox<>();

                        if (i - 2 >= 0 &&
                            matchGetArgument(body.get(i - 2), AstCode.Store, pv1, pe1) &&
                            pv1.get() == v1.get() &&
                            matchGetArgument(body.get(i - 1), AstCode.Store, pv2, pe2) &&
                            pv2.get() == v2.get()) {

                            pe1.get().getRanges().addAll(((Expression) node).getRanges());
                            pe2.get().getRanges().addAll(((Expression) node).getRanges());

                            //
                            // Ignore POP2.
                            //
                        }
                    }
                }
                else if (node instanceof Label) {
                    final Label label = (Label) node;
                    final MutableInteger referenceCount = labelReferenceCount.get(label);

                    if (referenceCount != null && referenceCount.getValue() > 0) {
                        newBody.add(label);
                    }
                }
                else if (node instanceof TryCatchBlock) {
                    final TryCatchBlock tryCatch = (TryCatchBlock) node;

                    if (!isEmptyTryCatch(tryCatch)) {
                        newBody.add(node);
                    }
                }
                else if (match(node, AstCode.Switch) && !settings.getRetainPointlessSwitches()) {
                    final Expression e = (Expression) node;
                    final Label[] targets = (Label[]) e.getOperand();

                    if (targets.length == 1) {
                        final Expression test = e.getArguments().get(0);

                        e.setCode(AstCode.Goto);
                        e.setOperand(targets[0]);

                        if (Inlining.canBeExpressionStatement(test)) {
                            newBody.add(test);
                        }

                        e.getArguments().clear();
                    }

                    newBody.add(node);
                }
                else {
                    newBody.add(node);
                }
            }

            body.clear();
            body.addAll(newBody);
        }

        //
        // DUP removal.
        //

        for (final Expression e : method.getSelfAndChildrenRecursive(Expression.class)) {
            final List<Expression> arguments = e.getArguments();

            for (int i = 0, n = arguments.size(); i < n; i++) {
                final Expression argument = arguments.get(i);

                switch (argument.getCode()) {
                    case Dup:
                    case Dup2:
                    case DupX1:
                    case DupX2:
                    case Dup2X1:
                    case Dup2X2:
                        final Expression firstArgument = argument.getArguments().get(0);
                        firstArgument.getRanges().addAll(argument.getRanges());
                        arguments.set(i, firstArgument);
                        break;
                }
            }
        }

        cleanUpTryBlocks(method);
    }

    private static boolean isEmptyTryCatch(final TryCatchBlock tryCatch) {
        if (tryCatch.getFinallyBlock() != null && !tryCatch.getFinallyBlock().getBody().isEmpty()) {
            return false;
        }

        final List<Node> body = tryCatch.getTryBlock().getBody();

        if (body.isEmpty()) {
            return true;
        }

        final StrongBox<Label> label = new StrongBox<>();

        return body.size() == 3 &&
               matchGetOperand(body.get(0), AstCode.Goto, label) &&
               body.get(1) == label.get() &&
               match(body.get(2), AstCode.EndFinally);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="IntroducePreIncrementOptimization Step">

    private static void introducePreIncrementOptimization(final DecompilerContext context, final Block method) {
        final Inlining inlining = new Inlining(context, method);

        inlining.analyzeMethod();

        for (final Block block : method.getSelfAndChildrenRecursive(Block.class)) {
            final List<Node> body = block.getBody();
            final MutableInteger position = new MutableInteger();

            for (; position.getValue() < body.size() - 1; position.increment()) {
                if (!introducePreIncrementForVariables(body, position) &&
                    !introducePreIncrementForStaticFields(body, position, inlining)) {

                    introducePreIncrementForInstanceFields(body, position, inlining);
                }
            }
        }
    }

    private static boolean introducePreIncrementForVariables(final List<Node> body, final MutableInteger position) {
        final int i = position.getValue();

        if (i >= body.size() - 1) {
            return false;
        }

        final Node node = body.get(i);
        final Node next = body.get(i + 1);

        final StrongBox<Variable> v = new StrongBox<>();
        final StrongBox<Expression> t = new StrongBox<>();
        final StrongBox<Integer> d = new StrongBox<>();

        if (!(node instanceof Expression && next instanceof Expression)) {
            return false;
        }

        final Expression e = (Expression) node;
        final Expression n = (Expression) next;

        if (matchGetArgument(e, AstCode.Inc, v, t) &&
            matchGetOperand(t.get(), AstCode.LdC, Integer.class, d) &&
            Math.abs(d.get()) == 1 &&
            match(n, AstCode.Store) &&
            matchLoad(n.getArguments().get(0), v.get())) {

            n.getArguments().set(
                0,
                new Expression(AstCode.PreIncrement, d.get(), n.getArguments().get(0).getOffset(), n.getArguments().get(0))
            );

            body.remove(i);
            position.decrement();

            return true;
        }

        return false;
    }

    private static boolean introducePreIncrementForStaticFields(final List<Node> body, final MutableInteger position, final Inlining inlining) {
        final int i = position.getValue();

        if (i >= body.size() - 3) {
            return false;
        }

        final Node n1 = body.get(i);
        final Node n2 = body.get(i + 1);
        final Node n3 = body.get(i + 2);
        final Node n4 = body.get(i + 3);

        final StrongBox<Object> tAny = new StrongBox<>();
        final List<Expression> a = new ArrayList<>();

        if (!matchGetArguments(n1, AstCode.Store, tAny, a)) {
            return false;
        }

        final Variable t = (Variable) tAny.get();

        if (!matchGetOperand(a.get(0), AstCode.GetStatic, tAny)) {
            return false;
        }

        final Variable u;
        final FieldReference f = (FieldReference) tAny.get();

        if (!(matchGetArguments(n2, AstCode.Store, tAny, a) &&
              (u = (Variable) tAny.get()) != null &&
              matchGetOperand(a.get(0), AstCode.LdC, tAny) &&
              tAny.get() instanceof Integer &&
              Math.abs((int) tAny.get()) == 1)) {

            return false;
        }

        final Variable v;
        final int amount = (int) tAny.get();

        if (matchGetArguments(n3, AstCode.Store, tAny, a) &&
            inlining.loadCounts.get(v = (Variable) tAny.get()).getValue() > 1 &&
            matchGetArguments(a.get(0), AstCode.Add, a) &&
            matchLoad(a.get(0), t) &&
            matchLoad(a.get(1), u) &&
            matchGetArguments(n4, AstCode.PutStatic, tAny, a) &&
            tAny.get() instanceof FieldReference &&
            StringUtilities.equals(f.getFullName(), ((FieldReference) tAny.get()).getFullName()) &&
            matchLoad(a.get(0), v)) {

            ((Expression) n3).getArguments().set(
                0,
                new Expression(AstCode.PreIncrement, amount, ((Expression) n1).getArguments().get(0).getOffset(), ((Expression) n1).getArguments().get(0))
            );

            body.remove(i);
            body.remove(i);
            body.remove(i + 1);
            position.decrement();

            return true;
        }

        return false;
    }

    private static boolean introducePreIncrementForInstanceFields(final List<Node> body, final MutableInteger position, final Inlining inlining) {
        final int i = position.getValue();

        if (i < 1 || i >= body.size() - 3) {
            return false;
        }

        //
        // +-------------------------------+
        // | n = load(o)                   |
        // | t = getfield(f, load(n))      |
        // | u = ldc(1)                    |
        // | v = add(load(t), load(u))     |
        // | putfield(f, load(n), load(v)) |
        // +-------------------------------+
        //   |
        //   |    +--------------------------------------------+
        //   +--> | v = postincrement(1, getfield(f, load(n))) |
        //        +--------------------------------------------+
        //
        //   == OR ==
        //
        // +---------------------------+
        // | n = ?                     |
        // | t = loadelement(o, n)     |
        // | u = ldc(1)                |
        // | v = add(load(t), load(u)) |
        // | storeelement(o, n, v)     |
        // +---------------------------+
        //   |
        //   |    +-----------------------------------------+
        //   +--> | v = postincrement(1, loadelement(o, n)) |
        //        +-----------------------------------------+
        //

        if (!(body.get(i) instanceof Expression &&
              body.get(i - 1) instanceof Expression &&
              body.get(i + 1) instanceof Expression &&
              body.get(i + 2) instanceof Expression &&
              body.get(i + 3) instanceof Expression)) {

            return false;
        }

        final Expression e0 = (Expression) body.get(i - 1);
        final Expression e1 = (Expression) body.get(i);

        final List<Expression> a = new ArrayList<>();
        final StrongBox<Variable> tVar = new StrongBox<>();

        if (!matchGetArguments(e0, AstCode.Store, tVar, a)) {
            return false;
        }

        final Variable n = tVar.get();
        final StrongBox<Object> unused = new StrongBox<>();
        final boolean field;

        if (!matchGetArguments(e1, AstCode.Store, tVar, a) ||
            !((field = match(a.get(0), AstCode.GetField)) ? matchGetArguments(a.get(0), AstCode.GetField, unused, a)
                                                          : matchGetArguments(a.get(0), AstCode.LoadElement, a)) ||
            !matchLoad(a.get(field ? 0 : 1), n)) {

            return false;
        }

        final Variable t = tVar.get();
        final Variable o = field ? null : (Variable) a.get(0).getOperand();
        final FieldReference f = field ? (FieldReference) unused.get() : null;
        final Expression e2 = (Expression) body.get(i + 1);
        final StrongBox<Integer> amount = new StrongBox<>();

        if (!matchGetArguments(e2, AstCode.Store, tVar, a) ||
            !matchGetOperand(a.get(0), AstCode.LdC, Integer.class, amount) ||
            Math.abs(amount.get()) != 1) {

            return false;
        }

        final Variable u = tVar.get();

        //  v = add(load(t), load(u))
        //  putfield(field, load(n), load(v))

        final Expression e3 = (Expression) body.get(i + 2);

        if (!matchGetArguments(e3, AstCode.Store, tVar, a) ||
            tVar.get().isGenerated() && inlining.loadCounts.get(tVar.get()).getValue() <= 1 ||
            !matchGetArguments(a.get(0), AstCode.Add, a) ||
            !matchLoad(a.get(0), t) ||
            !matchLoad(a.get(1), u)) {

            return false;
        }

        final Variable v = tVar.get();
        final Expression e4 = (Expression) body.get(i + 3);

        if (!(field ? matchGetArguments(e4, AstCode.PutField, unused, a)
                    : matchGetArguments(e4, AstCode.StoreElement, a)) ||
            !(field ? StringUtilities.equals(f.getFullName(), ((FieldReference) unused.get()).getFullName())
                    : matchLoad(a.get(0), o)) ||
            !matchLoad(a.get(field ? 0 : 1), n) ||
            !matchLoad(a.get(field ? 1 : 2), v)) {

            return false;
        }

        final Expression newExpression = new Expression(
            AstCode.PreIncrement,
            amount.get(),
            e1.getArguments().get(0).getOffset(),
            e1.getArguments().get(0)
        );

        e3.getArguments().set(0, newExpression);

        body.remove(i);
        body.remove(i);
        body.remove(i + 1);

        position.decrement();

        return true;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="ReduceBranchInstructionSet Step">

    private static void reduceBranchInstructionSet(final Block block) {
        final List<Node> body = block.getBody();

        for (int i = 0; i < body.size(); i++) {
            final Node node = body.get(i);

            if (!(node instanceof Expression)) {
                continue;
            }

            final Expression e = (Expression) node;
            final AstCode code;

            switch (e.getCode()) {
                case __TableSwitch:
                case __LookupSwitch:
                case Switch: {
                    e.getArguments().get(0).getRanges().addAll(e.getRanges());
                    e.getRanges().clear();
                    continue;
                }

                case __LCmp:
                case __FCmpL:
                case __FCmpG:
                case __DCmpL:
                case __DCmpG: {
                    if (i == body.size() - 1 || !(body.get(i + 1) instanceof Expression)) {
                        continue;
                    }

                    final Expression next = (Expression) body.get(i + 1);

                    switch (next.getCode()) {
                        case __IfEq:
                            code = AstCode.CmpEq;
                            break;
                        case __IfNe:
                            code = AstCode.CmpNe;
                            break;
                        case __IfLt:
                            code = AstCode.CmpLt;
                            break;
                        case __IfGe:
                            code = AstCode.CmpGe;
                            break;
                        case __IfGt:
                            code = AstCode.CmpGt;
                            break;
                        case __IfLe:
                            code = AstCode.CmpLe;
                            break;
                        default:
                            continue;
                    }

                    body.remove(i);
                    break;
                }

                case __IfEq:
                    e.getArguments().add(new Expression(AstCode.LdC, 0, e.getOffset()));
                    code = AstCode.CmpEq;
                    break;

                case __IfNe:
                    e.getArguments().add(new Expression(AstCode.LdC, 0, e.getOffset()));
                    code = AstCode.CmpNe;
                    break;

                case __IfLt:
                    e.getArguments().add(new Expression(AstCode.LdC, 0, e.getOffset()));
                    code = AstCode.CmpLt;
                    break;
                case __IfGe:
                    e.getArguments().add(new Expression(AstCode.LdC, 0, e.getOffset()));
                    code = AstCode.CmpGe;
                    break;
                case __IfGt:
                    e.getArguments().add(new Expression(AstCode.LdC, 0, e.getOffset()));
                    code = AstCode.CmpGt;
                    break;
                case __IfLe:
                    e.getArguments().add(new Expression(AstCode.LdC, 0, e.getOffset()));
                    code = AstCode.CmpLe;
                    break;

                case __IfICmpEq:
                    code = AstCode.CmpEq;
                    break;
                case __IfICmpNe:
                    code = AstCode.CmpNe;
                    break;
                case __IfICmpLt:
                    code = AstCode.CmpLt;
                    break;
                case __IfICmpGe:
                    code = AstCode.CmpGe;
                    break;
                case __IfICmpGt:
                    code = AstCode.CmpGt;
                    break;
                case __IfICmpLe:
                    code = AstCode.CmpLe;
                    break;
                case __IfACmpEq:
                    code = AstCode.CmpEq;
                    break;
                case __IfACmpNe:
                    code = AstCode.CmpNe;
                    break;

                case __IfNull:
                    e.getArguments().add(new Expression(AstCode.AConstNull, null, e.getOffset()));
                    code = AstCode.CmpEq;
                    break;
                case __IfNonNull:
                    e.getArguments().add(new Expression(AstCode.AConstNull, null, e.getOffset()));
                    code = AstCode.CmpNe;
                    break;

                default:
                    continue;
            }

            final Expression newExpression = new Expression(code, null, e.getOffset(), e.getArguments());

            body.set(i, new Expression(AstCode.IfTrue, e.getOperand(), newExpression.getOffset(), newExpression));
            newExpression.getRanges().addAll(e.getRanges());
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="RemoveInnerClassInitSecurityChecks Step">

    private final static class RemoveInnerClassInitSecurityChecksOptimization extends AbstractExpressionOptimization {
        protected RemoveInnerClassInitSecurityChecksOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public boolean run(final List<Node> body, final Expression head, final int position) {
            final StrongBox<Expression> getClassArgument = new StrongBox<>();
            final StrongBox<Variable> getClassArgumentVariable = new StrongBox<>();
            final StrongBox<Variable> constructorTargetVariable = new StrongBox<>();
            final StrongBox<Variable> constructorArgumentVariable = new StrongBox<>();
            final StrongBox<MethodReference> constructor = new StrongBox<>();
            final StrongBox<MethodReference> getClassMethod = new StrongBox<>();
            final List<Expression> arguments = new ArrayList<>();

            if (position > 0) {
                final Node previous = body.get(position - 1);

                arguments.clear();

                if (matchGetArguments(head, AstCode.InvokeSpecial, constructor, arguments) &&
                    arguments.size() > 1 &&
                    matchGetOperand(arguments.get(0), AstCode.Load, constructorTargetVariable) &&
                    matchGetOperand(arguments.get(1), AstCode.Load, constructorArgumentVariable) &&
                    matchGetArgument(previous, AstCode.InvokeVirtual, getClassMethod, getClassArgument) &&
                    isGetClassMethod(getClassMethod.get()) &&
                    matchGetOperand(getClassArgument.get(), AstCode.Load, getClassArgumentVariable) &&
                    getClassArgumentVariable.get() == constructorArgumentVariable.get()) {

                    final TypeReference constructorTargetType = constructorTargetVariable.get().getType();
                    final TypeReference constructorArgumentType = constructorArgumentVariable.get().getType();

                    if (constructorTargetType != null && constructorArgumentType != null) {
                        final TypeDefinition resolvedConstructorTargetType = constructorTargetType.resolve();
                        final TypeDefinition resolvedConstructorArgumentType = constructorArgumentType.resolve();

                        if (resolvedConstructorTargetType != null &&
                            resolvedConstructorArgumentType != null &&
                            resolvedConstructorTargetType.isNested() &&
                            !resolvedConstructorTargetType.isStatic() &&
                            (!resolvedConstructorArgumentType.isNested() ||
                             isEnclosedBy(resolvedConstructorTargetType, resolvedConstructorArgumentType))) {

                            body.remove(position - 1);
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        private static boolean isGetClassMethod(final MethodReference method) {
            return method.getParameters().isEmpty() &&
                   StringUtilities.equals(method.getName(), "getClass");
        }

        private static boolean isEnclosedBy(final TypeReference innerType, final TypeReference outerType) {
            if (innerType == null) {
                return false;
            }

            for (TypeReference current = innerType.getDeclaringType();
                 current != null;
                 current = current.getDeclaringType()) {

                if (MetadataResolver.areEquivalent(current, outerType)) {
                    return true;
                }
            }

            final TypeDefinition resolvedInnerType = innerType.resolve();

            return resolvedInnerType != null &&
                   isEnclosedBy(resolvedInnerType.getBaseType(), outerType);
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="ReduceComparisonInstructionSet Step">

    private static void reduceComparisonInstructionSet(final Expression expression) {
        final List<Expression> arguments = expression.getArguments();
        final Expression firstArgument = arguments.isEmpty() ? null : arguments.get(0);

        if (matchSimplifiableComparison(expression)) {
            arguments.clear();
            arguments.addAll(firstArgument.getArguments());
            expression.getRanges().addAll(firstArgument.getRanges());
        }

        if (matchReversibleComparison(expression)) {
            final AstCode reversedCode;

            switch (firstArgument.getCode()) {
                case CmpEq:
                    reversedCode = AstCode.CmpNe;
                    break;
                case CmpNe:
                    reversedCode = AstCode.CmpEq;
                    break;
                case CmpLt:
                    reversedCode = AstCode.CmpGe;
                    break;
                case CmpGe:
                    reversedCode = AstCode.CmpLt;
                    break;
                case CmpGt:
                    reversedCode = AstCode.CmpLe;
                    break;
                case CmpLe:
                    reversedCode = AstCode.CmpGt;
                    break;

                default:
                    throw ContractUtils.unreachable();
            }

            expression.setCode(reversedCode);
            expression.getRanges().addAll(firstArgument.getRanges());

            arguments.clear();
            arguments.addAll(firstArgument.getArguments());
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="SplitToMovableBlocks Step">

    private void splitToMovableBlocks(final Block block) {
        final List<Node> basicBlocks = new ArrayList<>();

        final List<Node> body = block.getBody();
        final Object firstNode = firstOrDefault(body);

        final Label entryLabel;

        if (firstNode instanceof Label) {
            entryLabel = (Label) firstNode;
        }
        else {
            entryLabel = new Label();
            entryLabel.setName("Block_" + (_nextLabelIndex++));
        }

        BasicBlock basicBlock = new BasicBlock();
        List<Node> basicBlockBody = basicBlock.getBody();

        basicBlocks.add(basicBlock);
        basicBlockBody.add(entryLabel);

        block.setEntryGoto(new Expression(AstCode.Goto, entryLabel, Expression.MYSTERY_OFFSET));

        if (!body.isEmpty()) {
            if (body.get(0) != entryLabel) {
                basicBlockBody.add(body.get(0));
            }

            for (int i = 1; i < body.size(); i++) {
                final Node lastNode = body.get(i - 1);
                final Node currentNode = body.get(i);

                //
                // Start a new basic block if necessary.
                //
                if (currentNode instanceof Label ||
                    currentNode instanceof TryCatchBlock ||
                    lastNode.isConditionalControlFlow() ||
                    lastNode.isUnconditionalControlFlow()) {

                    //
                    // Try to reuse the label.
                    //
                    final Label label = currentNode instanceof Label ? (Label) currentNode
                                                                     : new Label("Block_" + (_nextLabelIndex++));

                    //
                    // Terminate the last block.
                    //
                    if (!lastNode.isUnconditionalControlFlow()) {
                        basicBlockBody.add(new Expression(AstCode.Goto, label, Expression.MYSTERY_OFFSET));
                    }

                    //
                    // Start the new block.
                    //
                    basicBlock = new BasicBlock();
                    basicBlocks.add(basicBlock);
                    basicBlockBody = basicBlock.getBody();
                    basicBlockBody.add(label);

                    //
                    // Add the node to the basic block.
                    //
                    if (currentNode != label) {
                        basicBlockBody.add(currentNode);
                    }

                    if (currentNode instanceof TryCatchBlock) {
                        //
                        // If we have a TryCatchBlock with all nested blocks exiting to the same label,
                        // go ahead and insert an explicit jump to that label.  This prevents us from
                        // potentially adding a jump to the next node that would never actually be followed
                        // (possibly throwing a wrench in FindLoopsAndConditions later).
                        //
                        final Label exitLabel = checkExit(currentNode);

                        if (exitLabel != null) {
                            body.add(i + 1, new Expression(AstCode.Goto, exitLabel, Expression.MYSTERY_OFFSET));
                        }
                    }
                }
                else {
                    basicBlockBody.add(currentNode);
                }
            }
        }

        body.clear();
        body.addAll(basicBlocks);
    }

    private Label checkExit(final Node node) {
        if (node == null) {
            return null;
        }

        if (node instanceof BasicBlock) {
            return checkExit(lastOrDefault(((BasicBlock) node).getBody()));
        }

        if (node instanceof TryCatchBlock) {
            final TryCatchBlock tryCatch = (TryCatchBlock) node;
            final Label exitLabel = checkExit(lastOrDefault(tryCatch.getTryBlock().getBody()));

            if (exitLabel == null) {
                return null;
            }

            for (final CatchBlock catchBlock : tryCatch.getCatchBlocks()) {
                if (checkExit(lastOrDefault(catchBlock.getBody())) != exitLabel) {
                    return null;
                }
            }

            final Block finallyBlock = tryCatch.getFinallyBlock();

            if (finallyBlock != null && checkExit(lastOrDefault(finallyBlock.getBody())) != exitLabel) {
                return null;
            }

            return exitLabel;
        }

        if (node instanceof Expression) {
            final Expression expression = (Expression) node;
            final AstCode code = expression.getCode();

            if (code == AstCode.Goto) {
                return (Label) expression.getOperand();
            }
        }

        return null;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="SimplifyShortCircuit Step">

    private static final class SimplifyShortCircuitOptimization extends AbstractBasicBlockOptimization {
        public SimplifyShortCircuitOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public final boolean run(final List<Node> body, final BasicBlock head, final int position) {
            assert body.contains(head);

            final StrongBox<Expression> condition = new StrongBox<>();
            final StrongBox<Label> trueLabel = new StrongBox<>();
            final StrongBox<Label> falseLabel = new StrongBox<>();

            final StrongBox<Expression> nextCondition = new StrongBox<>();
            final StrongBox<Label> nextTrueLabel = new StrongBox<>();
            final StrongBox<Label> nextFalseLabel = new StrongBox<>();

            if (matchLastAndBreak(head, AstCode.IfTrue, trueLabel, condition, falseLabel)) {
                for (int pass = 0; pass < 2; pass++) {
                    //
                    // On second pass, swap labels and negate expression of the first branch.
                    // It is slightly ugly, but much better than copy-pasting the whole block.
                    //

                    final Label nextLabel = pass == 0 ? trueLabel.get() : falseLabel.get();
                    final Label otherLabel = pass == 0 ? falseLabel.get() : trueLabel.get();
                    final boolean negate = pass == 1;

                    final BasicBlock next = labelToBasicBlock.get(nextLabel);

                    //
                    // Try to match short circuit operators, e.g.,:
                    //
                    // c = a || b
                    // c = a && b
                    //
                    if (body.contains(next) &&
                        next != head &&
                        labelGlobalRefCount.get(nextLabel).getValue() == 1 &&
                        matchSingleAndBreak(next, AstCode.IfTrue, nextTrueLabel, nextCondition, nextFalseLabel) &&
                        (otherLabel == nextFalseLabel.get() || otherLabel == nextTrueLabel.get())) {

                        //
                        // Create short circuit branch.
                        //
                        final Expression logicExpression;

                        if (otherLabel == nextFalseLabel.get()) {
                            logicExpression = makeLeftAssociativeShortCircuit(
                                AstCode.LogicalAnd,
                                negate ? new Expression(AstCode.LogicalNot, null, condition.get().getOffset(), condition.get()) : condition.get(),
                                nextCondition.get()
                            );
                        }
                        else {
                            logicExpression = makeLeftAssociativeShortCircuit(
                                AstCode.LogicalOr,
                                negate ? condition.get() : new Expression(AstCode.LogicalNot, null, condition.get().getOffset(), condition.get()),
                                nextCondition.get()
                            );
                        }

                        final List<Node> headBody = head.getBody();

                        removeTail(headBody, AstCode.IfTrue, AstCode.Goto);

                        headBody.add(new Expression(AstCode.IfTrue, nextTrueLabel.get(), logicExpression.getOffset(), logicExpression));
                        headBody.add(new Expression(AstCode.Goto, nextFalseLabel.get(), logicExpression.getOffset()));

                        labelGlobalRefCount.get(trueLabel.get()).decrement();
                        labelGlobalRefCount.get(falseLabel.get()).decrement();

                        //
                        // Remove the inlined branch from scope.
                        //
                        removeOrThrow(body, next);

                        return true;
                    }
                }
            }

            return false;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="PreProcessShortCircuitAssignments Step">

    private static final class PreProcessShortCircuitAssignmentsOptimization extends AbstractBasicBlockOptimization {
        public PreProcessShortCircuitAssignmentsOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public final boolean run(final List<Node> body, final BasicBlock head, final int position) {
            assert body.contains(head);

            final StrongBox<Expression> condition = new StrongBox<>();
            final StrongBox<Label> trueLabel = new StrongBox<>();
            final StrongBox<Label> falseLabel = new StrongBox<>();

            if (matchLastAndBreak(head, AstCode.IfTrue, trueLabel, condition, falseLabel)) {
                final StrongBox<Label> nextTrueLabel = new StrongBox<>();
                final StrongBox<Label> nextFalseLabel = new StrongBox<>();

                final StrongBox<Variable> sourceVariable = new StrongBox<>();
                final StrongBox<Expression> assignedValue = new StrongBox<>();
                final StrongBox<Expression> equivalentLoad = new StrongBox<>();

                final StrongBox<Expression> left = new StrongBox<>();
                final StrongBox<Expression> right = new StrongBox<>();

                boolean modified = false;

                for (int pass = 0; pass < 2; pass++) {
                    //
                    // On second pass, swap labels and negate expression of the first branch.
                    // It is slightly ugly, but much better than copy-pasting the whole block.
                    //

                    final Label nextLabel = pass == 0 ? trueLabel.get() : falseLabel.get();
                    final Label otherLabel = pass == 0 ? falseLabel.get() : trueLabel.get();

                    final BasicBlock next = labelToBasicBlock.get(nextLabel);
                    final BasicBlock other = labelToBasicBlock.get(otherLabel);

                    //
                    // Try to match short circuit operators, e.g.,:
                    //
                    // c = a || b
                    // c = a && b
                    //
                    if (body.contains(next) &&
                        next != head &&
                        labelGlobalRefCount.get(nextLabel).getValue() == 1 &&
                        matchLastAndBreak(next, AstCode.IfTrue, nextTrueLabel, condition, nextFalseLabel) &&
                        (otherLabel == nextFalseLabel.get() || otherLabel == nextTrueLabel.get())) {

                        final List<Node> nextBody = next.getBody();
                        final List<Node> otherBody = other.getBody();

                        while (nextBody.size() > 3 &&
                               matchAssignment(nextBody.get(nextBody.size() - 3), assignedValue, equivalentLoad) &&
                               matchLoad(assignedValue.value, sourceVariable) &&
                               matchComparison(condition.value, left, right)) {

                            if (matchLoad(left.value, sourceVariable.value)) {
                                condition.value.getArguments().set(0, (Expression) nextBody.get(nextBody.size() - 3));
                                nextBody.remove(nextBody.size() - 3);
                                modified = true;
                            }
                            else if (matchLoad(right.value, sourceVariable.value) && !containsMatch(left.value, equivalentLoad.value)) {
                                condition.value.getArguments().set(1, (Expression) nextBody.get(nextBody.size() - 3));
                                nextBody.remove(nextBody.size() - 3);
                                modified = true;
                            }
                            else {
                                break;
                            }
                        }

                        final boolean modifiedNext = modified;

                        modified = false;

                        while (matchAssignmentAndConditionalBreak(other, assignedValue, condition, trueLabel, falseLabel, equivalentLoad) &&
                               matchLoad(assignedValue.value, sourceVariable) &&
                               matchComparison(condition.value, left, right)) {

                            if (matchLoad(left.value, sourceVariable.value)) {
                                condition.value.getArguments().set(0, (Expression) otherBody.get(otherBody.size() - 3));
                                otherBody.remove(otherBody.size() - 3);
                                modified = true;
                            }
                            else if (matchLoad(right.value, sourceVariable.value) && !containsMatch(left.value, equivalentLoad.value)) {
                                condition.value.getArguments().set(1, (Expression) otherBody.get(otherBody.size() - 3));
                                otherBody.remove(otherBody.size() - 3);
                                modified = true;
                            }
                            else {
                                break;
                            }
                        }

                        final boolean modifiedOther = modified;

                        if (modifiedNext || modifiedOther) {
                            final Inlining inlining = new Inlining(context, method);

                            if (modifiedNext) {
                                inlining.inlineAllInBasicBlock(next);
                            }

                            if (modifiedOther) {
                                inlining.inlineAllInBasicBlock(other);
                            }

                            return true;
                        }

                        return false;
                    }
                }
            }

            return false;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="InlineConditionalAssignments Step">

    private static final class InlineConditionalAssignmentsOptimization extends AbstractBasicBlockOptimization {
        public InlineConditionalAssignmentsOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public final boolean run(final List<Node> body, final BasicBlock head, final int position) {
            assert body.contains(head);

            final StrongBox<Expression> condition = new StrongBox<>();
            final StrongBox<Label> trueLabel = new StrongBox<>();
            final StrongBox<Label> falseLabel = new StrongBox<>();

            if (matchLastAndBreak(head, AstCode.IfTrue, trueLabel, condition, falseLabel)) {
                final StrongBox<Variable> sourceVariable = new StrongBox<>();
                final StrongBox<Expression> assignedValue = new StrongBox<>();
                final StrongBox<Expression> equivalentLoad = new StrongBox<>();
                final StrongBox<Expression> left = new StrongBox<>();
                final StrongBox<Expression> right = new StrongBox<>();

                final Label thenLabel = trueLabel.value;
                final Label elseLabel = falseLabel.value;
                final BasicBlock thenSuccessor = labelToBasicBlock.get(thenLabel);
                final BasicBlock elseSuccessor = labelToBasicBlock.get(elseLabel);

                boolean modified = false;

                if (matchAssignmentAndConditionalBreak(elseSuccessor, assignedValue, condition, trueLabel, falseLabel, equivalentLoad) &&
                    matchLoad(assignedValue.value, sourceVariable) &&
                    matchComparison(condition.value, left, right)) {

                    final List<Node> b = elseSuccessor.getBody();

                    if (matchLoad(left.value, sourceVariable.value)) {
                        condition.value.getArguments().set(0, (Expression) b.get(b.size() - 3));
                        b.remove(b.size() - 3);
                        modified = true;
                    }
                    else if (matchLoad(right.value, sourceVariable.value) && !containsMatch(left.value, equivalentLoad.value)) {
                        condition.value.getArguments().set(1, (Expression) b.get(b.size() - 3));
                        b.remove(b.size() - 3);
                        modified = true;
                    }
                }

                if (matchAssignmentAndConditionalBreak(thenSuccessor, assignedValue, condition, trueLabel, falseLabel, equivalentLoad) &&
                    matchLoad(assignedValue.value, sourceVariable) &&
                    matchComparison(condition.value, left, right)) {

                    final List<Node> b = thenSuccessor.getBody();

                    if (matchLoad(left.value, sourceVariable.value)) {
                        condition.value.getArguments().set(0, (Expression) b.get(b.size() - 3));
                        b.remove(b.size() - 3);
                        modified = true;
                    }
                    else if (matchLoad(right.value, sourceVariable.value) && !containsMatch(left.value, equivalentLoad.value)) {
                        condition.value.getArguments().set(1, (Expression) b.get(b.size() - 3));
                        b.remove(b.size() - 3);
                        modified = true;
                    }
                }

                return modified;
            }

            return false;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="JoinBasicBlocks Step">

    private final static class JoinBasicBlocksOptimization extends AbstractBasicBlockOptimization {
        protected JoinBasicBlocksOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public final boolean run(final List<Node> body, final BasicBlock head, final int position) {
            final StrongBox<Label> nextLabel = new StrongBox<>();
            final List<Node> headBody = head.getBody();
            final BasicBlock nextBlock;

            final Node secondToLast = CollectionUtilities.getOrDefault(headBody, headBody.size() - 2);

            if (secondToLast != null &&
                !secondToLast.isConditionalControlFlow() &&
                matchGetOperand(headBody.get(headBody.size() - 1), AstCode.Goto, nextLabel) &&
                labelGlobalRefCount.get(nextLabel.get()).getValue() == 1 &
                (nextBlock = labelToBasicBlock.get(nextLabel.get())) != null &&
                nextBlock != EMPTY_BLOCK &&
                body.contains(nextBlock) &&
                nextBlock.getBody().get(0) == nextLabel.get() &&
                !CollectionUtilities.any(nextBlock.getBody(), Predicates.instanceOf(BasicBlock.class))) {

                final Node secondInNext = getOrDefault(nextBlock.getBody(), 1);

                if (secondInNext instanceof TryCatchBlock) {
                    final Block tryBlock = ((TryCatchBlock) secondInNext).getTryBlock();
                    final Node firstInTry = firstOrDefault(tryBlock.getBody());

                    if (firstInTry instanceof BasicBlock) {
                        final Node firstInTryBody = firstOrDefault(((BasicBlock) firstInTry).getBody());

                        if (firstInTryBody instanceof Label &&
                            labelGlobalRefCount.get(firstInTryBody).getValue() > 1) {

                            return false;
                        }
                    }
                }

                removeTail(headBody, AstCode.Goto);
                nextBlock.getBody().remove(0);
                headBody.addAll(nextBlock.getBody());
                removeOrThrow(body, nextBlock);
                return true;
            }

            return false;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="SimplifyTernaryOperator Step">

    private final static class SimplifyTernaryOperatorOptimization extends AbstractBasicBlockOptimization {
        protected SimplifyTernaryOperatorOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public final boolean run(final List<Node> body, final BasicBlock head, final int position) {
            final StrongBox<Expression> condition = new StrongBox<>();
            final StrongBox<Label> trueLabel = new StrongBox<>();
            final StrongBox<Label> falseLabel = new StrongBox<>();
            final StrongBox<Variable> trueVariable = new StrongBox<>();
            final StrongBox<Expression> trueExpression = new StrongBox<>();
            final StrongBox<Label> trueFall = new StrongBox<>();
            final StrongBox<Variable> falseVariable = new StrongBox<>();
            final StrongBox<Expression> falseExpression = new StrongBox<>();
            final StrongBox<Label> falseFall = new StrongBox<>();

            final StrongBox<Object> unused = new StrongBox<>();

            if (matchLastAndBreak(head, AstCode.IfTrue, trueLabel, condition, falseLabel) &&
                labelGlobalRefCount.get(trueLabel.value).getValue() == 1 &&
                labelGlobalRefCount.get(falseLabel.value).getValue() == 1 &&
                body.contains(labelToBasicBlock.get(trueLabel.value)) &&
                body.contains(labelToBasicBlock.get(falseLabel.value))) {

                if (((matchSingleAndBreak(labelToBasicBlock.get(trueLabel.value), AstCode.Store, trueVariable, trueExpression, trueFall) &&
                      matchSingleAndBreak(labelToBasicBlock.get(falseLabel.value), AstCode.Store, falseVariable, falseExpression, falseFall) &&
                      trueVariable.value == falseVariable.value &&
                      trueFall.value == falseFall.value) ||
                     (matchSingle(labelToBasicBlock.get(trueLabel.value), AstCode.Return, unused, trueExpression) &&
                      matchSingle(labelToBasicBlock.get(falseLabel.value), AstCode.Return, unused, falseExpression)))) {

                    final boolean isStore = trueVariable.value != null;
                    final AstCode opCode = isStore ? AstCode.Store : AstCode.Return;
                    final TypeReference returnType = isStore ? trueVariable.value.getType() : context.getCurrentMethod().getReturnType();

                    final boolean returnTypeIsBoolean = TypeAnalysis.isBoolean(returnType);

                    final StrongBox<Boolean> leftBooleanValue = new StrongBox<>();
                    final StrongBox<Boolean> rightBooleanValue = new StrongBox<>();

                    final Expression newExpression;

                    // a ? true:false  is equivalent to  a
                    // a ? false:true  is equivalent to  !a
                    // a ? true : b    is equivalent to  a || b
                    // a ? b : true    is equivalent to  !a || b
                    // a ? b : false   is equivalent to  a && b
                    // a ? false : b   is equivalent to  !a && b

                    if (returnTypeIsBoolean &&
                        matchBooleanConstant(trueExpression.value, leftBooleanValue) &&
                        matchBooleanConstant(falseExpression.value, rightBooleanValue) &&
                        (leftBooleanValue.value && !rightBooleanValue.value ||
                         !leftBooleanValue.value && rightBooleanValue.value)) {

                        //
                        // It can be expressed as a trivial expression.
                        //
                        if (leftBooleanValue.value) {
                            newExpression = condition.value;
                        }
                        else {
                            newExpression = new Expression(AstCode.LogicalNot, null, condition.value.getOffset(), condition.value);
                            newExpression.setInferredType(BuiltinTypes.Boolean);
                        }
                    }
                    else if ((returnTypeIsBoolean || TypeAnalysis.isBoolean(falseExpression.value.getInferredType())) &&
                             matchBooleanConstant(trueExpression.value, leftBooleanValue)) {

                        //
                        // It can be expressed as a logical expression.
                        //
                        if (leftBooleanValue.value) {
                            newExpression = makeLeftAssociativeShortCircuit(
                                AstCode.LogicalOr,
                                condition.value,
                                falseExpression.value
                            );
                        }
                        else {
                            newExpression = makeLeftAssociativeShortCircuit(
                                AstCode.LogicalAnd,
                                new Expression(AstCode.LogicalNot, null, condition.value.getOffset(), condition.value),
                                falseExpression.value
                            );
                        }
                    }
                    else if ((returnTypeIsBoolean || TypeAnalysis.isBoolean(trueExpression.value.getInferredType())) &&
                             matchBooleanConstant(falseExpression.value, rightBooleanValue)) {

                        //
                        // It can be expressed as a logical expression.
                        //
                        if (rightBooleanValue.value) {
                            newExpression = makeLeftAssociativeShortCircuit(
                                AstCode.LogicalOr,
                                new Expression(AstCode.LogicalNot, null, condition.value.getOffset(), condition.value),
                                trueExpression.value
                            );
                        }
                        else {
                            newExpression = makeLeftAssociativeShortCircuit(
                                AstCode.LogicalAnd,
                                condition.value,
                                trueExpression.value
                            );
                        }
                    }
                    else {
                        //
                        // Ternary operator tends to create long, complicated return statements.
                        //
                        if (opCode == AstCode.Return) {
                            return false;
                        }

                        //
                        // Only simplify generated variables.
                        //
                        if (opCode == AstCode.Store && !trueVariable.value.isGenerated()) {
                            return false;
                        }

                        //
                        // Create ternary expression.
                        //
                        // Default behavior seems to be to invert the condition.  Try to reverse it.
                        //

                        if (simplifyLogicalNotArgument(condition.value)) {
                            newExpression = new Expression(
                                AstCode.TernaryOp,
                                null,
                                condition.value.getOffset(),
                                condition.value,
                                falseExpression.value,
                                trueExpression.value
                            );
                        }
                        else {
                            newExpression = new Expression(
                                AstCode.TernaryOp,
                                null,
                                condition.value.getOffset(),
                                condition.value,
                                trueExpression.value,
                                falseExpression.value
                            );
                        }
                    }

                    final List<Node> headBody = head.getBody();

                    removeTail(headBody, AstCode.IfTrue, AstCode.Goto);
                    headBody.add(new Expression(opCode, trueVariable.value, newExpression.getOffset(), newExpression));

                    if (isStore) {
                        headBody.add(new Expression(AstCode.Goto, trueFall.value, trueFall.value.getOffset()));
                    }

                    //
                    // Remove the old basic blocks.
                    //

                    removeOrThrow(body, labelToBasicBlock.get(trueLabel.value));
                    removeOrThrow(body, labelToBasicBlock.get(falseLabel.value));

                    return true;
                }
                else {
                    final StrongBox<Label> innerTrue = new StrongBox<>();
                    final StrongBox<Label> innerFalse = new StrongBox<>();
                    final StrongBox<Label> trueBreak = new StrongBox<>();
                    final StrongBox<Label> falseBreak = new StrongBox<>();
                    final StrongBox<Label> intermediateJump = new StrongBox<>();

                    if (matchSingleAndBreak(labelToBasicBlock.get(trueLabel.value), AstCode.IfTrue, innerTrue, trueExpression, trueFall) &&
                        matchSingleAndBreak(labelToBasicBlock.get(falseLabel.value), AstCode.IfTrue, unused, falseExpression, falseFall) &&
                        unused.value == innerTrue.value &&
                        matchLast(labelToBasicBlock.get(falseFall.value), AstCode.Goto, innerFalse)) {

                        final StrongBox<Expression> innerTrueExpression = new StrongBox<>();
                        final StrongBox<Expression> innerFalseExpression = new StrongBox<>();

                        //
                        // (a ? b : c) ? d : e
                        //
                        if (labelGlobalRefCount.get(innerTrue.value).getValue() == 2 &&
                            labelGlobalRefCount.get(innerFalse.value).getValue() == 2 &&
                            matchSingleAndBreak(labelToBasicBlock.get(innerTrue.value), AstCode.Store, trueVariable, innerTrueExpression, trueBreak) &&
                            matchSingleAndBreak(labelToBasicBlock.get(innerFalse.value), AstCode.Store, falseVariable, innerFalseExpression, falseBreak) &&
                            trueVariable.value == falseVariable.value &&
                            trueFall.value == innerFalse.value &&
                            trueBreak.value == falseBreak.value) {

                            final Expression newCondition;
                            final Expression newExpression;

                            final boolean negateInner = simplifyLogicalNotArgument(trueExpression.value);

                            if (negateInner && !simplifyLogicalNotArgument(falseExpression.value)) {
                                final Expression newFalseExpression = new Expression(AstCode.LogicalNot, null, falseExpression.value.getOffset(), falseExpression.value);
                                newFalseExpression.getRanges().addAll(falseExpression.value.getRanges());
                                falseExpression.set(newFalseExpression);
                            }

                            if (simplifyLogicalNotArgument(condition.value)) {
                                newCondition = new Expression(
                                    AstCode.TernaryOp,
                                    null,
                                    condition.value.getOffset(),
                                    condition.value,
                                    falseExpression.value,
                                    trueExpression.value
                                );
                            }
                            else {
                                newCondition = new Expression(
                                    AstCode.TernaryOp,
                                    null,
                                    condition.value.getOffset(),
                                    condition.value,
                                    trueExpression.value,
                                    falseExpression.value
                                );
                            }

                            if (negateInner) {
                                newExpression = new Expression(
                                    AstCode.TernaryOp,
                                    null,
                                    newCondition.getOffset(),
                                    newCondition,
                                    innerFalseExpression.value,
                                    innerTrueExpression.value
                                );
                            }
                            else {
                                newExpression = new Expression(
                                    AstCode.TernaryOp,
                                    null,
                                    newCondition.getOffset(),
                                    newCondition,
                                    innerTrueExpression.value,
                                    innerFalseExpression.value
                                );
                            }

                            final List<Node> headBody = head.getBody();

                            removeTail(headBody, AstCode.IfTrue, AstCode.Goto);

                            headBody.add(new Expression(AstCode.Store, trueVariable.value, newExpression.getOffset(), newExpression));
                            headBody.add(new Expression(AstCode.Goto, trueBreak.value, trueBreak.value.getOffset()));

                            //
                            // Remove the old basic blocks.
                            //

                            removeOrThrow(body, labelToBasicBlock.get(trueLabel.value));
                            removeOrThrow(body, labelToBasicBlock.get(falseLabel.value));
                            removeOrThrow(body, labelToBasicBlock.get(falseFall.value));
                            removeOrThrow(body, labelToBasicBlock.get(innerTrue.value));
                            removeOrThrow(body, labelToBasicBlock.get(innerFalse.value));

                            return true;
                        }

                        //
                        // (a ? b : c)
                        //
                        if (matchSingleAndBreak(labelToBasicBlock.get(innerTrue.value), AstCode.Store, trueVariable, innerTrueExpression, trueBreak) &&
                            (matchSingleAndBreak(labelToBasicBlock.get(falseFall.value), AstCode.Store, falseVariable, innerFalseExpression, falseBreak) ||
                             (matchSimpleBreak(labelToBasicBlock.get(falseFall.value), intermediateJump) &&
                              matchSingleAndBreak(
                                  labelToBasicBlock.get(intermediateJump.value),
                                  AstCode.Store,
                                  falseVariable,
                                  innerFalseExpression,
                                  falseBreak
                              ))) &&
                            trueVariable.value == falseVariable.value &&
                            trueBreak.value == falseBreak.value) {

                            final List<Expression> arguments = condition.value.getArguments();
                            final Expression oldCondition = condition.value.clone();

                            condition.value.setCode(AstCode.TernaryOp);
                            arguments.clear();

                            Collections.addAll(
                                arguments,
                                simplifyLogicalNot(oldCondition),
                                simplifyLogicalNot(trueExpression.value),
                                simplifyLogicalNot(falseExpression.value)
                            );

                            final List<Node> headBody = head.getBody();

                            ((Expression) headBody.get(headBody.size() - 2)).setOperand(innerTrue.value);

                            if (matchSimpleBreak(labelToBasicBlock.get(falseFall.value), intermediateJump)) {
                                if (labelGlobalRefCount.get(falseFall.value).getValue() == 1) {
                                    removeOrThrow(body, labelToBasicBlock.get(falseFall.value));
                                }
                                ((Expression) headBody.get(headBody.size() - 1)).setOperand(intermediateJump.value);
                            }
                            else {
                                ((Expression) headBody.get(headBody.size() - 1)).setOperand(falseFall.value);
                            }

                            if (labelGlobalRefCount.get(trueFall.value).getValue() == 1) {
                                removeOrThrow(body, labelToBasicBlock.get(trueFall.value));
                            }

                            removeOrThrow(body, labelToBasicBlock.get(trueLabel.value));
                            removeOrThrow(body, labelToBasicBlock.get(falseLabel.value));

                            return true;
                        }
                    }
                }
            }

            return false;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="SimplifyTernaryOperatorRoundTwo Step">

    private final static class SimplifyTernaryOperatorRoundTwoOptimization extends AbstractExpressionOptimization {
        protected SimplifyTernaryOperatorRoundTwoOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public boolean run(final List<Node> body, final Expression head, final int position) {
            final BooleanBox modified = new BooleanBox();
            final Expression simplified = simplify(head, modified);

            if (simplified != head) {
                body.set(position, simplified);
            }

            return modified.get();
        }

        private static Expression simplify(final Expression head, final BooleanBox modified) {
            if (match(head, AstCode.TernaryOp)) {
                return simplifyTernaryDirect(head);
            }

            final List<Expression> arguments = head.getArguments();

            for (int i = 0; i < arguments.size(); i++) {
                final Expression argument = arguments.get(i);
                final Expression simplified = simplify(argument, modified);

                if (simplified != argument) {
                    arguments.set(i, simplified);
                    modified.set(true);
                }
            }

            final AstCode opType = head.getCode();

            if (opType != AstCode.CmpEq && opType != AstCode.CmpNe) {
                return head;
            }

            final Boolean right = matchBooleanConstant(arguments.get(1));

            if (right == null) {
                return head;
            }

            final Expression ternary = arguments.get(0);

            if (ternary.getCode() != AstCode.TernaryOp) {
                return head;
            }

            final Boolean ifTrue = matchBooleanConstant(ternary.getArguments().get(1));
            final Boolean ifFalse = matchBooleanConstant(ternary.getArguments().get(2));

            if (ifTrue == null || ifFalse == null || ifTrue.equals(ifFalse)) {
                return head;
            }

            final boolean invert = !ifTrue.equals(right) ^ opType == AstCode.CmpNe;
            final Expression condition = ternary.getArguments().get(0);

            condition.getRanges().addAll(ternary.getRanges());
            modified.set(true);

            return invert ? new Expression(AstCode.LogicalNot, null, condition.getOffset(), condition)
                          : condition;
        }

        private static Expression simplifyTernaryDirect(final Expression head) {
            final List<Expression> a = new ArrayList<>();

            final StrongBox<Variable> v;
            final StrongBox<Expression> left;
            final StrongBox<Expression> right;

            //
            // c ? (v = a) : (v = b) => v = c ? a : b;
            //
            if (matchGetArguments(head, AstCode.TernaryOp, a)) {
                if (matchGetArgument(a.get(1), AstCode.Store, v = new StrongBox<>(), left = new StrongBox<>()) &&
                    matchStore(a.get(2), v.get(), right = new StrongBox<>())) {

                    final Expression condition = a.get(0);
                    final Expression leftValue = left.value;
                    final Expression rightValue = right.value;

                    final Expression newTernary = new Expression(
                        AstCode.TernaryOp,
                        null,
                        condition.getOffset(),
                        condition,
                        leftValue,
                        rightValue
                    );

                    head.setCode(AstCode.Store);
                    head.setOperand(v.get());
                    head.getArguments().clear();
                    head.getArguments().add(newTernary);

                    newTernary.getRanges().addAll(head.getRanges());

                    return head;
                }
                else {
                    final Boolean ifTrue = matchBooleanConstant(head.getArguments().get(1));
                    final Boolean ifFalse = matchBooleanConstant(head.getArguments().get(2));

                    if (ifTrue == null || ifFalse == null || ifTrue.equals(ifFalse)) {
                        return head;
                    }

                    final boolean invert = Boolean.FALSE.equals(ifTrue);
                    final Expression condition = head.getArguments().get(0);

                    condition.getRanges().addAll(head.getRanges());

                    return invert ? new Expression(AstCode.LogicalNot, null, condition.getOffset(), condition)
                                  : condition;
                }
            }

            return head;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="SimplifyLogicalNot Step">

    private final static class SimplifyLogicalNotOptimization extends AbstractExpressionOptimization {
        protected SimplifyLogicalNotOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public final boolean run(final List<Node> body, final Expression head, final int position) {
            final BooleanBox modified = new BooleanBox();
            final Expression simplified = simplifyLogicalNot(head, modified);

            assert simplified == null;

            return modified.get();
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="TransformObjectInitializers Step">

    private final static class TransformObjectInitializersOptimization extends AbstractExpressionOptimization {
        protected TransformObjectInitializersOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public boolean run(final List<Node> body, final Expression head, final int position) {
            if (position >= body.size() - 1) {
                return false;
            }

            final StrongBox<Variable> v = new StrongBox<>();
            final StrongBox<Expression> newObject = new StrongBox<>();
            final StrongBox<TypeReference> objectType = new StrongBox<>();
            final StrongBox<MethodReference> constructor = new StrongBox<>();
            final List<Expression> arguments = new ArrayList<>();

            if (position < body.size() - 1 &&
                matchGetArgument(head, AstCode.Store, v, newObject) &&
                matchGetOperand(newObject.get(), AstCode.__New, objectType)) {

                final Node next = body.get(position + 1);

                if (matchGetArguments(next, AstCode.InvokeSpecial, constructor, arguments) &&
                    !arguments.isEmpty() &&
                    matchLoad(arguments.get(0), v.get())) {

                    final Expression initExpression = new Expression(AstCode.InitObject, constructor.get(), ((Expression) next).getOffset());

                    arguments.remove(0);
                    initExpression.getArguments().addAll(arguments);
                    initExpression.getRanges().addAll(((Expression) next).getRanges());
                    head.getArguments().set(0, initExpression);
                    body.remove(position + 1);

                    return true;
                }
            }

            if (matchGetArguments(head, AstCode.InvokeSpecial, constructor, arguments) &&
                constructor.get().isConstructor() &&
                !arguments.isEmpty() &&
                matchGetArgument(arguments.get(0), AstCode.Store, v, newObject) &&
                matchGetOperand(newObject.get(), AstCode.__New, objectType)) {

                final Expression initExpression = new Expression(AstCode.InitObject, constructor.get(), newObject.get().getOffset());

                arguments.remove(0);

                initExpression.getArguments().addAll(arguments);
                initExpression.getRanges().addAll(head.getRanges());

                body.set(position, new Expression(AstCode.Store, v.get(), initExpression.getOffset(), initExpression));

                return true;
            }

            return false;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="MergeDisparateObjectInitializations Step">

    private static boolean mergeDisparateObjectInitializations(final DecompilerContext context, final Block method) {
        final Inlining inlining = new Inlining(context, method);
        final Map<Node, Node> parentLookup = new IdentityHashMap<>();
        final Map<Variable, Expression> newExpressions = new IdentityHashMap<>();

        final StrongBox<Variable> variable = new StrongBox<>();
        final StrongBox<MethodReference> ctor = new StrongBox<>();
        final List<Expression> args = new ArrayList<>();

        boolean anyChanged = false;

        parentLookup.put(method, Node.NULL);

        for (final Node node : method.getSelfAndChildrenRecursive(Node.class)) {
            if (matchStore(node, variable, args) &&
                match(single(args), AstCode.__New)) {

                newExpressions.put(variable.get(), (Expression) node);
            }

            for (final Node child : node.getChildren()) {
                if (parentLookup.containsKey(child)) {
                    throw Error.expressionLinkedFromMultipleLocations(child);
                }

                parentLookup.put(child, node);
            }
        }

        for (final Expression e : method.getSelfAndChildrenRecursive(Expression.class)) {
            if (matchGetArguments(e, AstCode.InvokeSpecial, ctor, args) &&
                ctor.get().isConstructor() &&
                args.size() > 0 &&
                matchLoad(first(args), variable)) {

                final Expression storeNew = newExpressions.get(variable.value);

                if (storeNew != null &&
                    Inlining.count(inlining.storeCounts, variable.value) == 1) {

                    final Node parent = parentLookup.get(storeNew);

                    if (parent instanceof Block || parent instanceof BasicBlock) {
                        final List<Node> body;

                        if (parent instanceof Block) {
                            body = ((Block) parent).getBody();
                        }
                        else {
                            body = ((BasicBlock) parent).getBody();
                        }

                        boolean moveInitToNew = false;

                        if (parentLookup.get(e) == parent) {
                            final int newIndex = body.indexOf(storeNew);
                            final int initIndex = body.indexOf(e);

                            if (initIndex > newIndex) {
                                for (int i = newIndex + 1; i < initIndex; i++) {
                                    if (references(body.get(i), variable.value)) {
                                        moveInitToNew = true;
                                        break;
                                    }
                                }
                            }
                        }

                        final Expression toRemove = moveInitToNew ? e : storeNew;
                        final Expression toRewrite = moveInitToNew ? storeNew : e;

                        final List<Expression> arguments = e.getArguments();
                        final Expression initExpression = new Expression(AstCode.InitObject, ctor.get(), storeNew.getOffset());

                        arguments.remove(0);

                        initExpression.getArguments().addAll(arguments);
                        initExpression.getRanges().addAll(e.getRanges());

                        body.remove(toRemove);

                        toRewrite.setCode(AstCode.Store);
                        toRewrite.setOperand(variable.value);
                        toRewrite.getArguments().clear();
                        toRewrite.getArguments().add(initExpression);

                        anyChanged = true;
                    }
                }
            }
        }

        return anyChanged;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="TransformArrayInitializers Step">

    private final static class TransformArrayInitializersOptimization extends AbstractExpressionOptimization {
        protected TransformArrayInitializersOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public boolean run(final List<Node> body, final Expression head, final int position) {
            final StrongBox<Variable> v = new StrongBox<>();
            final StrongBox<Expression> newArray = new StrongBox<>();

            if (matchGetArgument(head, AstCode.Store, v, newArray) &&
                match(newArray.get(), AstCode.InitArray)) {

                return tryRefineArrayInitialization(body, head, position);
            }

            final StrongBox<Variable> v3 = new StrongBox<>();
            final StrongBox<TypeReference> elementType = new StrongBox<>();
            final StrongBox<Expression> lengthExpression = new StrongBox<>();
            final StrongBox<Number> arrayLength = new StrongBox<>();

            if (matchGetArgument(head, AstCode.Store, v, newArray) &&
                matchGetArgument(newArray.get(), AstCode.NewArray, elementType, lengthExpression) &&
                matchGetOperand(lengthExpression.get(), AstCode.LdC, Number.class, arrayLength) &&
                arrayLength.get().intValue() > 0) {

                final int actualArrayLength = arrayLength.get().intValue();

                final StrongBox<Number> arrayPosition = new StrongBox<>();
                final List<Expression> initializers = new ArrayList<>();

                int instructionsToRemove = 0;

                for (int j = position + 1; j < body.size(); j++) {
                    final Node node = body.get(j);

                    if (!(node instanceof Expression)) {
                        continue;
                    }

                    final Expression next = (Expression) node;

                    if (next.getCode() == AstCode.StoreElement &&
                        matchGetOperand(next.getArguments().get(0), AstCode.Load, v3) &&
                        v3.get() == v.get() &&
                        matchGetOperand(next.getArguments().get(1), AstCode.LdC, Number.class, arrayPosition) &&
                        arrayPosition.get().intValue() >= initializers.size() &&
                        !next.getArguments().get(2).containsReferenceTo(v3.get())) {

                        while (initializers.size() < arrayPosition.get().intValue()) {
                            initializers.add(new Expression(AstCode.DefaultValue, elementType.get(), next.getOffset()));
                        }

                        initializers.add(next.getArguments().get(2));
                        instructionsToRemove++;
                    }
                    else {
                        break;
                    }
                }

                if (initializers.size() < actualArrayLength &&
                    initializers.size() >= actualArrayLength / 2) {

                    //
                    // Some compilers like Eclipse emit sparse array initializers.  If at least half
                    // of the elements in the array are initialized, emit an InitArray expression.
                    // I think this is a reasonable threshold.
                    //

                    while (initializers.size() < actualArrayLength) {
                        initializers.add(new Expression(AstCode.DefaultValue, elementType.get(), head.getOffset()));
                    }
                }

                if (initializers.size() == actualArrayLength) {
                    final TypeReference arrayType = elementType.get().makeArrayType();

                    head.getArguments().set(0, new Expression(AstCode.InitArray, arrayType, head.getOffset(), initializers));

                    for (int i = 0; i < instructionsToRemove; i++) {
                        body.remove(position + 1);
                    }

                    new Inlining(context, method).inlineIfPossible(body, new MutableInteger(position));
                    return true;
                }
            }

            return false;
        }

        private boolean tryRefineArrayInitialization(final List<Node> body, final Expression head, final int position) {
            final StrongBox<Variable> v = new StrongBox<>();
            final List<Expression> a = new ArrayList<>();
            final StrongBox<TypeReference> arrayType = new StrongBox<>();

            if (matchGetArguments(head, AstCode.Store, v, a) &&
                matchGetArguments(a.get(0), AstCode.InitArray, arrayType, a)) {

                final Expression initArray = head.getArguments().get(0);
                final List<Expression> initializers = initArray.getArguments();
                final int actualArrayLength = initializers.size();
                final StrongBox<Integer> arrayPosition = new StrongBox<>();

                for (int j = position + 1; j < body.size(); j++) {
                    final Node node = body.get(j);

                    if (matchGetArguments(node, AstCode.StoreElement, a) &&
                        matchLoad(a.get(0), v.get()) &&
                        !a.get(2).containsReferenceTo(v.get()) &&
                        matchGetOperand(a.get(1), AstCode.LdC, Integer.class, arrayPosition) &&
                        arrayPosition.get() >= 0 &&
                        arrayPosition.get() < actualArrayLength &&
                        match(initializers.get(arrayPosition.get()), AstCode.DefaultValue)) {

                        initializers.set(arrayPosition.get(), a.get(2));
                        body.remove(j--);
                    }
                    else {
                        break;
                    }
                }
            }

            return false;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="MakeAssignmentExpressions Step">

    private final static class MakeAssignmentExpressionsOptimization extends AbstractExpressionOptimization {
        protected MakeAssignmentExpressionsOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public boolean run(final List<Node> body, final Expression head, final int position) {
            //
            // ev = ...; store(v, ev) => ev = store(v, ...)
            //

            final StrongBox<Variable> ev = new StrongBox<>();
            final StrongBox<Expression> initializer = new StrongBox<>();

            final Node next = getOrDefault(body, position + 1);
            final StrongBox<Variable> v = new StrongBox<>();
            final StrongBox<Expression> storeArgument = new StrongBox<>();

            if (matchGetArgument(head, AstCode.Store, ev, initializer) &&
                !match(initializer.value, AstCode.__New)) {

                if (matchGetArgument(next, AstCode.Store, v, storeArgument) &&
                    matchLoad(storeArgument.get(), ev.get())) {

                    final Expression nextExpression = (Expression) next;
                    final Node store2 = getOrDefault(body, position + 2);

                    if (canConvertStoreToAssignment(store2, ev.get())) {
                        //
                        // e = ...; store(v1, e); anyStore(v2, e) => store(v1, anyStore(v2, ...)
                        //

                        final Inlining inlining = new Inlining(context, method);
                        final MutableInteger loadCounts = inlining.loadCounts.get(ev.get());
                        final MutableInteger storeCounts = inlining.storeCounts.get(ev.get());

                        if (loadCounts != null &&
                            loadCounts.getValue() == 2 &&
                            storeCounts != null &&
                            storeCounts.getValue() == 1) {

                            final Expression storeExpression = (Expression) store2;

                            body.remove(position + 2);  // remove store2
                            body.remove(position);      // remove ev = ...

                            nextExpression.getArguments().set(0, storeExpression);
                            storeExpression.getArguments().set(storeExpression.getArguments().size() - 1, initializer.get());

                            inlining.inlineIfPossible(body, new MutableInteger(position));

                            return true;
                        }
                    }

                    body.remove(position + 1);  // remove store

                    nextExpression.getArguments().set(0, initializer.get());
                    ((Expression) body.get(position)).getArguments().set(0, nextExpression);

                    return true;
                }

                if (match(next, AstCode.PutStatic)) {
                    final Expression nextExpression = (Expression) next;

                    //
                    // ev = ...; putstatic(f, ev) => ev = putstatic(f, ...)
                    //

                    if (matchLoad(nextExpression.getArguments().get(0), ev.get())) {
                        body.remove(position + 1);  // remove putstatic

                        nextExpression.getArguments().set(0, initializer.get());
                        ((Expression) body.get(position)).getArguments().set(0, nextExpression);

                        return true;
                    }
                }

                return false;
            }

            final StrongBox<Expression> equivalentLoad = new StrongBox<>();

            if (matchAssignment(head, initializer, equivalentLoad) &&
                next instanceof Expression) {

                if (equivalentLoad.get().getCode() == AstCode.GetField) {
                    final FieldReference field = (FieldReference) equivalentLoad.get().getOperand();
                    final FieldDefinition resolvedField = field != null ? field.resolve() : null;

                    if (resolvedField != null && resolvedField.isSynthetic()) {
                        return false;
                    }
                }

                final boolean isLoad = matchLoad(initializer.value, v);
                final ArrayDeque<Expression> agenda = new ArrayDeque<>();

                agenda.push((Expression) next);

            processNext:
                while (!agenda.isEmpty()) {
                    final Expression e = agenda.removeFirst();

                    if (e.getCode().isShortCircuiting() || e.getCode().isStore() || e.getCode().isFieldWrite()) {
                        break;
                    }

                    final List<Expression> arguments = e.getArguments();

                    for (int i = 0; i < arguments.size(); i++) {
                        final Expression a = arguments.get(i);

                        if (a.isEquivalentTo(equivalentLoad.value) ||
                            isLoad && matchLoad(a, v.get()) ||
                            (Inlining.hasNoSideEffect(initializer.get()) &&
                             a.isEquivalentTo(initializer.get()) &&
                             initializer.get().getInferredType() != null &&
                             MetadataHelper.isSameType(initializer.get().getInferredType(), a.getInferredType(), true))) {

                            arguments.set(i, head);
                            body.remove(position);
                            return true;
                        }

                        if (!Inlining.isSafeForInlineOver(a, head)) {
                            break processNext;
                        }

                        agenda.push(a);
                    }
                }
            }

            return false;
        }

        private boolean canConvertStoreToAssignment(final Node store, final Variable variable) {
            if (store instanceof Expression) {
                final Expression storeExpression = (Expression) store;

                switch (storeExpression.getCode()) {
                    case Store:
                    case PutStatic:
                    case PutField:
                    case StoreElement:
                        return matchLoad(lastOrDefault(storeExpression.getArguments()), variable);
                }
            }

            return false;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="IntroducePostIncrement Step">

    private final static class IntroducePostIncrementOptimization extends AbstractExpressionOptimization {
        protected IntroducePostIncrementOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public boolean run(final List<Node> body, final Expression head, final int position) {
            boolean modified = introducePostIncrementForVariables(body, head, position);

            assert body.get(position) == head;

            if (position > 0) {
                final Expression newExpression = introducePostIncrementForInstanceFields(head, body.get(position - 1));

                if (newExpression != null) {
                    modified = true;
                    body.remove(position);
                    new Inlining(context, method).inlineIfPossible(body, new MutableInteger(position - 1));
                }
            }

            return modified;
        }

        private boolean introducePostIncrementForVariables(final List<Node> body, final Expression e, final int position) {
            //
            // Works for local variables and static fields:
            //
            // e = load(i); inc(i, 1) => e = postincrement(i, 1)
            //   --or--
            // e = load(i); store(i, add(e, ldc(1)) => e = postincrement(i, 1)
            //

            final StrongBox<Variable> variable = new StrongBox<>();
            final StrongBox<Expression> initializer = new StrongBox<>();

            if (!matchGetArgument(e, AstCode.Store, variable, initializer) || !variable.get().isGenerated()) {
                return false;
            }

            final Node next = getOrDefault(body, position + 1);

            if (!(next instanceof Expression)) {
                return false;
            }

            final Expression nextExpression = (Expression) next;
            final AstCode loadCode = initializer.get().getCode();
            final AstCode storeCode = nextExpression.getCode();

            boolean recombineVariables = false;

            switch (loadCode) {
                case Load: {
                    if (storeCode != AstCode.Inc && storeCode != AstCode.Store) {
                        return false;
                    }

                    final Variable loadVariable = (Variable) initializer.get().getOperand();
                    final Variable storeVariable = (Variable) nextExpression.getOperand();

                    if (loadVariable != storeVariable) {
                        if (loadVariable.getOriginalVariable() != null &&
                            loadVariable.getOriginalVariable() == storeVariable.getOriginalVariable()) {

                            recombineVariables = true;
                        }
                        else {
                            return false;
                        }
                    }

                    break;
                }

                case GetStatic: {
                    if (storeCode != AstCode.PutStatic) {
                        return false;
                    }

                    final FieldReference initializerOperand = (FieldReference) initializer.get().getOperand();
                    final FieldReference nextOperand = (FieldReference) nextExpression.getOperand();

                    if (initializerOperand == null ||
                        nextOperand == null ||
                        !StringUtilities.equals(initializerOperand.getFullName(), nextOperand.getFullName())) {

                        return false;
                    }

                    break;
                }

                default: {
                    return false;
                }
            }

            final Expression add = storeCode == AstCode.Inc ? nextExpression : nextExpression.getArguments().get(0);
            final StrongBox<Number> incrementAmount = new StrongBox<>();
            final AstCode incrementCode = getIncrementCode(add, incrementAmount);

            if (incrementCode == AstCode.Nop || !(match(add, AstCode.Inc) || match(add.getArguments().get(0), AstCode.Load))) {
                return false;
            }

            if (recombineVariables) {
                replaceVariables(
                    method,
                    new Function<Variable, Variable>() {
                        @Override
                        public Variable apply(final Variable old) {
                            return old == nextExpression.getOperand() ? (Variable) initializer.get().getOperand() : old;
                        }
                    }
                );
            }

            e.getArguments().set(
                0,
                new Expression(incrementCode, incrementAmount.get(), initializer.get().getOffset(), initializer.get())
            );

            body.remove(position + 1);
            return true;
        }

        @SuppressWarnings("UnusedParameters")
        private Expression introducePostIncrementForInstanceFields(final Expression e, final Node previous) {
            //
            // t = getfield(field, load(p)); putfield(field, load(p), add(load(t), ldc(1)))
            //   => store(t, postincrement(1, load(field, load(p))))
            //
            // Also works for array elements:
            //
            // t = loadelement(T, load(p), load(i)); storeelement(T, load(p), load(i), add(load(t), ldc(1)))
            //   => store(t, postincrement(1, loadelement(load(p), load(i))))
            //

            if (!(previous instanceof Expression)) {
                return null;
            }

            final Expression p = (Expression) previous;
            final StrongBox<Variable> t = new StrongBox<>();
            final StrongBox<Expression> initialValue = new StrongBox<>();

            if (!matchGetArgument(p, AstCode.Store, t, initialValue) ||
                initialValue.get().getCode() != AstCode.GetField && initialValue.get().getCode() != AstCode.LoadElement) {

                return null;
            }

            final AstCode code = e.getCode();
            final Variable tempVariable = t.get();

            if (code != AstCode.PutField && code != AstCode.StoreElement) {
                return null;
            }

            //
            // Test that all arguments except the last are load (1 arg for fields, 2 args for arrays).
            //

            final List<Expression> arguments = e.getArguments();

            for (int i = 0, n = arguments.size() - 1; i < n; i++) {
                if (arguments.get(i).getCode() != AstCode.Load) {
                    return null;
                }
            }

            final StrongBox<Number> incrementAmount = new StrongBox<>();
            final Expression add = arguments.get(arguments.size() - 1);
            final AstCode incrementCode = getIncrementCode(add, incrementAmount);

            if (incrementCode == AstCode.Nop) {
                return null;
            }

            final List<Expression> addArguments = add.getArguments();

            if (!matchGetOperand(addArguments.get(0), AstCode.Load, t) || t.get() != tempVariable) {
                return null;
            }

            if (e.getCode() == AstCode.PutField) {
                if (initialValue.get().getCode() != AstCode.GetField) {
                    return null;
                }

                //
                // There might be two different FieldReference instances, so we compare the field's signatures:
                //
                final FieldReference getField = (FieldReference) initialValue.get().getOperand();
                final FieldReference setField = (FieldReference) e.getOperand();

                if (!StringUtilities.equals(getField.getFullName(), setField.getFullName())) {
                    return null;
                }
            }
            else if (initialValue.get().getCode() != AstCode.LoadElement) {
                return null;
            }

            final List<Expression> initialValueArguments = initialValue.get().getArguments();

            assert (arguments.size() - 1 == initialValueArguments.size());

            for (int i = 0, n = initialValueArguments.size(); i < n; i++) {
                if (!matchLoad(initialValueArguments.get(i), (Variable) arguments.get(i).getOperand())) {
                    return null;
                }
            }

            p.getArguments().set(0, new Expression(AstCode.PostIncrement, incrementAmount.get(), initialValue.get().getOffset(), initialValue.get()));

            return p;
        }

        private AstCode getIncrementCode(final Expression add, final StrongBox<Number> incrementAmount) {
            final AstCode incrementCode;
            final Expression amountArgument;
            final boolean decrement;

            switch (add.getCode()) {
                case Add: {
                    incrementCode = AstCode.PostIncrement;
                    amountArgument = add.getArguments().get(1);
                    decrement = false;
                    break;
                }

                case Sub: {
                    incrementCode = AstCode.PostIncrement;
                    amountArgument = add.getArguments().get(1);
                    decrement = true;
                    break;
                }

                case Inc: {
                    incrementCode = AstCode.PostIncrement;
                    amountArgument = add.getArguments().get(0);
                    decrement = false;
                    break;
                }

                default: {
                    return AstCode.Nop;
                }
            }

            if (matchGetOperand(amountArgument, AstCode.LdC, incrementAmount) &&
                !(incrementAmount.get() instanceof Float ||
                  incrementAmount.get() instanceof Double)) {

                if (incrementAmount.get().longValue() == 1L || incrementAmount.get().longValue() == -1L) {
                    incrementAmount.set(
                        decrement ? -incrementAmount.get().intValue()
                                  : incrementAmount.get().intValue()
                    );
                    return incrementCode;
                }
            }

            return AstCode.Nop;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="FlattenBasicBlocks Step">

    @SuppressWarnings("StatementWithEmptyBody")
    private static void flattenBasicBlocks(final Node node) {
        if (node instanceof Block) {
            final Block block = (Block) node;
            final List<Node> flatBody = new ArrayList<>();

            for (final Node child : block.getChildren()) {
                flattenBasicBlocks(child);

                if (child instanceof BasicBlock) {
                    final BasicBlock childBasicBlock = (BasicBlock) child;
                    final Node firstChild = firstOrDefault(childBasicBlock.getBody());
                    final Node lastChild = lastOrDefault(childBasicBlock.getBody());

                    if (!(firstChild instanceof Label)) {
                        throw new IllegalStateException("Basic block must start with a label.");
                    }

                    if (lastChild instanceof Expression && !lastChild.isUnconditionalControlFlow()) {
                        throw new IllegalStateException("Basic block must end with an unconditional branch.");
                    }

                    flatBody.addAll(childBasicBlock.getBody());
                }
                else {
                    flatBody.add(child);
                }
            }

            block.setEntryGoto(null);
            block.getBody().clear();
            block.getBody().addAll(flatBody);
        }
        else if (node != null) {
            for (final Node child : node.getChildren()) {
                flattenBasicBlocks(child);
            }
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="DuplicateReturns Step">

    private static void duplicateReturnStatements(final Block method) {
        final List<Node> methodBody = method.getBody();
        final Map<Node, Node> nextSibling = new IdentityHashMap<>();
        final StrongBox<Object> constant = new StrongBox<>();
        final StrongBox<Variable> localVariable = new StrongBox<>();
        final StrongBox<Label> targetLabel = new StrongBox<>();
        final List<Expression> returnArguments = new ArrayList<>();

        //
        // Build navigation data.
        //
        for (final Block block : method.getSelfAndChildrenRecursive(Block.class)) {
            final List<Node> body = block.getBody();

            for (int i = 0; i < body.size() - 1; i++) {
                final Node current = body.get(i);

                if (current instanceof Label) {
                    nextSibling.put(current, body.get(i + 1));
                }
            }
        }

        //
        // Duplicate returns.
        //
        for (final Block block : method.getSelfAndChildrenRecursive(Block.class)) {
            final List<Node> body = block.getBody();

            for (int i = 0; i < body.size(); i++) {
                final Node node = body.get(i);

                if (matchGetOperand(node, AstCode.Goto, targetLabel)) {
                    //
                    // Skip extra labels.
                    //
                    while (nextSibling.get(targetLabel.get()) instanceof Label) {
                        targetLabel.accept((Label) nextSibling.get(targetLabel.get()));
                    }

                    //
                    // Inline return statement.
                    //
                    final Node target = nextSibling.get(targetLabel.get());

                    if (target != null &&
                        matchGetArguments(target, AstCode.Return, returnArguments)) {

                        if (returnArguments.isEmpty()) {
                            body.set(
                                i,
                                new Expression(AstCode.Return, null, Expression.MYSTERY_OFFSET)
                            );
                        }
                        else if (matchGetOperand(returnArguments.get(0), AstCode.Load, localVariable)) {
                            body.set(
                                i,
                                new Expression(AstCode.Return, null, Expression.MYSTERY_OFFSET, new Expression(AstCode.Load, localVariable.get(), Expression.MYSTERY_OFFSET))
                            );
                        }
                        else if (matchGetOperand(returnArguments.get(0), AstCode.LdC, constant)) {
                            body.set(
                                i,
                                new Expression(AstCode.Return, null, Expression.MYSTERY_OFFSET, new Expression(AstCode.LdC, constant.get(), Expression.MYSTERY_OFFSET))
                            );
                        }
                    }
                    else if (!methodBody.isEmpty() && methodBody.get(methodBody.size() - 1) == targetLabel.get()) {
                        //
                        // It exits the main method, so it is effectively a return.
                        //
                        body.set(i, new Expression(AstCode.Return, null, Expression.MYSTERY_OFFSET));
                    }
                }
            }
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="ReduceIfNesting Step">

    private static void reduceIfNesting(final Node node) {
        if (node instanceof Block) {
            final Block block = (Block) node;
            final List<Node> blockBody = block.getBody();

            for (int i = 0; i < blockBody.size(); i++) {
                final Node n = blockBody.get(i);

                if (!(n instanceof Condition)) {
                    continue;
                }

                final Condition condition = (Condition) n;

                final Node trueEnd = lastOrDefault(condition.getTrueBlock().getBody());
                final Node falseEnd = lastOrDefault(condition.getFalseBlock().getBody());

                final boolean trueExits = trueEnd != null && trueEnd.isUnconditionalControlFlow();
                final boolean falseExits = falseEnd != null && falseEnd.isUnconditionalControlFlow();

                if (trueExits) {
                    //
                    // Move the false block after the condition.
                    //
                    blockBody.addAll(i + 1, condition.getFalseBlock().getChildren());
                    condition.setFalseBlock(new Block());
                }
                else if (falseExits) {
                    //
                    // Move the true block after the condition.
                    //
                    blockBody.addAll(i + 1, condition.getTrueBlock().getChildren());
                    condition.setTrueBlock(new Block());
                }

                //
                // Eliminate empty true block.
                //
                if (condition.getTrueBlock().getChildren().isEmpty() && !condition.getFalseBlock().getChildren().isEmpty()) {
                    final Block temp = condition.getTrueBlock();
                    final Expression conditionExpression = condition.getCondition();

                    condition.setTrueBlock(condition.getFalseBlock());
                    condition.setFalseBlock(temp);
                    condition.setCondition(simplifyLogicalNot(new Expression(AstCode.LogicalNot, null, conditionExpression.getOffset(), conditionExpression)));
                }
            }
        }

        for (final Node child : node.getChildren()) {
            if (child != null && !(child instanceof Expression)) {
                reduceIfNesting(child);
            }
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="RecombineVariables Step">

    private static void recombineVariables(final Block method) {
        final Map<VariableDefinition, Variable> map = new IdentityHashMap<>();

        replaceVariables(
            method,
            new Function<Variable, Variable>() {
                @Override
                public final Variable apply(final Variable v) {
                    final VariableDefinition originalVariable = v.getOriginalVariable();

                    if (originalVariable == null) {
                        return v;
                    }

                    Variable combinedVariable = map.get(originalVariable);

                    if (combinedVariable == null) {
                        map.put(originalVariable, v);
                        combinedVariable = v;
                    }

                    return combinedVariable;
                }
            }
        );
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="InlineLambdas Step">

    private final static class InlineLambdasOptimization extends AbstractExpressionOptimization {
        private final MutableInteger _lambdaCount = new MutableInteger();

        protected InlineLambdasOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public boolean run(final List<Node> body, final Expression head, final int position) {
            final StrongBox<DynamicCallSite> c = new StrongBox<>();
            final List<Expression> a = new ArrayList<>();

            boolean modified = false;

            for (final Expression e : head.getChildrenAndSelfRecursive(Expression.class)) {
                if (matchGetArguments(e, AstCode.InvokeDynamic, c, a)) {
                    final Lambda lambda = tryInlineLambda(e, c.value);

                    if (lambda != null) {
                        modified = true;
                    }
                }
            }

            return modified;
        }

        private Lambda tryInlineLambda(final Expression site, final DynamicCallSite callSite) {
            final MethodReference bootstrapMethod = callSite.getBootstrapMethod();

            if ("java/lang/invoke/LambdaMetafactory".equals(bootstrapMethod.getDeclaringType().getInternalName()) &&
                (StringUtilities.equals("metafactory", bootstrapMethod.getName(), StringComparison.OrdinalIgnoreCase) ||
                 StringUtilities.equals("altMetafactory", bootstrapMethod.getName(), StringComparison.OrdinalIgnoreCase)) &&
                callSite.getBootstrapArguments().size() >= 3 &&
                callSite.getBootstrapArguments().get(1) instanceof MethodHandle) {

                final MethodHandle targetMethodHandle = (MethodHandle) callSite.getBootstrapArguments().get(1);
                final MethodReference targetMethod = targetMethodHandle.getMethod();
                final MethodDefinition resolvedMethod = targetMethod.resolve();

                if (resolvedMethod == null ||
                    resolvedMethod.getBody() == null ||
                    !resolvedMethod.isSynthetic() ||
                    !MetadataHelper.isEnclosedBy(resolvedMethod.getDeclaringType(), context.getCurrentType()) ||
                    (StringUtilities.equals(resolvedMethod.getFullName(), context.getCurrentMethod().getFullName()) &&
                     StringUtilities.equals(resolvedMethod.getSignature(), context.getCurrentMethod().getSignature()))) {

                    return null;
                }

                final TypeReference functionType = callSite.getMethodType().getReturnType();

                final List<MethodReference> methods = MetadataHelper.findMethods(
                    functionType,
                    MetadataFilters.matchName(callSite.getMethodName())
                );

                MethodReference functionMethod = null;

                for (final MethodReference m : methods) {
                    final MethodDefinition r = m.resolve();

                    if (r != null && r.isAbstract() && !r.isStatic() && !r.isDefault()) {
                        functionMethod = r;
                        break;
                    }
                }

                if (functionMethod == null) {
                    return null;
                }

                final DecompilerContext innerContext = new DecompilerContext(context.getSettings());

                innerContext.setCurrentType(resolvedMethod.getDeclaringType());
                innerContext.setCurrentMethod(resolvedMethod);

                final MethodBody methodBody = resolvedMethod.getBody();
                final List<ParameterDefinition> parameters = resolvedMethod.getParameters();
                final Variable[] parameterMap = new Variable[methodBody.getMaxLocals()];

                final List<Node> nodes = new ArrayList<>();
                final Block body = new Block();
                final Lambda lambda = new Lambda(body, functionType);

                lambda.setMethod(functionMethod);
                lambda.setCallSite(callSite);

                final List<Variable> lambdaParameters = lambda.getParameters();

                if (resolvedMethod.hasThis()) {
                    final Variable variable = new Variable();

                    variable.setName("this");
                    variable.setType(context.getCurrentMethod().getDeclaringType());
                    variable.setOriginalParameter(context.getCurrentMethod().getBody().getThisParameter());

                    parameterMap[0] = variable;

                    lambdaParameters.add(variable);
                }

                for (final ParameterDefinition p : parameters) {
                    final Variable variable = new Variable();

                    variable.setName(p.getName());
                    variable.setType(p.getParameterType());
                    variable.setOriginalParameter(p);
                    variable.setLambdaParameter(true);

                    parameterMap[p.getSlot()] = variable;

                    lambdaParameters.add(variable);
                }

                final List<Expression> arguments = site.getArguments();

                for (int i = 0; i < arguments.size(); i++) {
                    final Variable v = lambdaParameters.get(0);

                    v.setOriginalParameter(null);
                    v.setGenerated(true);

                    final Expression argument = arguments.get(i).clone();

                    nodes.add(new Expression(AstCode.Store, v, argument.getOffset(), argument));

                    lambdaParameters.remove(0);
                }

                arguments.clear();
                nodes.addAll(AstBuilder.build(methodBody, true, innerContext));
                body.getBody().addAll(nodes);

                for (final Expression e : body.getSelfAndChildrenRecursive(Expression.class)) {
                    final Object operand = e.getOperand();

                    if (operand instanceof Variable) {
                        final Variable oldVariable = (Variable) operand;

                        if (oldVariable.isParameter() &&
                            oldVariable.getOriginalParameter().getMethod() == resolvedMethod) {

                            final Variable newVariable = parameterMap[oldVariable.getOriginalParameter().getSlot()];

                            if (newVariable != null) {
                                e.setOperand(newVariable);
                            }
                        }
                    }
                }

                AstOptimizer.optimize(innerContext, body, AstOptimizationStep.InlineVariables2);

                final int lambdaId = _lambdaCount.increment().getValue();
                final Set<Label> renamedLabels = new HashSet<>();

                for (final Node n : body.getSelfAndChildrenRecursive()) {
                    if (n instanceof Label) {
                        final Label label = (Label) n;
                        if (renamedLabels.add(label)) {
                            label.setName(label.getName() + "_" + lambdaId);
                        }
                        continue;
                    }

                    if (!(n instanceof Expression)) {
                        continue;
                    }

                    final Expression e = (Expression) n;
                    final Object operand = e.getOperand();

                    if (operand instanceof Label) {
                        final Label label = (Label) operand;
                        if (renamedLabels.add(label)) {
                            label.setName(label.getName() + "_" + lambdaId);
                        }
                    }
                    else if (operand instanceof Label[]) {
                        for (final Label label : (Label[]) operand) {
                            if (renamedLabels.add(label)) {
                                label.setName(label.getName() + "_" + lambdaId);
                            }
                        }
                    }

                    if (match(e, AstCode.Return)) {
                        e.putUserData(AstKeys.PARENT_LAMBDA_BINDING, site);
                    }
                }

                site.setCode(AstCode.Bind);
                site.setOperand(lambda);

                final List<Range> ranges = site.getRanges();

                for (final Expression e : lambda.getSelfAndChildrenRecursive(Expression.class)) {
                    ranges.addAll(e.getRanges());
                }

                return lambda;
            }

            return null;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="JoinBranchConditions Step">

    private static final class JoinBranchConditionsOptimization extends AbstractBranchBlockOptimization {
        public JoinBranchConditionsOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        protected boolean run(
            final List<Node> body,
            final BasicBlock branchBlock,
            final Expression branchCondition,
            final Label thenLabel,
            final Label elseLabel,
            final boolean negate) {

            if (labelGlobalRefCount.get(elseLabel).getValue() != 1) {
                return false;
            }

            final BasicBlock elseBlock = labelToBasicBlock.get(elseLabel);

            if (matchSingleAndBreak(elseBlock, AstCode.IfTrue, label1, expression, label2)) {
                final Label elseThenLabel = label1.get();
                final Label elseElseLabel = label2.get();

                final Expression elseCondition = expression.get();

                return runCore(body, branchBlock, branchCondition, thenLabel, elseLabel, elseCondition, negate, elseThenLabel, elseElseLabel, false) ||
                       runCore(body, branchBlock, branchCondition, thenLabel, elseLabel, elseCondition, negate, elseElseLabel, elseThenLabel, true);
            }

            return false;
        }

        private boolean runCore(
            final List<Node> body,
            final BasicBlock branchBlock,
            final Expression branchCondition,
            final Label thenLabel,
            final Label elseLabel,
            final Expression elseCondition,
            final boolean negateFirst,
            final Label elseThenLabel,
            final Label elseElseLabel,
            final boolean negateSecond) {

            final BasicBlock thenBlock = labelToBasicBlock.get(thenLabel);
            final BasicBlock elseThenBlock = labelToBasicBlock.get(elseThenLabel);

            BasicBlock alsoRemove = null;
            Label alsoDecrement = null;

            if (elseThenBlock != thenBlock) {
                if (matchSimpleBreak(elseThenBlock, label1) &&
                    labelGlobalRefCount.get(label1.get()).getValue() <= 2) {

                    final BasicBlock intermediateBlock = labelToBasicBlock.get(label1.get());

                    if (intermediateBlock != thenBlock) {
                        return false;
                    }

                    alsoRemove = elseThenBlock;
                    alsoDecrement = label1.get();
                }
                else {
                    return false;
                }
            }

            final BasicBlock elseBlock = labelToBasicBlock.get(elseLabel);

            final Expression logicExpression = new Expression(
                AstCode.LogicalOr,
                null,
                Expression.MYSTERY_OFFSET,
                negateFirst ? simplifyLogicalNotArgument(branchCondition) ? branchCondition
                                                                          : new Expression(AstCode.LogicalNot, null, branchCondition.getOffset(), branchCondition)
                            : branchCondition,
                negateSecond ? simplifyLogicalNotArgument(elseCondition) ? elseCondition
                                                                         : new Expression(AstCode.LogicalNot, null, elseCondition.getOffset(), elseCondition)
                             : elseCondition
            );

            final List<Node> branchBody = branchBlock.getBody();

            removeTail(branchBody, AstCode.IfTrue, AstCode.Goto);

            branchBody.add(new Expression(AstCode.IfTrue, thenLabel, logicExpression.getOffset(), logicExpression));
            branchBody.add(new Expression(AstCode.Goto, elseElseLabel, Expression.MYSTERY_OFFSET));

            labelGlobalRefCount.get(elseLabel).decrement();
            labelGlobalRefCount.get(elseThenLabel).decrement();

            body.remove(elseBlock);

            if (alsoRemove != null) {
                body.remove(alsoRemove);
            }

            if (alsoDecrement != null) {
                labelGlobalRefCount.get(alsoDecrement).decrement();
            }

            return true;
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Optimization Helpers">

    private interface BasicBlockOptimization {
        boolean run(final List<Node> body, final BasicBlock head, final int position);
    }

    private interface ExpressionOptimization {
        boolean run(final List<Node> body, final Expression head, final int position);
    }

    @SuppressWarnings("ProtectedField")
    private static abstract class AbstractBasicBlockOptimization implements BasicBlockOptimization {
        protected final static BasicBlock EMPTY_BLOCK = new BasicBlock();

        protected final Map<Label, MutableInteger> labelGlobalRefCount = new DefaultMap<>(MutableInteger.SUPPLIER);
        protected final Map<Label, BasicBlock> labelToBasicBlock = new DefaultMap<>(Suppliers.forValue(EMPTY_BLOCK));

        protected final DecompilerContext context;
        protected final IMetadataResolver resolver;
        protected final Block method;

        protected AbstractBasicBlockOptimization(final DecompilerContext context, final Block method) {
            this.context = VerifyArgument.notNull(context, "context");
            this.resolver = context.getCurrentType().getResolver();
            this.method = VerifyArgument.notNull(method, "method");

            for (final Expression e : method.getSelfAndChildrenRecursive(Expression.class)) {
                if (e.isBranch()) {
                    for (final Label target : e.getBranchTargets()) {
                        labelGlobalRefCount.get(target).increment();
                    }
                }
            }

            for (final BasicBlock basicBlock : method.getSelfAndChildrenRecursive(BasicBlock.class)) {
                for (final Node child : basicBlock.getChildren()) {
                    if (child instanceof Label) {
                        labelToBasicBlock.put((Label) child, basicBlock);
                    }
                }
            }
        }
    }

    @SuppressWarnings("ProtectedField")
    private static abstract class AbstractExpressionOptimization implements ExpressionOptimization {
        protected final DecompilerContext context;
        protected final MetadataSystem metadataSystem;
        protected final Block method;

        protected AbstractExpressionOptimization(final DecompilerContext context, final Block method) {
            this.context = VerifyArgument.notNull(context, "context");
            this.metadataSystem = MetadataSystem.instance();
            this.method = VerifyArgument.notNull(method, "method");
        }
    }

    private static boolean runOptimization(final Block block, final BasicBlockOptimization optimization) {
        boolean modified = false;

        final List<Node> body = block.getBody();

        for (int i = body.size() - 1; i >= 0; i--) {
            if (i < body.size() && optimization.run(body, (BasicBlock) body.get(i), i)) {
                modified = true;
                ++i;
            }
        }

        return modified;
    }

    private static boolean runOptimization(final Block block, final ExpressionOptimization optimization) {
        boolean modified = false;

        for (final Node node : block.getBody()) {
            final BasicBlock basicBlock = (BasicBlock) node;
            final List<Node> body = basicBlock.getBody();

            for (int i = body.size() - 1; i >= 0; i--) {
                if (i >= body.size()) {
                    continue;
                }

                final Node n = body.get(i);

                if (n instanceof Expression && optimization.run(body, (Expression) n, i)) {
                    modified = true;
                    ++i;
                }
            }
        }

        return modified;
    }

    private static abstract class AbstractBranchBlockOptimization extends AbstractBasicBlockOptimization {
        protected final StrongBox<Expression> expression = new StrongBox<>();
        protected final StrongBox<Label> label1 = new StrongBox<>();
        protected final StrongBox<Label> label2 = new StrongBox<>();

        public AbstractBranchBlockOptimization(final DecompilerContext context, final Block method) {
            super(context, method);
        }

        @Override
        public final boolean run(final List<Node> body, final BasicBlock head, final int position) {
            if (matchLastAndBreak(head, AstCode.IfTrue, label1, expression, label2)) {
                final Label thenLabel = label1.get();
                final Label elseLabel = label2.get();

                final Expression condition = expression.get();

                return run(body, head, condition, thenLabel, elseLabel, false) ||
                       run(body, head, condition, elseLabel, thenLabel, true);
            }

            return false;
        }

        protected abstract boolean run(
            final List<Node> body,
            final BasicBlock branchBlock,
            final Expression branchCondition,
            final Label thenLabel,
            final Label elseLabel,
            final boolean negate);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Utility Methods">

    public static void replaceVariables(final Node node, final Function<Variable, Variable> mapping) {
        if (node instanceof Expression) {
            final Expression expression = (Expression) node;
            final Object operand = expression.getOperand();

            if (operand instanceof Variable) {
                expression.setOperand(mapping.apply((Variable) operand));
            }

            for (final Expression argument : expression.getArguments()) {
                replaceVariables(argument, mapping);
            }
        }
        else {
            if (node instanceof CatchBlock) {
                final CatchBlock catchBlock = (CatchBlock) node;
                final Variable exceptionVariable = catchBlock.getExceptionVariable();

                if (exceptionVariable != null) {
                    catchBlock.setExceptionVariable(mapping.apply(exceptionVariable));
                }
            }

            for (final Node child : node.getChildren()) {
                replaceVariables(child, mapping);
            }
        }
    }

    static <T> void removeOrThrow(final Collection<T> collection, final T item) {
        if (!collection.remove(item)) {
            throw new IllegalStateException("The item was not found in the collection.");
        }
    }

    static void removeTail(final List<Node> body, final AstCode... codes) {
        for (int i = 0; i < codes.length; i++) {
            if (((Expression) body.get(body.size() - codes.length + i)).getCode() != codes[i]) {
                throw new IllegalStateException("Tailing code does not match expected.");
            }
        }

        //noinspection UnusedDeclaration
        for (final AstCode code : codes) {
            body.remove(body.size() - 1);
        }
    }

    static Expression makeLeftAssociativeShortCircuit(final AstCode code, final Expression left, final Expression right) {
        //
        // Assuming that the inputs are already left-associative.
        //
        if (match(right, code)) {
            //
            // Find the leftmost logical expression.
            //
            Expression current = right;

            while (match(current.getArguments().get(0), code)) {
                current = current.getArguments().get(0);
            }

            final Expression newArgument = new Expression(code, null, left.getOffset(), left, current.getArguments().get(0));

            newArgument.setInferredType(BuiltinTypes.Boolean);
            current.getArguments().set(0, newArgument);

            return right;
        }
        else {
            final Expression newExpression = new Expression(code, null, left.getOffset(), left, right);
            newExpression.setInferredType(BuiltinTypes.Boolean);
            return newExpression;
        }
    }

    private final static BooleanBox SCRATCH_BOOLEAN_BOX = new BooleanBox();

    static Expression simplifyLogicalNot(final Expression expression) {
        final Expression result = simplifyLogicalNot(expression, SCRATCH_BOOLEAN_BOX);
        return result != null ? result : expression;
    }

    static Expression simplifyLogicalNot(final Expression expression, final BooleanBox modified) {
        Expression a;
        Expression e = expression;

        //
        // CmpEq(a, ldc, 0) becomes LogicalNot(a) if the inferred type for expression 'a' is boolean.
        //

        List<Expression> arguments = e.getArguments();

        final StrongBox<Boolean> b = new StrongBox<>();
        final Expression operand = arguments.isEmpty() ? null : arguments.get(0);

        if (e.getCode() == AstCode.CmpEq &&
            TypeAnalysis.isBoolean(operand.getInferredType()) &&
            matchBooleanConstant(a = arguments.get(1), b) &&
            Boolean.FALSE.equals(b.get())) {

            e.setCode(AstCode.LogicalNot);
            e.getRanges().addAll(a.getRanges());

            arguments.remove(1);
            modified.set(true);
        }

        Expression result = null;

        if (e.getCode() == AstCode.CmpNe &&
            TypeAnalysis.isBoolean(operand.getInferredType()) &&
            matchBooleanConstant(arguments.get(1), b) &&
            Boolean.FALSE.equals(b.get())) {

            modified.set(true);
            return e.getArguments().get(0);
        }

        if (e.getCode() == AstCode.TernaryOp) {
            final Expression condition = arguments.get(0);

            if (match(condition, AstCode.LogicalNot)) {
                final Expression temp = arguments.get(1);

                arguments.set(0, condition.getArguments().get(0));
                arguments.set(1, arguments.get(2));
                arguments.set(2, temp);
            }
        }

        while (e.getCode() == AstCode.LogicalNot) {
            a = operand;

            //
            // Remove double negation.
            //
            if (a.getCode() == AstCode.LogicalNot) {
                result = a.getArguments().get(0);
                result.getRanges().addAll(e.getRanges());
                result.getRanges().addAll(a.getRanges());
                e = result;
                arguments = e.getArguments();
            }
            else {
                if (simplifyLogicalNotArgument(a)) {
                    result = e = a;
                    arguments = e.getArguments();
                    modified.set(true);
                }
                break;
            }
        }

        for (int i = 0; i < arguments.size(); i++) {
            a = simplifyLogicalNot(arguments.get(i), modified);

            if (a != null) {
                arguments.set(i, a);
                modified.set(true);
            }
        }

        return result;
    }

    static boolean simplifyLogicalNotArgument(final Expression e) {
        if (!canSimplifyLogicalNotArgument(e)) {
            return false;
        }

        final List<Expression> arguments = e.getArguments();

        switch (e.getCode()) {
            case CmpEq:
            case CmpNe:
            case CmpLt:
            case CmpGe:
            case CmpGt:
            case CmpLe:
                e.setCode(e.getCode().reverse());
                return true;

            case LogicalNot:
                final Expression a = arguments.get(0);
                e.setCode(a.getCode());
                e.setOperand(a.getOperand());
                arguments.clear();
                arguments.addAll(a.getArguments());
                e.getRanges().addAll(a.getRanges());
                return true;

            case LogicalAnd:
            case LogicalOr:
                if (!simplifyLogicalNotArgument(arguments.get(0))) {
                    negate(arguments.get(0));
                }
                if (!simplifyLogicalNotArgument(arguments.get(1))) {
                    negate(arguments.get(1));
                }
                e.setCode(e.getCode().reverse());
                return true;

            case TernaryOp:
                simplifyLogicalNotArgument(arguments.get(1));
                simplifyLogicalNotArgument(arguments.get(2));
                return true;

            default:
                return TypeAnalysis.isBoolean(e.getInferredType()) &&
                       negate(e);
        }
    }

    private static boolean negate(final Expression e) {
        if (TypeAnalysis.isBoolean(e.getInferredType())) {
            final Expression copy = e.clone();
            e.setCode(AstCode.LogicalNot);
            e.setOperand(null);
            e.getArguments().clear();
            e.getArguments().add(copy);
            return true;
        }
        return false;
    }

    private static boolean canSimplifyLogicalNotArgument(final Expression e) {
        switch (e.getCode()) {
            case CmpEq:
            case CmpNe:
            case CmpLt:
            case CmpGe:
            case CmpGt:
            case CmpLe:
                return true;

            case LogicalNot:
                return true;

            case LogicalAnd:
            case LogicalOr:
                final List<Expression> arguments = e.getArguments();
                return canSimplifyLogicalNotArgument(arguments.get(0)) ||
                       canSimplifyLogicalNotArgument(arguments.get(1));

            case TernaryOp:
                return TypeAnalysis.isBoolean(e.getInferredType()) &&
                       canSimplifyLogicalNotArgument(e.getArguments().get(1)) &&
                       canSimplifyLogicalNotArgument(e.getArguments().get(2));

            default:
                return false;
        }
    }

    static boolean references(final Node node, final Variable v) {
        for (final Expression e : node.getSelfAndChildrenRecursive(Expression.class)) {
            if (matchLoad(e, v)) {
                return true;
            }
        }
        return false;
    }

    private static boolean containsMatch(final Node node, final Expression pattern) {
        for (final Expression e : node.getSelfAndChildrenRecursive(Expression.class)) {
            if (e.isEquivalentTo(pattern)) {
                return true;
            }
        }
        return false;
    }

    // </editor-fold>
}
