/*
 * Copyright 2016 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.nativeplatform.internal;

import org.apache.commons.lang.StringUtils;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.Task;
import org.gradle.api.file.FileCollection;
import org.gradle.api.internal.resolve.ProjectModelResolver;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.gradle.model.ModelMap;
import org.gradle.model.internal.registry.ModelRegistry;
import org.gradle.model.internal.type.ModelTypes;
import org.gradle.nativeplatform.NativeBinarySpec;
import org.gradle.nativeplatform.NativeComponentSpec;
import org.gradle.nativeplatform.NativeDependencySet;
import org.gradle.nativeplatform.NativeExecutableFileSpec;
import org.gradle.nativeplatform.NativeInstallationSpec;
import org.gradle.nativeplatform.tasks.InstallExecutable;
import org.gradle.nativeplatform.tasks.LinkExecutable;
import org.gradle.platform.base.BinaryContainer;
import org.gradle.platform.base.ComponentSpec;
import org.gradle.platform.base.ComponentSpecContainer;
import org.gradle.platform.base.VariantComponentSpec;
import org.gradle.platform.base.internal.BinaryNamingScheme;
import org.gradle.platform.base.internal.BinarySpecInternal;
import org.gradle.platform.base.internal.dependents.DependentBinariesResolvedResult;
import org.gradle.platform.base.internal.dependents.DependentBinariesResolver;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;

public class NativeComponents {

    private static final String ASSEMBLE_DEPENDENTS_TASK_NAME = "assembleDependents";
    private static final String BUILD_DEPENDENTS_TASK_NAME = "buildDependents";

    public static void createExecutableTask(final NativeBinarySpecInternal binary, final File executableFile) {
        String taskName = binary.getNamingScheme().getTaskName("link");
        binary.getTasks().create(taskName, LinkExecutable.class, new Action<LinkExecutable>() {
            @Override
            public void execute(LinkExecutable linkTask) {
                linkTask.setDescription("Links " + binary.getDisplayName());
                linkTask.getToolChain().set(binary.getToolChain());
                linkTask.getTargetPlatform().set(binary.getTargetPlatform());
                linkTask.getLinkedFile().set(executableFile);
                linkTask.getLinkerArgs().set(binary.getLinker().getArgs());

                linkTask.lib(new BinaryLibs(binary) {
                    @Override
                    protected FileCollection getFiles(NativeDependencySet nativeDependencySet) {
                        return nativeDependencySet.getLinkFiles();
                    }
                });
                binary.builtBy(linkTask);
            }
        });
    }

    public static void createInstallTask(final NativeBinarySpecInternal binary, final NativeInstallationSpec installation, final NativeExecutableFileSpec executable, final BinaryNamingScheme namingScheme) {
        binary.getTasks().create(namingScheme.getTaskName("install"), InstallExecutable.class, new Action<InstallExecutable>() {
            @Override
            public void execute(InstallExecutable installTask) {
                installTask.setDescription("Installs a development image of " + binary.getDisplayName());
                installTask.setGroup(LifecycleBasePlugin.BUILD_GROUP);
                installTask.getToolChain().set(executable.getToolChain());
                installTask.getTargetPlatform().set(binary.getTargetPlatform());
                installTask.getExecutableFile().set(executable.getFile());
                installTask.getInstallDirectory().set(installation.getDirectory());
                //TODO:HH wire binary libs via executable
                installTask.lib(new BinaryLibs(binary) {
                    @Override
                    protected FileCollection getFiles(NativeDependencySet nativeDependencySet) {
                        return nativeDependencySet.getRuntimeFiles();
                    }
                });

                //TODO:HH installTask.dependsOn(executable)
                installTask.dependsOn(binary);
            }
        });
    }

    public static void createBuildDependentComponentsTasks(ModelMap<Task> tasks, ComponentSpecContainer components) {
        for (final VariantComponentSpec component : components.withType(NativeComponentSpec.class).withType(VariantComponentSpec.class)) {
            tasks.create(getAssembleDependentComponentsTaskName(component), DefaultTask.class, new Action<DefaultTask>() {
                @Override
                public void execute(DefaultTask assembleDependents) {
                    assembleDependents.setGroup("Build Dependents");
                    assembleDependents.setDescription("Assemble dependents of " + component.getDisplayName() + ".");
                }
            });
            tasks.create(getBuildDependentComponentsTaskName(component), DefaultTask.class, new Action<DefaultTask>() {
                @Override
                public void execute(DefaultTask buildDependents) {
                    buildDependents.setGroup("Build Dependents");
                    buildDependents.setDescription("Build dependents of " + component.getDisplayName() + ".");
                }
            });
        }
    }

    public static void createBuildDependentBinariesTasks(final NativeBinarySpecInternal binary, BinaryNamingScheme namingScheme) {
        binary.getTasks().create(namingScheme.getTaskName(ASSEMBLE_DEPENDENTS_TASK_NAME), DefaultTask.class, new Action<DefaultTask>() {
            @Override
            public void execute(DefaultTask buildDependentsTask) {
                buildDependentsTask.setGroup("Build Dependents");
                buildDependentsTask.setDescription("Assemble dependents of " + binary.getDisplayName() + ".");
                buildDependentsTask.dependsOn(binary);
            }
        });
        binary.getTasks().create(namingScheme.getTaskName(BUILD_DEPENDENTS_TASK_NAME), DefaultTask.class, new Action<DefaultTask>() {
            @Override
            public void execute(DefaultTask buildDependentsTask) {
                buildDependentsTask.setGroup("Build Dependents");
                buildDependentsTask.setDescription("Build dependents of " + binary.getDisplayName() + ".");
                buildDependentsTask.dependsOn(binary);
            }
        });
    }

    public static void wireBuildDependentTasks(final ModelMap<Task> tasks, BinaryContainer binaries, final DependentBinariesResolver dependentsResolver, final ProjectModelResolver projectModelResolver) {
        final ModelMap<NativeBinarySpecInternal> nativeBinaries = binaries.withType(NativeBinarySpecInternal.class);
        for (final BinarySpecInternal binary : nativeBinaries) {
            Task assembleDependents = tasks.get(binary.getNamingScheme().getTaskName(ASSEMBLE_DEPENDENTS_TASK_NAME));
            Task buildDependents = tasks.get(binary.getNamingScheme().getTaskName(BUILD_DEPENDENTS_TASK_NAME));
            // Wire build dependent components tasks dependencies
            Task assembleDependentComponents = tasks.get(getAssembleDependentComponentsTaskName(binary.getComponent()));
            if (assembleDependentComponents != null) {
                assembleDependentComponents.dependsOn(assembleDependents);
            }
            Task buildDependentComponents = tasks.get(getBuildDependentComponentsTaskName(binary.getComponent()));
            if (buildDependentComponents != null) {
                buildDependentComponents.dependsOn(buildDependents);
            }
            // Wire build dependent binaries tasks dependencies
            // Defer dependencies gathering as we need to resolve across project's boundaries
            assembleDependents.dependsOn(new Callable<Iterable<Task>>() {
                @Override
                public Iterable<Task> call() {
                    return getDependentTaskDependencies(ASSEMBLE_DEPENDENTS_TASK_NAME, binary, dependentsResolver, projectModelResolver);
                }
            });
            buildDependents.dependsOn(new Callable<Iterable<Task>>() {
                @Override
                public Iterable<Task> call() {
                    return getDependentTaskDependencies(BUILD_DEPENDENTS_TASK_NAME, binary, dependentsResolver, projectModelResolver);
                }
            });
        }
    }

    private static List<Task> getDependentTaskDependencies(String dependedOnBinaryTaskName, BinarySpecInternal binary, DependentBinariesResolver dependentsResolver, ProjectModelResolver projectModelResolver) {
        List<Task> dependencies = new ArrayList<>();
        DependentBinariesResolvedResult result = dependentsResolver.resolve(binary).getRoot();
        for (DependentBinariesResolvedResult dependent : result.getChildren()) {
            if (dependent.isBuildable()) {
                ModelRegistry modelRegistry = projectModelResolver.resolveProjectModel(dependent.getId().getProjectPath());
                ModelMap<NativeBinarySpecInternal> projectBinaries = modelRegistry.realize("binaries", ModelTypes.modelMap(NativeBinarySpecInternal.class));
                ModelMap<Task> projectTasks = modelRegistry.realize("tasks", ModelTypes.modelMap(Task.class));
                NativeBinarySpecInternal dependentBinary = projectBinaries.get(dependent.getProjectScopedName());
                dependencies.add(projectTasks.get(dependentBinary.getNamingScheme().getTaskName(dependedOnBinaryTaskName)));
            }
        }
        return dependencies;
    }

    private static String getAssembleDependentComponentsTaskName(ComponentSpec component) {
        return ASSEMBLE_DEPENDENTS_TASK_NAME + StringUtils.capitalize(component.getName());
    }

    private static String getBuildDependentComponentsTaskName(ComponentSpec component) {
        return BUILD_DEPENDENTS_TASK_NAME + StringUtils.capitalize(component.getName());
    }

    public abstract static class BinaryLibs implements Callable<List<FileCollection>> {
        private final NativeBinarySpec binary;

        public BinaryLibs(NativeBinarySpec binary) {
            this.binary = binary;
        }

        @Override
        public List<FileCollection> call() throws Exception {
            List<FileCollection> runtimeFiles = new ArrayList<>();
            for (NativeDependencySet nativeDependencySet : binary.getLibs()) {
                runtimeFiles.add(getFiles(nativeDependencySet));
            }
            return runtimeFiles;
        }

        protected abstract FileCollection getFiles(NativeDependencySet nativeDependencySet);
    }
}
