/*
 * 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 com.google.common.collect.ImmutableList;
import org.gradle.api.Action;
import org.gradle.api.Named;
import org.gradle.api.Task;
import org.gradle.internal.Cast;
import org.gradle.model.ModelMap;
import org.gradle.model.internal.core.DirectNodeNoInputsModelAction;
import org.gradle.model.internal.core.DomainObjectCollectionBackedModelMap;
import org.gradle.model.internal.core.InstanceModelView;
import org.gradle.model.internal.core.ModelAction;
import org.gradle.model.internal.core.ModelActionRole;
import org.gradle.model.internal.core.ModelReference;
import org.gradle.model.internal.core.ModelView;
import org.gradle.model.internal.core.ModelViews;
import org.gradle.model.internal.core.MutableModelNode;
import org.gradle.model.internal.core.NamedEntityInstantiator;
import org.gradle.model.internal.inspect.AbstractExtractedModelRule;
import org.gradle.model.internal.inspect.ExtractedModelRule;
import org.gradle.model.internal.inspect.MethodModelRuleApplicationContext;
import org.gradle.model.internal.inspect.MethodModelRuleExtractionContext;
import org.gradle.model.internal.inspect.MethodRuleDefinition;
import org.gradle.model.internal.inspect.ModelRuleInvoker;
import org.gradle.model.internal.inspect.RuleSourceValidationProblemCollector;
import org.gradle.model.internal.type.ModelType;
import org.gradle.platform.base.BinaryContainer;
import org.gradle.platform.base.BinarySpec;
import org.gradle.platform.base.BinaryTasks;
import org.gradle.platform.base.plugins.BinaryBasePlugin;

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

import static org.gradle.model.internal.core.NodePredicate.allLinks;

public class BinaryTasksModelRuleExtractor extends AbstractAnnotationDrivenComponentModelRuleExtractor<BinaryTasks> {
    private static final ModelType<BinarySpec> BINARY_SPEC = ModelType.of(BinarySpec.class);
    private static final ModelType<NamedEntityInstantiator<Task>> TASK_FACTORY = new ModelType<NamedEntityInstantiator<Task>>() {
    };
    private static final ModelType<Task> TASK = ModelType.of(Task.class);
    private static final ModelReference<BinaryContainer> BINARIES_CONTAINER = ModelReference.of("binaries", ModelType.of(BinaryContainer.class));

    @Nullable
    @Override
    public <R, S> ExtractedModelRule registration(MethodRuleDefinition<R, S> ruleDefinition, MethodModelRuleExtractionContext context) {
        return createRegistration(ruleDefinition, context);
    }

    private <R, S extends BinarySpec> ExtractedModelRule createRegistration(final MethodRuleDefinition<R, ?> ruleDefinition, RuleSourceValidationProblemCollector problems) {
        RuleMethodDataCollector dataCollector = new RuleMethodDataCollector();
        verifyMethodSignature(dataCollector, ruleDefinition, problems);
        if (problems.hasProblems()) {
            return null;
        }

        ModelType<S> binaryType = dataCollector.getParameterType(BINARY_SPEC);
        return new ExtractedBinaryTasksRule<S>(ruleDefinition, binaryType);
    }

    private void verifyMethodSignature(RuleMethodDataCollector taskDataCollector, MethodRuleDefinition<?, ?> ruleDefinition, RuleSourceValidationProblemCollector problems) {
        validateIsVoidMethod(ruleDefinition, problems);
        visitSubject(taskDataCollector, ruleDefinition, TASK, problems);
        visitDependency(taskDataCollector, ruleDefinition, BINARY_SPEC, problems);
    }

    private static class BinaryTaskRule<T extends BinarySpec> extends ModelMapBasedRule<T, T> {

        public BinaryTaskRule(ModelType<T> binaryType, MethodRuleDefinition<?, ?> ruleDefinition) {
            super(ModelReference.of(binaryType), binaryType, ruleDefinition, ModelReference.of(TASK_FACTORY));
        }

        @Override
        protected void execute(ModelRuleInvoker<?> invoker, final T binary, List<ModelView<?>> inputs) {
            NamedEntityInstantiator<Task> taskFactory = Cast.uncheckedCast(ModelViews.getInstance(inputs.get(0), TASK_FACTORY));
            ModelMap<Task> cast = DomainObjectCollectionBackedModelMap.wrap(
                    "tasks",
                    Task.class,
                    binary.getTasks(),
                    taskFactory,
                    Named.Namer.INSTANCE,
                    new Action<Task>() {
                        @Override
                        public void execute(Task task) {
                            binary.getTasks().add(task);
                            binary.builtBy(task);
                        }
                    });

            List<ModelView<?>> inputsWithBinary = new ArrayList<ModelView<?>>(inputs.size());
            inputsWithBinary.addAll(inputs.subList(1, inputs.size()));
            inputsWithBinary.add(InstanceModelView.of(getSubject().getPath(), getSubject().getType(), binary));

            invoke(invoker, inputsWithBinary, cast, binary, binary);
        }
    }

    private static class ExtractedBinaryTasksRule<T extends BinarySpec>  extends AbstractExtractedModelRule {
        private final ModelType<T> binaryType;

        public ExtractedBinaryTasksRule(MethodRuleDefinition<?, ?> ruleDefinition, ModelType<T> binaryType) {
            super(ruleDefinition);
            this.binaryType = binaryType;
        }

        @Override
        public void apply(MethodModelRuleApplicationContext context, MutableModelNode target) {
            MethodRuleDefinition<?, ?> ruleDefinition = getRuleDefinition();
            final BinaryTaskRule<T> binaryTaskRule = new BinaryTaskRule<T>(binaryType, ruleDefinition);
            final ModelAction binaryTaskAction = context.contextualize(binaryTaskRule);
            context.getRegistry().configure(ModelActionRole.Defaults, DirectNodeNoInputsModelAction.of(
                    BINARIES_CONTAINER,
                    ruleDefinition.getDescriptor(),
                    new Action<MutableModelNode>() {
                        @Override
                        public void execute(MutableModelNode modelNode) {
                            modelNode.applyTo(allLinks(), ModelActionRole.Finalize, binaryTaskAction);
                        }
                    }
            ));
        }

        @Override
        public List<? extends Class<?>> getRuleDependencies() {
            return ImmutableList.of(BinaryBasePlugin.class);
        }
    }
}
