/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.codehaus.groovy.control;

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyRuntimeException;
import groovy.transform.CompilationUnitAware;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.CompileUnit;
import org.codehaus.groovy.ast.GroovyClassVisitor;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.classgen.AsmClassGenerator;
import org.codehaus.groovy.classgen.ClassCompletionVerifier;
import org.codehaus.groovy.classgen.EnumCompletionVisitor;
import org.codehaus.groovy.classgen.EnumVisitor;
import org.codehaus.groovy.classgen.ExtendedVerifier;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.classgen.InnerClassCompletionVisitor;
import org.codehaus.groovy.classgen.InnerClassVisitor;
import org.codehaus.groovy.classgen.Verifier;
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.io.InputStreamReaderSource;
import org.codehaus.groovy.control.io.ReaderSource;
import org.codehaus.groovy.control.messages.ExceptionMessage;
import org.codehaus.groovy.control.messages.Message;
import org.codehaus.groovy.syntax.RuntimeParserException;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.tools.GroovyClass;
import org.codehaus.groovy.transform.ASTTransformationVisitor;
import org.codehaus.groovy.transform.AnnotationCollectorTransform;
import org.codehaus.groovy.transform.trait.TraitComposer;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;

import static java.util.stream.Collectors.toList;
import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
import static org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys.DYNAMIC_OUTER_NODE_CALLBACK;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.SWITCH_CONDITION_EXPRESSION_TYPE;

/**
 * The CompilationUnit collects all compilation data as it is generated by the compiler system.
 * You can use this object to add additional source units to the compilation, or force the
 * compilation to be run again (to affect only the deltas).
 * <p>
 * You can also add PhaseOperations to this compilation using the addPhaseOperation method.
 * This is commonly used when you want to wire a new AST Transformation into the compilation.
 */
public class CompilationUnit extends ProcessingUnit {

    /** The overall AST for this CompilationUnit. */
    protected CompileUnit ast; // TODO: Switch to private and access through getAST().

    /** The source units from which this unit is built. */
    protected Map<String, SourceUnit> sources = new LinkedHashMap<>();
    protected Queue<SourceUnit> queuedSources = new LinkedList<>();

    /** The classes generated during classgen. */
    private List<GroovyClass> generatedClasses = new ArrayList<>();

    private Deque<PhaseOperation>[] phaseOperations;
    private Deque<PhaseOperation>[] newPhaseOperations;
    {
        final int n = Phases.ALL + 1;
        phaseOperations = new Deque[n];
        newPhaseOperations = new Deque[n];
        for (int i = 0; i < n; i += 1) {
            phaseOperations[i] = new LinkedList<>();
            newPhaseOperations[i] = new LinkedList<>();
        }
    }

    /** If set, outputs a little more information during compilation when errors occur. */
    protected boolean debug;
    /** True after the first {@link #configure(CompilerConfiguration)} operation. */
    protected boolean configured;

    /** A callback called during the {@code classgen} phase of compilation */
    protected ClassgenCallback classgenCallback;
    /** A callback for use during {@link #compile()} */
    protected ProgressCallback progressCallback;

    protected ClassNodeResolver classNodeResolver = new ClassNodeResolver();
    protected ResolveVisitor resolveVisitor = new ResolveVisitor(this);

    /** The AST transformations state data. */
    protected ASTTransformationsContext astTransformationsContext;

    private Set<javax.tools.JavaFileObject> javaCompilationUnitSet = new HashSet<>();

    /**
     * Initializes the CompilationUnit with defaults.
     */
    public CompilationUnit() {
        this(null, null, null);
    }

    /**
     * Initializes the CompilationUnit with defaults except for class loader.
     */
    public CompilationUnit(final GroovyClassLoader loader) {
        this(null, null, loader);
    }

    /**
     * Initializes the CompilationUnit with no security considerations.
     */
    public CompilationUnit(final CompilerConfiguration configuration) {
        this(configuration, null, null);
    }

    /**
     * Initializes the CompilationUnit with a CodeSource for controlling
     * security stuff and a class loader for loading classes.
     */
    public CompilationUnit(final CompilerConfiguration configuration, final CodeSource codeSource, final GroovyClassLoader loader) {
        this(configuration, codeSource, loader, null);
    }

