/*
 *  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.macro.runtime;

import groovy.lang.Closure;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.builder.AstBuilder;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;

import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import static org.codehaus.groovy.macro.methods.MacroGroovyMethods.DOLLAR_VALUE;

/**
 * Runtime support for <pre>{@code macro {} }</pre> method.
 *
 * @since 2.5.0
 */
public enum MacroBuilder {
    /** Shared macro builder instance. */
    INSTANCE;

    /**
     * Builds a macro value from source text using the conversion phase.
     *
     * @param source the source to parse
     * @param context the substitution closures used for {@code $v} placeholders
     * @param resultClass the expected result type
     * @param <T> the expected result type
     * @return the built macro value
     */
    public <T> T macro(String source, final List<Closure<Expression>> context, Class<T> resultClass) {
        return macro(false, source, context, resultClass);
    }

    /**
     * Builds a macro value from source text using the conversion phase.
     *
     * @param asIs whether to keep a single-statement closure as a block
     * @param source the source to parse
     * @param context the substitution closures used for {@code $v} placeholders
     * @param resultClass the expected result type
     * @param <T> the expected result type
     * @return the built macro value
     */
    public <T> T macro(boolean asIs, String source, final List<Closure<Expression>> context, Class<T> resultClass) {
        return macro(null, asIs, source, context, resultClass);
    }

    /**
     * Builds a macro value from source text at the supplied compile phase.
     *
     * @param compilePhase the phase to use when parsing the source
     * @param source the source to parse
     * @param context the substitution closures used for {@code $v} placeholders
     * @param resultClass the expected result type
     * @param <T> the expected result type
     * @return the built macro value
     */
    public <T> T macro(CompilePhase compilePhase, String source, final List<Closure<Expression>> context, Class<T> resultClass) {
        return macro(compilePhase, false, source, context, resultClass);
    }

    private static final AtomicInteger COUNTER = new AtomicInteger();

    /**
     * Builds a macro value from source text.
     *
     * @param compilePhase the phase to use when parsing the source, or {@code null} for conversion
     * @param asIs whether to keep a single-statement closure as a block
     * @param source the source to parse
     * @param context the substitution closures used for {@code $v} placeholders
     * @param resultClass the expected result type
     * @param <T> the expected result type
     * @return the built macro value
     */
    @SuppressWarnings("unchecked")
    public <T> T macro(CompilePhase compilePhase, boolean asIs, String source, final List<Closure<Expression>> context, Class<T> resultClass) {
        boolean isClosure = source.startsWith("{");
        final String label = isClosure ? "__synthesized__label__" + COUNTER.incrementAndGet() + "__:" : "";
        final String labelledSource = label + source;

        if (compilePhase == null) {
            compilePhase = CompilePhase.CONVERSION;
        }

        List<ASTNode> nodes = (new AstBuilder()).buildFromString(compilePhase, true, labelledSource);

        for (ASTNode node : nodes) {
            if (node instanceof BlockStatement) {

                List<Statement> statements = ((BlockStatement) node).getStatements();
                if (!statements.isEmpty()) {
                    BlockStatement closureBlock = (BlockStatement) statements.get(0);

                    performSubstitutions(context, closureBlock);

                    return (T) getMacroValue(closureBlock, asIs);
                }
            }
            if (node instanceof ClassNode) {
                performSubstitutions(context, node);
                return (T) node;
            }
        }
        return null;
    }

    private static void performSubstitutions(final List<Closure<Expression>> context, final ASTNode astNode) {
        final Iterator<Closure<Expression>> iterator = context.iterator();
        ClassCodeExpressionTransformer trn = new ClassCodeExpressionTransformer() {
            /**
             * Replaces {@code $v} calls with the next substitution expression.
             *
             * @param expression the expression to transform
             * @return the transformed expression
             */
            @Override
            public Expression transform(Expression expression) {
                if (!(expression instanceof MethodCallExpression call)) {
                    return super.transform(expression);
                }

                if (!DOLLAR_VALUE.equals(call.getMethodAsString())) {
                    return super.transform(expression);
                }

                return iterator.next().call();
            }

            /**
             * Returns no source unit when applying runtime substitutions.
             *
             * @return {@code null}
             */
            @Override
            protected SourceUnit getSourceUnit() {
                // Could be null if there are no errors
                return null;
            }
        };
        if (astNode instanceof BlockStatement) {
            trn.visitBlockStatement((BlockStatement) astNode);
        } else if (astNode instanceof ClassNode) {
            trn.visitClass((ClassNode) astNode);
        }
    }

    /**
     * Extracts the value represented by a parsed macro closure.
     *
     * @param closureBlock the parsed closure block
     * @param asIs whether to keep a single-statement closure as a block
     * @return the resulting AST node
     */
    public static ASTNode getMacroValue(BlockStatement closureBlock, boolean asIs) {
        if(!asIs && closureBlock.getStatements().size() == 1) {
            Statement result = closureBlock.getStatements().get(0);
            if(result instanceof ExpressionStatement) {
                return ((ExpressionStatement) result).getExpression();
            } else {
                return result;
            }
        }
        return closureBlock;
    }
}
