/*
 * Copyright 2014 the original author or authors.
 *
 * 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 org.gradle.platform.base.internal.registry;

import org.gradle.model.ModelMap;
import org.gradle.model.internal.core.ModelReference;
import org.gradle.model.internal.inspect.AbstractAnnotationDrivenModelRuleExtractor;
import org.gradle.model.internal.inspect.MethodRuleDefinition;
import org.gradle.model.internal.inspect.RuleSourceValidationProblemCollector;
import org.gradle.model.internal.type.ModelType;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.List;

public abstract class AbstractAnnotationDrivenComponentModelRuleExtractor<T extends Annotation> extends AbstractAnnotationDrivenModelRuleExtractor<T> {
    protected <V> void visitSubject(RuleMethodDataCollector dataCollector, MethodRuleDefinition<?, ?> ruleDefinition, ModelType<V> typeParameter, RuleSourceValidationProblemCollector problems) {
        if (ruleDefinition.getReferences().size() == 0) {
            problems.add(ruleDefinition, "A method " + getDescription() + " must have at least two parameters.");
            return;
        }

        ModelType<?> subjectType = ruleDefinition.getSubjectReference().getType();
        if (!isModelMap(subjectType)) {
            problems.add(ruleDefinition, String.format("The first parameter of a method %s must be of type %s.", getDescription(), ModelMap.class.getName()));
            return;
        }

        List<ModelType<?>> typeVariables = subjectType.getTypeVariables();
        if (typeVariables.size() != 1) {
            problems.add(ruleDefinition, String.format("Parameter of type %s must declare a type parameter extending %s.", ModelMap.class.getSimpleName(), typeParameter.getDisplayName()));
            return;
        }

        ModelType<?> elementType = typeVariables.get(0);
        if (elementType.isWildcard()) {
            problems.add(ruleDefinition, String.format("%s type %s cannot be a wildcard type (i.e. cannot use ? super, ? extends etc.).", typeParameter.getDisplayName(), elementType.getDisplayName()));
            return;
        }
        dataCollector.parameterTypes.put(typeParameter, elementType);
    }

    private boolean isModelMap(ModelType<?> modelType) {
        return ModelType.of(ModelMap.class).isAssignableFrom(modelType);
    }

    protected static class RuleMethodDataCollector {
        private HashMap<ModelType<?>, ModelType<?>> parameterTypes = new HashMap<ModelType<?>, ModelType<?>>();

        @SuppressWarnings("unchecked")
        public <S, R extends S> ModelType<R> getParameterType(ModelType<S> baseClass) {
            return (ModelType<R>) parameterTypes.get(baseClass);
        }

        public <S> void put(ModelType<S> baseClass, ModelType<? extends S> concreteClass) {
            parameterTypes.put(baseClass, concreteClass);
        }
    }

    protected <S> void visitDependency(RuleMethodDataCollector dataCollector, MethodRuleDefinition<?, ?> ruleDefinition, ModelType<S> expectedDependency, RuleSourceValidationProblemCollector problems) {
        if (ruleDefinition.getReferences().isEmpty() && problems.hasProblems()) {
            return;
        }

        List<ModelReference<?>> references = ruleDefinition.getReferences();
        ModelType<? extends S> dependency = null;
        for (ModelReference<?> reference : references) {
            if (expectedDependency.isAssignableFrom(reference.getType())) {
                if (dependency != null) {
                    problems.add(ruleDefinition, String.format("A method %s must have one parameter extending %s. Found multiple parameter extending %s.", getDescription(),
                                                expectedDependency.getDisplayName(),
                                                expectedDependency.getDisplayName()));
                    return;
                }
                dependency = reference.getType().asSubtype(expectedDependency);
            }
        }

        if (dependency == null) {
            problems.add(ruleDefinition, String.format("A method %s must have one parameter extending %s. Found no parameter extending %s.", getDescription(),
                    expectedDependency.getDisplayName(),
                    expectedDependency.getDisplayName()));
            return;
        }
        dataCollector.put(expectedDependency, dependency);
    }
}