    /**
     * Initializes the CompilationUnit with a CodeSource for controlling
     * security stuff, a class loader for loading classes, and a class
     * loader for loading AST transformations.
     * <p>
     * <b>Note</b>: The transform loader must be able to load compiler classes.
     * That means {@link #classLoader} must be at last a parent to {@code transformLoader}.
     * The other loader has no such constraint.
     *
     * @param transformLoader - the loader for transforms
     * @param loader          - loader used to resolve classes against during compilation
     * @param codeSource      - security setting for the compilation
     * @param configuration   - compilation configuration
     */
    public CompilationUnit(final CompilerConfiguration configuration, final CodeSource codeSource,
                           final GroovyClassLoader loader, final GroovyClassLoader transformLoader) {
        super(configuration, loader, null);

        this.astTransformationsContext = new ASTTransformationsContext(this, transformLoader);
        this.ast = new CompileUnit(getClassLoader(), codeSource, getConfiguration());

        addPhaseOperations();
        applyCompilationCustomizers();
    }

    private void addPhaseOperations() {
        addPhaseOperation(SourceUnit::parse, Phases.PARSING);

        addPhaseOperation(source -> {
            source.convert();
            // add module to compile unit
            getAST().addModule(source.getAST());
            Optional.ofNullable(getProgressCallback())
                .ifPresent(callback -> callback.call(source, getPhase()));
        }, Phases.CONVERSION);

        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
            GroovyClassVisitor visitor = new EnumVisitor(this, source);
            visitor.visitClass(classNode);
        }, Phases.CONVERSION);

        addPhaseOperation(source -> {
            for (ClassNode classNode : source.getAST().getClasses()) {
                resolveVisitor.setClassNodeResolver(classNodeResolver);
                resolveVisitor.startResolving(classNode, source);
            }
        }, Phases.SEMANTIC_ANALYSIS);

        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
            GroovyClassVisitor visitor = new StaticImportVisitor(classNode, source);
            visitor.visitClass(classNode);
        }, Phases.SEMANTIC_ANALYSIS);

        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
            GroovyClassVisitor visitor = new InnerClassVisitor(this, source);
            visitor.visitClass(classNode);
        }, Phases.SEMANTIC_ANALYSIS);

        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
            if (!classNode.isSynthetic()) {
                GroovyClassVisitor visitor = new GenericsVisitor(source);
                visitor.visitClass(classNode);
            }
        }, Phases.SEMANTIC_ANALYSIS);

        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
            TraitComposer.doExtendTraits(classNode, source, this);
        }, Phases.CANONICALIZATION);

        addPhaseOperation(source -> {
            List<ClassNode> classes = source.getAST().getClasses();
            for (ClassNode node : classes) {
                CompileUnit cu = node.getCompileUnit();
                for (Iterator<String> it = cu.getClassesToCompile().keySet().iterator(); it.hasNext(); ) {
                    String name = it.next();
                    StringBuilder message = new StringBuilder();
                    message
                            .append("Compilation incomplete: expected to find the class ")
                            .append(name)
                            .append(" in ")
                            .append(source.getName());
                    if (classes.isEmpty()) {
                        message.append(", but the file seems not to contain any classes");
                    } else {
                        message.append(", but the file contains the classes: ");
                        boolean first = true;
                        for (ClassNode cn : classes) {
                            if (first) {
                                first = false;
                            } else {
                                message.append(", ");
                            }
                            message.append(cn.getName());
                        }
                    }

                    getErrorCollector().addErrorAndContinue(Message.create(message.toString(), this));

                    it.remove();
                }
            }
        }, Phases.CANONICALIZATION);

        addPhaseOperation(classgen, Phases.CLASS_GENERATION);

        addPhaseOperation(groovyClass -> {
            String name = groovyClass.getName().replace('.', File.separatorChar) + ".class";
            File path = new File(getConfiguration().getTargetDirectory(), name);

            // ensure the path is ready for the file
            File directory = path.getParentFile();
            if (directory != null && !directory.exists()) {
                directory.mkdirs();
            }

            // create the file and write out the data
            try (FileOutputStream stream = new FileOutputStream(path)) {
                stream.write(groovyClass.getBytes());
            } catch (IOException e) {
                getErrorCollector().addError(Message.create(e.getMessage(), this));
            }
        });

        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
            AnnotationCollectorTransform.ClassChanger xformer = new AnnotationCollectorTransform.ClassChanger();
            xformer.transformClass(classNode);
        }, Phases.SEMANTIC_ANALYSIS);
        ASTTransformationVisitor.addPhaseOperations(this);

        // post-transform operations:

        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
            StaticVerifier verifier = new StaticVerifier();
            verifier.visitClass(classNode, source);
        }, Phases.SEMANTIC_ANALYSIS);

        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
            GroovyClassVisitor visitor = new InnerClassCompletionVisitor(this, source);
            visitor.visitClass(classNode);

            visitor = new EnumCompletionVisitor(this, source);
            visitor.visitClass(classNode);
        }, Phases.CANONICALIZATION);

        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
            Object callback = classNode.getNodeMetaData(DYNAMIC_OUTER_NODE_CALLBACK);
            if (callback instanceof IPrimaryClassNodeOperation) {
                ((IPrimaryClassNodeOperation) callback).call(source, context, classNode);
                classNode.removeNodeMetaData(DYNAMIC_OUTER_NODE_CALLBACK);
            }
        }, Phases.INSTRUCTION_SELECTION);

        addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
            // TODO: Can this be moved into org.codehaus.groovy.transform.sc.transformers.VariableExpressionTransformer?
            GroovyClassVisitor visitor = new ClassCodeExpressionTransformer() {
                @Override
                protected SourceUnit getSourceUnit() {
                    return source;
                }

                @Override
                public Expression transform(final Expression expression) {
                    if (expression instanceof VariableExpression) {
                        // check for "switch(enumType) { case CONST: ... }"
                        ClassNode enumType = expression.getNodeMetaData(SWITCH_CONDITION_EXPRESSION_TYPE);
                        if (enumType != null) {
                            // replace "CONST" variable expression with "EnumType.CONST" property expression
                            Expression propertyExpression = propX(classX(enumType), expression.getText());
                            setSourcePosition(propertyExpression, expression);
                            return propertyExpression;
                        }
                    }
                    return expression;
                }
            };
            visitor.visitClass(classNode);
        }, Phases.INSTRUCTION_SELECTION);
    }

    private void applyCompilationCustomizers() {
        for (CompilationCustomizer customizer : getConfiguration().getCompilationCustomizers()) {
            if (customizer instanceof CompilationUnitAware) {
                ((CompilationUnitAware) customizer).setCompilationUnit(this);
            }
            addPhaseOperation(customizer, customizer.getPhase().getPhaseNumber());
        }
    }

    public void addPhaseOperation(final IGroovyClassOperation op) {
        phaseOperations[Phases.OUTPUT].addFirst(op);
    }

    public void addPhaseOperation(final ISourceUnitOperation op, final int phase) {
        validatePhase(phase);
        phaseOperations[phase].add(op);
    }

    public void addPhaseOperation(final IPrimaryClassNodeOperation op, final int phase) {
        validatePhase(phase);
        phaseOperations[phase].add(op);
    }

    public void addFirstPhaseOperation(final IPrimaryClassNodeOperation op, final int phase) {
        validatePhase(phase);
        phaseOperations[phase].addFirst(op);
    }

    public void addNewPhaseOperation(final ISourceUnitOperation op, final int phase) {
        validatePhase(phase);
        newPhaseOperations[phase].add(op);
    }

    private static void validatePhase(final int phase) {
        if (phase < 1 || phase > Phases.ALL) {
            throw new IllegalArgumentException("phase " + phase + " is unknown");
        }
    }

    /**
     * Configures its debugging mode and classloader classpath from a given compiler configuration.
     * This cannot be done more than once due to limitations in {@link java.net.URLClassLoader URLClassLoader}.
     */
    @Override
    public void configure(final CompilerConfiguration configuration) {
        super.configure(configuration);
        this.debug = getConfiguration().getDebug();
        this.configured = true;
    }

    /**
     * Returns the CompileUnit that roots our AST.
     */
    public CompileUnit getAST() {
        return this.ast;
    }

    /**
     * Get the GroovyClasses generated by compile().
     */
    public List<GroovyClass> getClasses() {
        return generatedClasses;
    }

    /**
     * Convenience routine to get the first ClassNode, for
     * when you are sure there is only one.
     */
    public ClassNode getFirstClassNode() {
        return getAST().getModules().get(0).getClasses().get(0);
    }

    /**
     * Convenience routine to get the named ClassNode.
     */
    public ClassNode getClassNode(final String name) {
        ClassNode[] result = new ClassNode[1];
        IPrimaryClassNodeOperation handler = (source, context, classNode) -> {
            if (classNode.getName().equals(name)) {
                result[0] = classNode;
            }
        };
        try {
            handler.doPhaseOperation(this);
        } catch (CompilationFailedException e) {
            if (debug) e.printStackTrace();
        }
        return result[0];
    }

    /**
     * @return the AST transformations current context
     */
    public ASTTransformationsContext getASTTransformationsContext() {
        return astTransformationsContext;
    }

    public ClassNodeResolver getClassNodeResolver() {
        return classNodeResolver;
    }

    public void setClassNodeResolver(final ClassNodeResolver classNodeResolver) {
        this.classNodeResolver = classNodeResolver;
    }

    public Set<javax.tools.JavaFileObject> getJavaCompilationUnitSet() {
        return javaCompilationUnitSet;
    }

    public void addJavaCompilationUnits(final Set<javax.tools.JavaFileObject> javaCompilationUnitSet) {
        this.javaCompilationUnitSet.addAll(javaCompilationUnitSet);
    }

    /**
     * Returns the class loader for loading AST transformations.
     */
    public GroovyClassLoader getTransformLoader() {
        return Optional.ofNullable(getASTTransformationsContext().getTransformLoader()).orElseGet(this::getClassLoader);
    }

    //---------------------------------------------------------------------------
    // SOURCE CREATION

    /**
     * Adds a set of file paths to the unit.
     */
    public void addSources(final String[] paths) {
        for (String path : paths) {
            addSource(new File(path));
        }
    }

    /**
     * Adds a set of source files to the unit.
     */
    public void addSources(final File[] files) {
        for (File file : files) {
            addSource(file);
        }
    }

    /**
     * Adds a source file to the unit.
     */
    public SourceUnit addSource(final File file) {
        return addSource(new SourceUnit(file, getConfiguration(), getClassLoader(), getErrorCollector()));
    }

    /**
     * Adds a source file to the unit.
     */
    public SourceUnit addSource(final URL url) {
        return addSource(new SourceUnit(url, getConfiguration(), getClassLoader(), getErrorCollector()));
    }

    /**
     * Adds a InputStream source to the unit.
     */
    public SourceUnit addSource(final String name, final InputStream stream) {
        ReaderSource source = new InputStreamReaderSource(stream, getConfiguration());
        return addSource(new SourceUnit(name, source, getConfiguration(), getClassLoader(), getErrorCollector()));
    }

    public SourceUnit addSource(final String name, final String scriptText) {
        return addSource(new SourceUnit(name, scriptText, getConfiguration(), getClassLoader(), getErrorCollector()));
    }

    /**
     * Adds a SourceUnit to the unit.
     */
    public SourceUnit addSource(final SourceUnit source) {
        String name = source.getName();
        source.setClassLoader(getClassLoader());
        for (SourceUnit su : queuedSources) {
            if (name.equals(su.getName())) return su;
        }
        queuedSources.add(source);
        return source;
    }

    /**
     * Returns an iterator on the unit's SourceUnits.
     */
    public Iterator<SourceUnit> iterator() {
        return new Iterator<SourceUnit>() {
            private Iterator<String> nameIterator = sources.keySet().iterator();
            @Override
            public boolean hasNext() {
                return nameIterator.hasNext();
            }
            @Override
            public SourceUnit next() {
                String name = nameIterator.next();
                return sources.get(name);
            }
            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    /**
     * Adds a ClassNode directly to the unit (ie. without source).
     * WARNING: the source is needed for error reporting, using
     * this method without setting a SourceUnit will cause
     * NullPinterExceptions
     */
    public void addClassNode(final ClassNode node) {
        ModuleNode module = new ModuleNode(getAST());
        getAST().addModule(module);
        module.addClass(node);
    }

    //---------------------------------------------------------------------------
    // EXTERNAL CALLBACKS

    /**
     * A callback interface you can use during the {@code classgen}
     * phase of compilation as the compiler traverses the ClassNode tree.
     * You will be called-back for each primary and inner class.
     * Use setClassgenCallback() before running compile() to set your callback.
     */
    @FunctionalInterface
    public interface ClassgenCallback {
        void call(ClassVisitor writer, ClassNode node) throws CompilationFailedException;
    }

    public ClassgenCallback getClassgenCallback() {
        return classgenCallback;
    }

    /**
     * Sets a ClassgenCallback.  You can have only one, and setting
     * it to {@code null} removes any existing setting.
     */
    public void setClassgenCallback(final ClassgenCallback visitor) {
        this.classgenCallback = visitor;
    }

    /**
     * A callback interface you can use to get a callback after every
     * unit of the compile process.  You will be called-back with a
     * ProcessingUnit and a phase indicator.  Use setProgressCallback()
     * before running compile() to set your callback.
     */
    @FunctionalInterface
    public interface ProgressCallback {
        void call(ProcessingUnit context, int phase) throws CompilationFailedException;
    }

    public ProgressCallback getProgressCallback() {
        return progressCallback;
    }

    /**
     * Sets a ProgressCallback.  You can have only one, and setting
     * it to {@code null} removes any existing setting.
     */
    public void setProgressCallback(final ProgressCallback callback) {
        this.progressCallback = callback;
    }

    //---------------------------------------------------------------------------
    // ACTIONS

    /**
     * Synonym for {@code compile(Phases.ALL)}.
     */
    public void compile() throws CompilationFailedException {
        compile(Phases.ALL);
    }

    /**
     * Compiles the compilation unit from sources.
     */
    public void compile(int throughPhase) throws CompilationFailedException {
        // to support incremental compilation, always restart the compiler;
        // individual passes are responsible for not re-processing old code
        gotoPhase(Phases.INITIALIZATION);
        throughPhase = Math.min(throughPhase, Phases.ALL);

        while (throughPhase >= phase && phase <= Phases.ALL) {
            if (phase == Phases.CONVERSION) {
                (sources.size() > 1 && Boolean.TRUE.equals(configuration.getOptimizationOptions().get(CompilerConfiguration.PARALLEL_PARSE))
                        ? sources.values().parallelStream() : sources.values().stream()
                ).forEach(SourceUnit::buildAST);
            }
            try {
                processPhaseOperations(phase);
            } catch (ResolveVisitor.Interrupt x) {
                assert !queuedSources.isEmpty();
            }
            if (dequeued()) continue; // bring new sources into phase

            // Grab processing may have brought in new AST transforms into various phases, process them as well
            processNewPhaseOperations(phase);

            Optional.ofNullable(getProgressCallback())
                .ifPresent(callback -> callback.call(this, phase));
            completePhase();
            mark();

            gotoPhase(phase + 1);

            if (phase == Phases.CLASS_GENERATION) {
                getAST().getModules().forEach(ModuleNode::sortClasses);
            }
        }

        getErrorCollector().failIfErrors();
    }

    private void processPhaseOperations(final int phase) {
        for (PhaseOperation op : phaseOperations[phase]) {
            op.doPhaseOperation(this);
        }
    }

    private void processNewPhaseOperations(final int phase) {
        recordPhaseOpsInAllOtherPhases(phase);
        Deque<PhaseOperation> currentPhaseNewOps = newPhaseOperations[phase];
        while (!currentPhaseNewOps.isEmpty()) {
            PhaseOperation operation = currentPhaseNewOps.removeFirst();
            // push this operation to master list and then process it
            phaseOperations[phase].add(operation);
            operation.doPhaseOperation(this);
            // if this operation has brought in more phase ops for ast transforms, keep recording them
            // in master list of other phases and keep processing them for this phase
            recordPhaseOpsInAllOtherPhases(phase);
            currentPhaseNewOps = newPhaseOperations[phase];
        }
    }

    private void recordPhaseOpsInAllOtherPhases(final int phase) {
        // apart from current phase, push new operations for every other phase in the master phase ops list
        for (int ph = Phases.INITIALIZATION; ph <= Phases.ALL; ph += 1) {
            if (ph != phase && !newPhaseOperations[ph].isEmpty()) {
                phaseOperations[ph].addAll(newPhaseOperations[ph]);
                newPhaseOperations[ph].clear();
            }
        }
    }

    /**
     * Dequeues any source units added through addSource and resets the compiler
     * phase to initialization.
     * <p>
     * Note: this does not mean a file is recompiled. If a SourceUnit has already
     * passed a phase it is skipped until a higher phase is reached.
     *
     * @return true if there was a queued source
     * @throws CompilationFailedException
     */
    protected boolean dequeued() throws CompilationFailedException {
        if (!queuedSources.isEmpty()) { SourceUnit unit;
            while ((unit = queuedSources.poll()) != null) {
                sources.put(unit.getName(), unit);
            }
            gotoPhase(Phases.INITIALIZATION);
            return true;
        }
        return false;
    }

    /**
     * Runs the class generation phase on a single {@code ClassNode}.
     */
    private final IPrimaryClassNodeOperation classgen = new IPrimaryClassNodeOperation() {
        @Override
        public void call(final SourceUnit source, final GeneratorContext context, final ClassNode classNode) throws CompilationFailedException {
            new OptimizerVisitor(CompilationUnit.this).visitClass(classNode, source); // GROOVY-4272: repositioned from static import visitor

            //
            // Run the Verifier on the outer class
            //
            GroovyClassVisitor visitor = new Verifier();
            try {
                visitor.visitClass(classNode);
            } catch (RuntimeParserException rpe) {
                getErrorCollector().addError(new SyntaxException(rpe.getMessage(), rpe.getNode()), source);
            }

            visitor = new LabelVerifier(source);
            visitor.visitClass(classNode);

            visitor = new InstanceOfVerifier() {
                @Override
                protected SourceUnit getSourceUnit() {
                    return source;
                }
            };
            visitor.visitClass(classNode);

            visitor = new ClassCompletionVerifier(source);
            visitor.visitClass(classNode);

            visitor = new ExtendedVerifier(source);
            visitor.visitClass(classNode);

            // because the class may be generated even if a error was found
            // and that class may have an invalid format we fail here if needed
            getErrorCollector().failIfErrors();

            //
            // Prep the generator machinery
            //
            ClassVisitor classVisitor = createClassVisitor();

            String sourceName = (source == null ? classNode.getModule().getDescription() : source.getName());
            // only show the file name and its extension like javac does in its stacktraces rather than the full path
            // also takes care of both \ and / depending on the host compiling environment
            if (sourceName != null) {
                sourceName = sourceName.substring(Math.max(sourceName.lastIndexOf('\\'), sourceName.lastIndexOf('/')) + 1);
            }

            //
            // Run the generation and create the class (if required)
            //
            visitor = new AsmClassGenerator(source, context, classVisitor, sourceName);
            visitor.visitClass(classNode);

            byte[] bytes = ((ClassWriter) classVisitor).toByteArray();
            getClasses().add(new GroovyClass(classNode.getName(), bytes));

            //
            // Handle any callback that's been set
            //
            if (classgenCallback != null) {
                classgenCallback.call(classVisitor, classNode);
            }

            //
            // Recurse for inner classes
            //
            LinkedList<ClassNode> innerClasses = ((AsmClassGenerator) visitor).getInnerClasses();
            while (!innerClasses.isEmpty()) {
                classgen.call(source, context, innerClasses.removeFirst());
            }
        }

        @Override
        public boolean needSortedInput() {
            return true;
        }
    };

    protected ClassVisitor createClassVisitor() {
        return new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES) {
            private ClassNode getClassNode(String name) {
                // try classes under compilation
                CompileUnit cu = getAST();
                ClassNode cn = cu.getClass(name);
                if (cn != null) return cn;
                // try inner classes
                cn = cu.getGeneratedInnerClass(name);
                if (cn != null) return cn;
                ClassNodeResolver.LookupResult lookupResult = getClassNodeResolver().resolveName(name, CompilationUnit.this);
                return lookupResult == null ? null : lookupResult.getClassNode();
            }
            private ClassNode getCommonSuperClassNode(ClassNode c, ClassNode d) {
                // adapted from ClassWriter code
                if (c.isDerivedFrom(d)) return d;
                if (d.isDerivedFrom(c)) return c;
                if (c.isInterface() || d.isInterface()) return ClassHelper.OBJECT_TYPE;
                do {
                    c = c.getSuperClass();
                } while (c != null && !d.isDerivedFrom(c));
                if (c == null) return ClassHelper.OBJECT_TYPE;
                return c;
            }
            @Override
            protected String getCommonSuperClass(String arg1, String arg2) {
                ClassNode a = getClassNode(arg1.replace('/', '.'));
                ClassNode b = getClassNode(arg2.replace('/', '.'));
                return getCommonSuperClassNode(a,b).getName().replace('.','/');
            }
        };
    }

    //---------------------------------------------------------------------------
    // PHASE HANDLING

    /**
     * Updates the phase marker on all sources.
     */
    protected void mark() throws CompilationFailedException {
        ISourceUnitOperation mark = (final SourceUnit source) -> {
            if (source.phase < phase) {
                source.gotoPhase(phase);
            }
            if (source.phase == phase && phaseComplete && !source.phaseComplete) {
                source.completePhase();
            }
        };
        mark.doPhaseOperation(this);
    }

    //---------------------------------------------------------------------------
    // LOOP SIMPLIFICATION FOR SourceUnit OPERATIONS

    private interface PhaseOperation {
        void doPhaseOperation(CompilationUnit unit);
    }

    @FunctionalInterface
    public interface ISourceUnitOperation extends PhaseOperation {
        void call(SourceUnit source) throws CompilationFailedException;

        /**
         * A loop driver for applying operations to all SourceUnits.
         * Automatically skips units that have already been processed
         * through the current phase.
         */
        @Override
        default void doPhaseOperation(final CompilationUnit unit) throws CompilationFailedException {
            for (Map.Entry<String, SourceUnit> entry : unit.sources.entrySet()) {
                SourceUnit source = entry.getValue();
                if (source.phase < unit.phase || (source.phase == unit.phase && !source.phaseComplete)) {
                    try {
                        this.call(source);
                    } catch (CompilationFailedException e) {
                        throw e;
                    } catch (Exception e) {
                        GroovyBugError gbe = new GroovyBugError(e);
                        unit.changeBugText(gbe, source);
                        throw gbe;
                    } catch (GroovyBugError e) {
                        unit.changeBugText(e, source);
                        throw e;
                    }
                }
            }
            unit.getErrorCollector().failIfErrors();
        }
    }

    //---------------------------------------------------------------------------
    // LOOP SIMPLIFICATION FOR PRIMARY ClassNode OPERATIONS

    @FunctionalInterface
    public interface IPrimaryClassNodeOperation extends PhaseOperation {
        void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException;

        /**
         * A loop driver for applying operations to all primary ClassNodes in
         * our AST.  Automatically skips units that have already been processed
         * through the current phase.
         */
        @Override
        default void doPhaseOperation(final CompilationUnit unit) throws CompilationFailedException {
            for (ClassNode classNode : unit.getPrimaryClassNodes(this.needSortedInput())) {
                SourceUnit context = null;
                try {
                    context = classNode.getModule().getContext();
                    if (context == null || context.phase < unit.phase || (context.phase == unit.phase && !context.phaseComplete)) {
                        int offset = 1;
                        for (Iterator<InnerClassNode> it = classNode.getInnerClasses(); it.hasNext(); ) {
                            it.next();
                            offset += 1;
                        }
                        this.call(context, new GeneratorContext(unit.getAST(), offset), classNode);
                    }
                } catch (CompilationFailedException e) {
                    // fall through
                } catch (NullPointerException npe) {
                    GroovyBugError gbe = new GroovyBugError("unexpected NullPointerException", npe);
                    unit.changeBugText(gbe, context);
                    throw gbe;
                } catch (GroovyBugError e) {
                    unit.changeBugText(e, context);
                    throw e;
                } catch (Exception | LinkageError e) {
                    ErrorCollector errorCollector = null;
                    // check for a nested compilation exception
                    for (Throwable t = e.getCause(); t != e && t != null; t = t.getCause()) {
                        if (t instanceof MultipleCompilationErrorsException) {
                            errorCollector = ((MultipleCompilationErrorsException) t).getErrorCollector();
                            break;
                        }
                    }

                    if (errorCollector != null) {
                        unit.getErrorCollector().addCollectorContents(errorCollector);
                    } else {
                        if (e instanceof GroovyRuntimeException) {
                            GroovyRuntimeException gre = (GroovyRuntimeException) e;
                            context = Optional.ofNullable(gre.getModule()).map(ModuleNode::getContext).orElse(context);
                        }
                        if (context != null) {
                            if (e instanceof SyntaxException) {
                                unit.getErrorCollector().addError((SyntaxException) e, context);
                            } else if (e.getCause() instanceof SyntaxException) {
                                unit.getErrorCollector().addError((SyntaxException) e.getCause(), context);
                            } else {
                                unit.getErrorCollector().addException(e instanceof Exception ? (Exception) e : new RuntimeException(e), context);
                            }
                        } else {
                            unit.getErrorCollector().addError(new ExceptionMessage(e instanceof Exception ? (Exception) e : new RuntimeException(e), unit.debug, unit));
                        }
                    }
                }
            }
            unit.getErrorCollector().failIfErrors();
        }

        default boolean needSortedInput() {
            return false;
        }
    }

    @FunctionalInterface
    public interface IGroovyClassOperation extends PhaseOperation {
        void call(GroovyClass groovyClass) throws CompilationFailedException;

        @Override
        default void doPhaseOperation(final CompilationUnit unit) throws CompilationFailedException {
            if (unit.phase != Phases.OUTPUT && !(unit.phase == Phases.CLASS_GENERATION && unit.phaseComplete)) {
                throw new GroovyBugError("CompilationUnit not ready for output(). Current phase=" + unit.getPhaseDescription());
            }

            for (GroovyClass groovyClass : unit.getClasses()) {
                try {
                    this.call(groovyClass);
                } catch (CompilationFailedException e) {
                    // fall through
                } catch (NullPointerException npe) {
                    throw npe;
                } catch (GroovyBugError e) {
                    unit.changeBugText(e, null);
                    throw e;
                } catch (Exception e) {
                    throw new GroovyBugError(e);
                }
            }
            unit.getErrorCollector().failIfErrors();
        }
    }

    private static int getSuperClassCount(ClassNode classNode) {
        int count = 0;
        while (classNode != null) {
            count += 1;
            classNode = classNode.getSuperClass();
        }
        return count;
    }

    private static int getSuperInterfaceCount(final ClassNode classNode) {
        int count = 1;
        for (ClassNode face : classNode.getInterfaces()) {
            count = Math.max(count, getSuperInterfaceCount(face) + 1);
        }
        return count;
    }

    private List<ClassNode> getPrimaryClassNodes(final boolean sort) {
        List<ClassNode> unsorted = getAST().getModules().stream()
            .flatMap(module -> module.getClasses().stream()).collect(toList());

        if (!sort) return unsorted;

        int n = unsorted.size();
        int[] indexClass = new int[n];
        int[] indexInterface = new int[n];
        {
            int i = 0;
            for (ClassNode element : unsorted) {
                if (element.isInterface()) {
                    indexInterface[i] = getSuperInterfaceCount(element);
                    indexClass[i] = -1;
                } else {
                    indexClass[i] = getSuperClassCount(element);
                    indexInterface[i] = -1;
                }
                i += 1;
            }
        }

        List<ClassNode> sorted = getSorted(indexInterface, unsorted);
        sorted.addAll(getSorted(indexClass, unsorted));
        return sorted;
    }

    private static List<ClassNode> getSorted(final int[] index, final List<ClassNode> unsorted) {
        int unsortedSize = unsorted.size();
        List<ClassNode> sorted = new ArrayList<>(unsortedSize);
        for (int i = 0; i < unsortedSize; i += 1) {
            int min = -1;
            for (int j = 0; j < unsortedSize; j += 1) {
                if (index[j] == -1) continue;
                if (min == -1 || index[j] < index[min]) {
                    min = j;
                }
            }
            if (min == -1) break;
            sorted.add(unsorted.get(min));
            index[min] = -1;
        }
        return sorted;
    }

    private void changeBugText(final GroovyBugError e, final SourceUnit context) {
        e.setBugText("exception in phase '" + getPhaseDescription() + "' in source unit '" + (context != null ? context.getName() : "?") + "' " + e.getBugText());
    }

    //--------------------------------------------------------------------------

    @Deprecated
    public void addPhaseOperation(final GroovyClassOperation op) {
        addPhaseOperation((IGroovyClassOperation) op);
    }

    @Deprecated
    public void addPhaseOperation(final SourceUnitOperation op, final int phase) {
        addPhaseOperation((ISourceUnitOperation) op, phase);
    }

    @Deprecated
    public void addPhaseOperation(final PrimaryClassNodeOperation op, final int phase) {
        addPhaseOperation((IPrimaryClassNodeOperation) op, phase);
    }

    @Deprecated
    public void addFirstPhaseOperation(final PrimaryClassNodeOperation op, final int phase) {
        addFirstPhaseOperation((IPrimaryClassNodeOperation) op, phase);
    }

    @Deprecated
    public void addNewPhaseOperation(final SourceUnitOperation op, final int phase) {
        addNewPhaseOperation((ISourceUnitOperation) op, phase);
    }

    @Deprecated
    public void applyToSourceUnits(final SourceUnitOperation op) throws CompilationFailedException {
        op.doPhaseOperation(this);
    }

    @Deprecated
    public void applyToPrimaryClassNodes(final PrimaryClassNodeOperation op) throws CompilationFailedException {
        op.doPhaseOperation(this);
    }

    @Deprecated
    public abstract static class SourceUnitOperation implements ISourceUnitOperation {
    }

    @Deprecated
    public abstract static class GroovyClassOperation implements IGroovyClassOperation {
    }

    @Deprecated
    public abstract static class PrimaryClassNodeOperation implements IPrimaryClassNodeOperation {
    }
}
