/*
 * Copyright 2013-2026, Seqera Labs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package nextflow.config.control;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import nextflow.config.ast.ConfigApplyBlockNode;
import nextflow.config.ast.ConfigApplyNode;
import nextflow.config.ast.ConfigAssignNode;
import nextflow.config.ast.ConfigBlockNode;
import nextflow.config.ast.ConfigIncludeNode;
import nextflow.config.ast.ConfigNode;
import nextflow.config.ast.ConfigVisitorSupport;
import nextflow.config.dsl.ConfigDsl;
import nextflow.config.spec.SpecNode;
import nextflow.script.ast.ASTNodeMarker;
import nextflow.script.ast.ImplicitClosureParameter;
import nextflow.script.control.VariableScopeChecker;
import nextflow.script.dsl.ProcessDsl;
import nextflow.script.dsl.ScriptDsl;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.DynamicVariable;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.CatchStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.syntax.Types;

/**
 * Initialize the variable scopes for an AST.
 *
 * @see org.codehaus.groovy.classgen.VariableScopeVisitor
 *
 * @author Ben Sherman <bentshermann@gmail.com>
 */
class VariableScopeVisitor extends ConfigVisitorSupport {

    private SourceUnit sourceUnit;

    private VariableScopeChecker vsc;

    private Stack<String> configScopes = new Stack<>();

    public VariableScopeVisitor(SourceUnit sourceUnit) {
        this.sourceUnit = sourceUnit;
        this.vsc = new VariableScopeChecker(sourceUnit, new ClassNode(ConfigDsl.class));
    }

    @Override
    protected SourceUnit getSourceUnit() {
        return sourceUnit;
    }

    public void visit() {
        var moduleNode = sourceUnit.getAST();
        if( moduleNode instanceof ConfigNode cn ) {
            super.visit(cn);
            vsc.checkUnusedVariables();
        }
    }

    @Override
    public void visitConfigApplyBlock(ConfigApplyBlockNode node) {
        configScopes.add(node.name);
        var names = currentConfigScopes();
        var option = SpecNode.ROOT.getDslOption(names);
        if( option != null ) {
            vsc.pushScope(option.dsl());
            super.visitConfigApplyBlock(node);
            vsc.popScope();
        }
        else {
            // invalid config apply block is handled by ScriptAstBuilder
            // addError("Unrecognized config block '" + node.name + "'", node);
        }
        configScopes.pop();
    }

    @Override
    public void visitConfigApply(ConfigApplyNode node) {
        checkMethodCall(node);
    }

    private boolean inProcessScope;

    private boolean inClosure;

    @Override
    public void visitConfigAssign(ConfigAssignNode node) {
        for( int i = 0; i < node.names.size() - 1; i++ )
            configScopes.add(node.names.get(i));

        var scopes = currentConfigScopes();
        inProcessScope = isProcessScope(scopes, node);
        inClosure = node.value instanceof ClosureExpression;
        if( isWorkflowHandler(scopes, node) )
            vsc.addWarning("The use of workflow handlers in the config is deprecated -- use the entry workflow or a plugin instead", String.join(".", node.names), node);
        if( inClosure ) {
            vsc.pushScope(ScriptDsl.class);
            if( inProcessScope )
                vsc.pushScope(ProcessDsl.class);
        }

        super.visitConfigAssign(node);

        if( inClosure ) {
            if( inProcessScope )
                vsc.popScope();
            vsc.popScope();
        }
        inClosure = false;
        inProcessScope = false;

        for( int i = 0; i < node.names.size() - 1; i++ )
            configScopes.pop();
    }

    /**
     * Determine whether a config option can access the process
     * DSL for dynamic settings.
     *
     * This includes options in the `process` config scope and `executor.jobName`.
     *
     * @param scopes
     * @param node
     */
    private static boolean isProcessScope(List<String> scopes, ConfigAssignNode node) {
        if( scopes.isEmpty() )
            return false;
        if( "process".equals(scopes.get(0)) )
            return true;
        var option = node.names.get(node.names.size() - 1);
        return scopes.size() == 1
            && "executor".equals(scopes.get(0))
            && "jobName".equals(option);
    }

    private static boolean isWorkflowHandler(List<String> scopes, ConfigAssignNode node) {
        var option = node.names.get(node.names.size() - 1);
        return scopes.size() == 1
            && "workflow".equals(scopes.get(0))
            && List.of("onComplete", "onError").contains(option);
    }

    @Override
    public void visitMapEntryExpression(MapEntryExpression node) {
        node.getKeyExpression().visit(this);

        var ic = inClosure;
        if( inProcessScope && node.getValueExpression() instanceof ClosureExpression )
            inClosure = true;
        node.getValueExpression().visit(this);
        inClosure = ic;
    }

    @Override
    public void visitConfigBlock(ConfigBlockNode node) {
        var newScope = node.kind == null;
        if( newScope )
            configScopes.add(node.name);
        super.visitConfigBlock(node);
        if( newScope )
            configScopes.remove(configScopes.size() - 1);
    }

