/*
 *  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.transform.stc;

import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.MethodCall;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.transform.trait.TraitASTTransformation;
import org.codehaus.groovy.transform.trait.Traits;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.chooseBestMethod;
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.isClassClassNodeWrappingConcreteType;

/**
 * An extension that handles field, super and static method calls within a trait.
 *
 * @since 2.3.0
 */
public class TraitTypeCheckingExtension extends AbstractTypeCheckingExtension {

    public TraitTypeCheckingExtension(final StaticTypeCheckingVisitor typeCheckingVisitor) {
        super(typeCheckingVisitor);
    }

    @Override
    public List<MethodNode> handleMissingMethod(final ClassNode receiver, final String name, final ArgumentListExpression argumentList, final ClassNode[] argumentTypes, final MethodCall call) {
        String[] decomposed = Traits.decomposeSuperCallName(name);
        if (decomposed != null) {
            String traitName = decomposed[0], methodName = decomposed[1];
            List<ClassNode> implementedTraits = Traits.findTraits(receiver);

            ClassNode nextTrait = null;
            for (int i = 0; i < implementedTraits.size() - 1; i += 1) {
                ClassNode implementedTrait = implementedTraits.get(i);
                if (implementedTrait.getName().equals(traitName)) {
                    nextTrait = implementedTraits.get(i + 1);
                }
            }

            ClassNode returnType = ClassHelper.OBJECT_TYPE;
            if (nextTrait != null) {
                List<MethodNode> candidates = typeCheckingVisitor.findMethod(nextTrait, methodName, argumentTypes);
                if (candidates.size() == 1) {
                    returnType = candidates.get(0).getReturnType();
                }
            }

            return Collections.singletonList(makeDynamic(call, returnType));
        }

        if (call instanceof MethodCallExpression) {
            MethodCallExpression mce = (MethodCallExpression) call;
            ClassNode returnType = mce.getNodeMetaData(TraitASTTransformation.DO_DYNAMIC);
            if (returnType != null) return Collections.singletonList(makeDynamic(call, returnType));

            // GROOVY-7322, GROOVY-8272, GROOVY-8587, GROOVY-8854, GROOVY-10312, GROOVY-12106: trait: this.m($static$self)
            ClassNode targetClass = isClassClassNodeWrappingConcreteType(receiver)? receiver.getGenericsTypes()[0].getType(): receiver;
            if (Traits.isTrait(targetClass.getOuterClass()) && argumentTypes.length > 0 && ClassHelper.isClassType(argumentTypes[0])) {
                List<ClassNode> traits = Traits.findTraits(targetClass.getOuterClass());
                traits.remove(targetClass.getOuterClass());

                for (ClassNode trait : traits) { // check super trait for static method
                    // GROOVY-12106: select over the super-trait helper's OWN declared statics
                    // with subtype-aware overload resolution, so an argument whose static type
                    // is a subtype of the declared parameter still resolves (matching plain-class
                    // static inheritance). chooseBestMethod (not findMethod) is used deliberately:
                    // it avoids folding in DGM/extension methods that could collide by name, and
                    // it makes the chosen candidate deterministic.
                    ClassNode helper = Traits.findHelper(trait);
                    List<MethodNode> candidates = new ArrayList<>();
                    for (MethodNode m : helper.getDeclaredMethods(name)) {
                        if (m.isStatic()) candidates.add(m);
                    }
                    if (!candidates.isEmpty()) {
                        List<MethodNode> best = chooseBestMethod(helper, candidates, argumentTypes);
                        if (!best.isEmpty()) { // one match -> its type; a tie -> Object (ambiguity-safe)
                            return Collections.singletonList(makeDynamic(call,
                                    best.size() == 1 ? best.get(0).getReturnType() : ClassHelper.OBJECT_TYPE));
                        }
                    }
                }
            }
        }

        return Collections.emptyList();
    }

    @Override
    public boolean handleUnresolvedProperty(final PropertyExpression pexp) {
        var objectExpression = pexp.getObjectExpression();
        if (objectExpression instanceof VariableExpression
                && objectExpression.getText().endsWith(Traits.THIS_OBJECT)) {
            String propertyName = pexp.getPropertyAsString();
            if (propertyName != null) {
                ClassNode objectExpressionType = getType(objectExpression);
                if (isClassClassNodeWrappingConcreteType(objectExpressionType)) {
                    objectExpressionType = objectExpressionType.getGenericsTypes()[0].getType();
                }
                for (ClassNode trait : Traits.findTraits(objectExpressionType)) {
                    if (propertyName.startsWith(trait.getName().replace('.', '_') + "__")) {
                        ClassNode staticFieldHelper = Traits.findStaticFieldHelper(trait);
                        if (staticFieldHelper != null) {
                            MethodNode getter = staticFieldHelper.getDeclaredMethod(propertyName + "$get", Parameter.EMPTY_ARRAY);
                            if (getter != null) { // GROOVY-11663: resolve "$self.pack_Type__name" to a static field access method
                                ClassNode returnType = typeCheckingVisitor.inferReturnTypeGenerics(objectExpressionType, getter, ArgumentListExpression.EMPTY_ARGUMENTS);
                                pexp.putNodeMetaData(StaticTypesMarker.DYNAMIC_RESOLUTION, returnType);
                                storeType(pexp, returnType);
                                return true;
                            }
                        }
                    }
                }
            }
        }
        return false;
    }
}
