/*
 *  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.apache.groovy.parser.antlr4;

import org.apache.groovy.util.Maps;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.ModifierNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.objectweb.asm.Opcodes;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.apache.groovy.parser.antlr4.GroovyLangParser.ABSTRACT;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.FINAL;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.NATIVE;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.STATIC;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.VOLATILE;

/**
 * Process modifiers for AST nodes
 */
class ModifierManager {
    private static final Map<Class, List<Integer>> INVALID_MODIFIERS_MAP = Maps.of(
            ConstructorNode.class, Arrays.asList(STATIC, FINAL, ABSTRACT, NATIVE),
            MethodNode.class, Arrays.asList(VOLATILE/*, TRANSIENT*/) // Transient is left open for properties for legacy reasons but should be removed before ClassCompletionVerifier runs (CLASSGEN)
    );
    private AstBuilder astBuilder;
    private List<ModifierNode> modifierNodeList;

    public ModifierManager(AstBuilder astBuilder, List<ModifierNode> modifierNodeList) {
        this.astBuilder = astBuilder;
        this.validate(modifierNodeList);
        this.modifierNodeList = Collections.unmodifiableList(modifierNodeList);
    }

    public int getModifierCount() {
        return modifierNodeList.size();
    }

    private void validate(List<ModifierNode> modifierNodeList) {
        Map<ModifierNode, Integer> modifierNodeCounter = new LinkedHashMap<>(modifierNodeList.size());
        int visibilityModifierCount = 0;

        for (ModifierNode modifierNode : modifierNodeList) {
            var count = modifierNodeCounter.get(modifierNode);
            if (count == null) {
                modifierNodeCounter.put(modifierNode, 1);
            } else if (count == 1 && !modifierNode.isRepeatable()) {
                throw astBuilder.createParsingFailedException("Cannot repeat modifier[" + modifierNode.getText() + "]", modifierNode);
            }

            if (modifierNode.isVisibilityModifier()) {
                visibilityModifierCount += 1;
                if (visibilityModifierCount > 1) {
                    throw astBuilder.createParsingFailedException("Cannot specify modifier[" + modifierNode.getText() + "] when access scope has already been defined", modifierNode);
                }
            }
        }
    }

    public void validate(MethodNode methodNode) {
        validate(INVALID_MODIFIERS_MAP.get(MethodNode.class), methodNode);
    }

    public void validate(ConstructorNode constructorNode) {
        validate(INVALID_MODIFIERS_MAP.get(ConstructorNode.class), constructorNode);
    }

    private void validate(List<Integer> invalidModifierList, MethodNode methodNode) {
        modifierNodeList.forEach(e -> {
            if (invalidModifierList.contains(e.getType())) {
                throw astBuilder.createParsingFailedException(methodNode.getClass().getSimpleName().replace("Node", "") + " has an incorrect modifier '" + e + "'.", methodNode);
            }
        });
    }

    // t    1: class modifiers value; 2: class member modifiers value
    private int calcModifiersOpValue(int t) {
        int result = 0;

        for (ModifierNode modifierNode : modifierNodeList) {
            result |= modifierNode.getOpcode();
        }

        if (!this.containsVisibilityModifier()) {
            if (1 == t) {
                result |= Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC;
            } else if (2 == t) {
                result |= Opcodes.ACC_PUBLIC;
            }
        }

        return result;
    }

    public int getClassModifiersOpValue() {
        return this.calcModifiersOpValue(1);
    }

    public int getClassMemberModifiersOpValue() {
        return this.calcModifiersOpValue(2);
    }

    public List<AnnotationNode> getAnnotations() {
        return modifierNodeList.stream()
                .filter(ModifierNode::isAnnotation)
                .map(ModifierNode::getAnnotationNode)
                .collect(Collectors.toList());
    }

    public boolean containsAny(final int... modifierTypes) {
        return modifierNodeList.stream().anyMatch(e -> {
            for (int modifierType : modifierTypes) {
                if (modifierType == e.getType()) {
                    return true;
                }
            }

            return false;
        });
    }

    public Optional<ModifierNode> get(int modifierType) {
        return modifierNodeList.stream().filter(e -> modifierType == e.getType()).findFirst();
    }

    public boolean containsAnnotations() {
        return modifierNodeList.stream().anyMatch(ModifierNode::isAnnotation);
    }

    public boolean containsVisibilityModifier() {
        return modifierNodeList.stream().anyMatch(ModifierNode::isVisibilityModifier);
    }

    public boolean containsNonVisibilityModifier() {
        return modifierNodeList.stream().anyMatch(ModifierNode::isNonVisibilityModifier);
    }

    public Parameter processParameter(Parameter parameter) {
        modifierNodeList.forEach(e -> {
            parameter.setModifiers(parameter.getModifiers() | e.getOpcode());

            if (e.isAnnotation()) {
                parameter.addAnnotation(e.getAnnotationNode());
            }
        });

        return parameter;
    }

    public int clearVisibilityModifiers(int modifiers) {
        return modifiers & ~Opcodes.ACC_PUBLIC & ~Opcodes.ACC_PROTECTED & ~Opcodes.ACC_PRIVATE;
    }

    public MethodNode processMethodNode(MethodNode mn) {
        modifierNodeList.forEach(e -> {
            mn.setModifiers((e.isVisibilityModifier() ? clearVisibilityModifiers(mn.getModifiers()) : mn.getModifiers()) | e.getOpcode());

            if (e.isAnnotation()) {
                mn.addAnnotation(e.getAnnotationNode());
            }
        });

        return mn;
    }

    public VariableExpression processVariableExpression(VariableExpression ve) {
        modifierNodeList.forEach(e -> {
            ve.setModifiers(ve.getModifiers() | e.getOpcode());

            // local variable does not attach annotations
        });

        return ve;
    }

    public <T extends AnnotatedNode> T attachAnnotations(T node) {
        this.getAnnotations().forEach(node::addAnnotation);

        return node;
    }
}