    @Override
    public void visitConfigInclude(ConfigIncludeNode node) {
        checkConfigInclude(node);
        visit(node.source);
    }

    private void checkConfigInclude(ConfigIncludeNode node) {
        if( configScopes.isEmpty() )
            return;
        if( configScopes.size() == 2 && "profiles".equals(configScopes.get(0)) )
            return;
        vsc.addError("Config includes are only allowed at the top-level or in a profile", node);
    }

    // statements

    @Override
    public void visitBlockStatement(BlockStatement node) {
        var newScope = node.getVariableScope() != null;
        if( newScope ) vsc.pushScope();
        node.setVariableScope(currentScope());
        super.visitBlockStatement(node);
        if( newScope ) vsc.popScope();
    }

    @Override
    public void visitCatchStatement(CatchStatement node) {
        vsc.pushScope();
        vsc.declare(node.getVariable(), node);
        super.visitCatchStatement(node);
        vsc.popScope();
    }

    @Override
    public void visitExpressionStatement(ExpressionStatement node) {
        var exp = node.getExpression();
        if( exp instanceof DeclarationExpression de ) {
            visitDeclarationExpression(de);
            return;
        }
        if( exp instanceof BinaryExpression be && Types.isAssignment(be.getOperation().getType()) ) {
            var source = be.getRightExpression();
            var target = be.getLeftExpression();
            visit(source);
            if( !visitAssignment(target) ) {
                visit(target);
            }
            return;
        }
        super.visitExpressionStatement(node);
    }

    private boolean visitAssignment(Expression node) {
        if( node instanceof TupleExpression te ) {
            var result = false;
            for( var el : te.getExpressions() )
                result |= visitAssignedVariable((VariableExpression) el);
            return result;
        }
        else if( node instanceof VariableExpression ve ) {
            return visitAssignedVariable(ve);
        }
        return false;
    }

    private boolean visitAssignedVariable(VariableExpression ve) {
        var variable = vsc.findVariableDeclaration(ve.getName(), ve);
        if( variable != null ) {
            ve.setAccessedVariable(variable);
            return false;
        }
        else {
            vsc.addError("`" + ve.getName() + "` was assigned but not declared", ve);
            return true;
        }
    }

    // expressions

    private static final List<String> KEYWORDS = List.of(
        "case",
        "for",
        "switch",
        "while"
    );

    @Override
    public void visitMethodCallExpression(MethodCallExpression node) {
        checkMethodCall(node);
        super.visitMethodCallExpression(node);
    }

    private void checkMethodCall(MethodCallExpression node) {
        if( !node.isImplicitThis() )
            return;
        var name = node.getMethodAsString();
        var methods = vsc.findDslFunction(name, node);
        if( methods.size() == 1 )
            node.putNodeMetaData(ASTNodeMarker.METHOD_TARGET, methods.get(0));
        else if( !methods.isEmpty() )
            node.putNodeMetaData(ASTNodeMarker.METHOD_OVERLOADS, methods);
        else if( !KEYWORDS.contains(name) )
            vsc.addError("`" + name + "` is not defined", node.getMethod());
    }

    @Override
    public void visitDeclarationExpression(DeclarationExpression node) {
        visit(node.getRightExpression());

        if( node.isMultipleAssignmentDeclaration() ) {
            for( var el : node.getTupleExpression() )
                vsc.declare((VariableExpression) el);
        }
        else {
            vsc.declare(node.getVariableExpression());
        }
    }

    @Override
    public void visitClosureExpression(ClosureExpression node) {
        vsc.pushScope();
        node.setVariableScope(currentScope());
        if( node.isParameterSpecified() ) {
            for( var parameter : node.getParameters() ) {
                vsc.declare(parameter, parameter);
                if( parameter.hasInitialExpression() )
                    parameter.getInitialExpression().visit(this);
            }
        }
        else if( node.getParameters() != null ) {
            var implicit = new ImplicitClosureParameter();
            currentScope().putDeclaredVariable(implicit);
        }
        super.visitClosureExpression(node);
        vsc.popScope();
    }

    @Override
    public void visitVariableExpression(VariableExpression node) {
        var name = node.getName();
        Variable variable = vsc.findVariableDeclaration(name, node);
        if( variable == null ) {
            if( inProcessScope && inClosure ) {
                // dynamic process directives can reference process inputs which are not known at this point
            }
            else {
                variable = new DynamicVariable(name, false);
            }
        }
        if( variable instanceof ImplicitClosureParameter ) {
            vsc.addWarning("Implicit closure parameter is deprecated, declare an explicit parameter instead", variable.getName(), node);
        }
        if( variable != null ) {
            node.setAccessedVariable(variable);
        }
    }

    // helpers

    private VariableScope currentScope() {
        return vsc.getCurrentScope();
    }

    private List<String> currentConfigScopes() {
        var names = new ArrayList<>(configScopes);
        if( !names.isEmpty() && "profiles".equals(names.get(0)) ) {
            if( !names.isEmpty() ) names.remove(0);
            if( !names.isEmpty() ) names.remove(0);
        }
        return names;
    }

}
