/*
 * 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.model.internal.inspect;

import org.gradle.internal.Cast;
import org.gradle.model.InvalidModelRuleDeclarationException;
import org.gradle.model.Model;
import org.gradle.model.internal.core.Hidden;
import org.gradle.model.internal.core.ModelPath;
import org.gradle.model.internal.core.ModelRegistrations;
import org.gradle.model.internal.core.MutableModelNode;

import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;

public abstract class AbstractModelCreationRuleExtractor extends AbstractAnnotationDrivenModelRuleExtractor<Model> {

    private ModelPath determineModelName(MethodRuleDefinition<?, ?> ruleDefinition, RuleSourceValidationProblemCollector problems) {
        String annotationValue = ruleDefinition.getAnnotation(Model.class).value();
        String modelName = (annotationValue == null || annotationValue.isEmpty()) ? ruleDefinition.getMethodName() : annotationValue;

        try {
            ModelPath.validatePath(modelName);
        } catch (Exception e) {
            problems.add(ruleDefinition, "The declared model element path '" + modelName + "' is not a valid path", e);
        }
        return ModelPath.path(modelName);
    }

    @Nullable
    @Override
    public <R, S> ExtractedModelRule registration(MethodRuleDefinition<R, S> ruleDefinition, MethodModelRuleExtractionContext context) {
        ModelPath modelPath = determineModelName(ruleDefinition, context);

        validateMethod(ruleDefinition, context);
        if (context.hasProblems()) {
            return null;
        }

        return buildRule(modelPath, ruleDefinition);
    }

    protected <R, S> void validateMethod(MethodRuleDefinition<R, S> ruleDefinition, MethodModelRuleExtractionContext context) {
    }

    protected abstract <R, S> ExtractedModelRule buildRule(ModelPath modelPath, MethodRuleDefinition<R, S> ruleDefinition);

    protected static abstract class ExtractedCreationRule<R, S>  extends AbstractExtractedModelRule {
        protected final ModelPath modelPath;
        private final boolean hidden;

        public ExtractedCreationRule(ModelPath modelPath, MethodRuleDefinition<R, S> ruleDefinition) {
            super(ruleDefinition);
            this.modelPath = modelPath;
            this.hidden = ruleDefinition.isAnnotationPresent(Hidden.class);
        }

        protected abstract void buildRegistration(MethodModelRuleApplicationContext context, ModelRegistrations.Builder registration);

        @Override
        public void apply(MethodModelRuleApplicationContext context, MutableModelNode target) {
            if (!target.getPath().equals(ModelPath.ROOT)) {
                throw new InvalidModelRuleDeclarationException(String.format("Rule %s cannot be applied at the scope of model element %s as creation rules cannot be used when applying rule sources to particular elements", getRuleDefinition().getDescriptor(), target.getPath()));
            }
            ModelRegistrations.Builder registration = ModelRegistrations.of(modelPath).descriptor(getRuleDefinition().getDescriptor());
            buildRegistration(context, registration);
            registration.hidden(hidden);

            context.getRegistry().register(registration.build());
        }

        @Override
        public List<Class<?>> getRuleDependencies() {
            return Collections.emptyList();
        }

        @Override
        public MethodRuleDefinition<R, S> getRuleDefinition() {
            return Cast.uncheckedCast(super.getRuleDefinition());
        }
    }
}
