/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * 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 com.android.build.gradle.internal.pipeline;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.api.transform.Context;
import com.android.build.api.transform.SecondaryFile;
import com.android.build.api.transform.SecondaryInput;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.gradle.internal.scope.TaskConfigAction;
import com.google.common.base.CaseFormat;
import com.google.wireless.android.sdk.stats.AndroidStudioStats;
import com.google.wireless.android.sdk.stats.AndroidStudioStats.GradleBuildProfileSpan.ExecutionType;
import com.android.builder.profile.Recorder;
import com.android.builder.profile.ThreadRecorder;
import com.android.ide.common.util.ReferenceHolder;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import org.gradle.api.logging.Logger;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputDirectories;
import org.gradle.api.tasks.OutputFiles;
import org.gradle.api.tasks.ParallelizableTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.incremental.IncrementalTaskInputs;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * A task running a transform.
 */
@ParallelizableTask
public class TransformTask extends StreamBasedTask implements Context {

    private Transform transform;
    Collection<SecondaryFile> secondaryFiles = null;

    public Transform getTransform() {
        return transform;
    }

    @InputFiles
    public Collection<File> getOtherFileInputs() {
        //noinspection deprecation: Needed for backward compatibility.
        return Stream.concat(
                        transform.getSecondaryFiles().stream().map(SecondaryFile::getFile),
                        transform.getSecondaryFileInputs().stream())
                .collect(Collectors.toList());
    }

    @OutputFiles
    public Collection<File> getOtherFileOutputs() {
        return transform.getSecondaryFileOutputs();
    }

    @OutputDirectories
    public Collection<File> getOtherFolderOutputs() {
        return transform.getSecondaryDirectoryOutputs();
    }

    @Input
    Map<String, Object> getOtherInputs() {
        return transform.getParameterInputs();
    }

    @TaskAction
    void transform(final IncrementalTaskInputs incrementalTaskInputs)
            throws IOException, TransformException, InterruptedException {

        final ReferenceHolder<List<TransformInput>> consumedInputs = ReferenceHolder.empty();
        final ReferenceHolder<List<TransformInput>> referencedInputs = ReferenceHolder.empty();
        final ReferenceHolder<Boolean> isIncremental = ReferenceHolder.empty();
        final ReferenceHolder<Collection<SecondaryInput>> changedSecondaryInputs =
                ReferenceHolder.empty();

        isIncremental.setValue(transform.isIncremental() && incrementalTaskInputs.isIncremental());

        AndroidStudioStats.GradleTransformExecution preExecutionInfo =
                AndroidStudioStats.GradleTransformExecution.newBuilder()
                        .setType(getTransformType(transform.getClass()))
                        .setIsIncremental(isIncremental.getValue())
                .build();

        ThreadRecorder.get().record(ExecutionType.TASK_TRANSFORM_PREPARATION, preExecutionInfo,
                getProject().getPath(), getVariantName(), new Recorder.Block<Void>() {
                    @Override
                    public Void call() throws Exception {

                        Map<File, Status> changedMap = Maps.newHashMap();
                        Set<File> removedFiles = Sets.newHashSet();
                        if (isIncremental.getValue()) {
                            // gather the changed files first.
                            gatherChangedFiles(getLogger(), incrementalTaskInputs, changedMap, removedFiles);

                            // and check against secondary files, which disables
                            // incremental mode.
                            isIncremental.setValue(checkSecondaryFiles(changedMap, removedFiles));
                        }

                        if (isIncremental.getValue()) {
                            // ok create temporary incremental data
                            List<IncrementalTransformInput> incInputs
                                    = createIncrementalInputs(consumedInputStreams);
                            List<IncrementalTransformInput> incReferencedInputs
                                    = createIncrementalInputs(referencedInputStreams);

                            // then compare to changed list and create final Inputs
                            if (isIncremental.setValue(updateIncrementalInputsWithChangedFiles(
                                    incInputs,
                                    incReferencedInputs,
                                    changedMap,
                                    removedFiles))) {
                                consumedInputs.setValue(convertToImmutable(incInputs));
                                referencedInputs.setValue(convertToImmutable(incReferencedInputs));
                            }
                        }

                        // at this point if we do not have incremental mode, got with
                        // default TransformInput with no inc data.
                        if (!isIncremental.getValue()) {
                            consumedInputs.setValue(
                                    computeNonIncTransformInput(consumedInputStreams));
                            referencedInputs.setValue(
                                    computeNonIncTransformInput(referencedInputStreams));
                            changedSecondaryInputs.setValue(
                                    ImmutableList.<SecondaryInput>of());
                        } else {
                            // gather all secondary input changes.
                            changedSecondaryInputs.setValue(
                                gatherSecondaryInputChanges(changedMap, removedFiles));
                        }

                        return null;
                    }
                });

        AndroidStudioStats.GradleTransformExecution executionInfo =
                preExecutionInfo.toBuilder().setIsIncremental(isIncremental.getValue()).build();

        ThreadRecorder.get().record(ExecutionType.TASK_TRANSFORM, executionInfo,
                getProject().getPath(), getVariantName(), new Recorder.Block<Void>() {
                    @Override
                    public Void call() throws Exception {

                        transform.transform(new TransformInvocationBuilder(TransformTask.this)
                                .addInputs(consumedInputs.getValue())
                                .addReferencedInputs(referencedInputs.getValue())
                                .addSecondaryInputs(changedSecondaryInputs.getValue())
                                .addOutputProvider(outputStream != null
                                        ? outputStream.asOutput()
                                        : null)
                                .setIncrementalMode(isIncremental.getValue())
                                .build());
                        return null;
                    }
                });
    }

    private static AndroidStudioStats.GradleTransformExecution.Type getTransformType(
            @NonNull Class<? extends Transform> taskClass) {
        String taskImpl = taskClass.getSimpleName();
        if (taskImpl.endsWith("Transform")) {
            taskImpl = taskImpl.substring(0, taskImpl.length() - "Transform".length());
        }
        String potentialExecutionTypeName =
                CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, taskImpl);

        try {
            return AndroidStudioStats.GradleTransformExecution.Type.valueOf(
                    potentialExecutionTypeName);
        } catch (IllegalArgumentException ignored) {
            return AndroidStudioStats.GradleTransformExecution.Type.UNKNOWN_TRANSFORM_TYPE;
        }
    }

    private Collection<SecondaryInput> gatherSecondaryInputChanges(
            Map<File, Status> changedMap, Set<File> removedFiles) {

        ImmutableList.Builder<SecondaryInput> builder = ImmutableList.builder();
        for (final SecondaryFile secondaryFile : getAllSecondaryInputs()) {
            final File file = secondaryFile.getFile();
            final Status status = changedMap.containsKey(file)
                    ? changedMap.get(file)
                    : removedFiles.contains(file)
                            ? Status.REMOVED
                            : Status.NOTCHANGED;

            builder.add(new SecondaryInput() {
                @Override
                public SecondaryFile getSecondaryInput() {
                    return secondaryFile;
                }

                @Override
                public Status getStatus() {
                    return status;
                }
            });
        }
        return builder.build();
    }

    /**
     * Returns a list of non incremental TransformInput.
     * @param streams the streams.
     * @return a list of non-incremental TransformInput matching the content of the streams.
     */
    @NonNull
    private static List<TransformInput> computeNonIncTransformInput(
            @NonNull Collection<TransformStream> streams) {
        return streams.stream()
                .map(TransformStream::asNonIncrementalInput)
                .collect(Collectors.toList());
    }

    /**
     * Returns a list of IncrementalTransformInput for all the inputs.
     */
    @NonNull
    private static List<IncrementalTransformInput> createIncrementalInputs(
            @NonNull Collection<TransformStream> streams) {
        return streams.stream()
                .map(TransformStream::asIncrementalInput)
                .collect(Collectors.toList());
    }

    private synchronized Collection<SecondaryFile> getAllSecondaryInputs() {
        if (secondaryFiles == null) {
            ImmutableList.Builder<SecondaryFile> builder = ImmutableList.builder();
            builder.addAll(transform.getSecondaryFiles());
            builder.addAll(
                    Iterables.transform(
                            transform.getSecondaryFileInputs(), SecondaryFile::nonIncremental));
            secondaryFiles = builder.build();
        }
        return secondaryFiles;
    }

    private static void gatherChangedFiles(
            @NonNull Logger logger,
            @NonNull IncrementalTaskInputs incrementalTaskInputs,
            @NonNull final Map<File, Status> changedFileMap,
            @NonNull final Set<File> removedFiles) {
        logger.info("Transform inputs calculations based on following changes");
        incrementalTaskInputs.outOfDate(inputFileDetails -> {
            logger.info(inputFileDetails.getFile().getAbsolutePath() + ":"
                    + IntermediateFolderUtils.inputFileDetailsToStatus(inputFileDetails));
            if (inputFileDetails.isAdded()) {
                changedFileMap.put(inputFileDetails.getFile(), Status.ADDED);
            } else if (inputFileDetails.isModified()) {
                changedFileMap.put(inputFileDetails.getFile(), Status.CHANGED);
            }
        });

        incrementalTaskInputs.removed(
                inputFileDetails -> {
                        logger.info(inputFileDetails.getFile().getAbsolutePath() + ":REMOVED");
                        removedFiles.add(inputFileDetails.getFile());
                });
    }

    private boolean checkSecondaryFiles(
            @NonNull Map<File, Status> changedMap,
            @NonNull Set<File> removedFiles) {

        for (SecondaryFile secondaryFile : getAllSecondaryInputs()) {
            File file = secondaryFile.getFile();
            if ((changedMap.containsKey(file) || removedFiles.contains(file))
                    && !secondaryFile.supportsIncrementalBuild()) {
                return false;
            }
        }
        return true;
    }

    private boolean isSecondaryFile(File file) {
        for (SecondaryFile secondaryFile : getAllSecondaryInputs()) {
            if (secondaryFile.getFile().equals(file)) {
                return true;
            }
        }
        return false;
    }

    private boolean updateIncrementalInputsWithChangedFiles(
            @NonNull List<IncrementalTransformInput> consumedInputs,
            @NonNull List<IncrementalTransformInput> referencedInputs,
            @NonNull Map<File, Status> changedFilesMap,
            @NonNull Set<File> removedFiles) {

        // we're going to concat both list multiple times, and the Iterators API ultimately put
        // all the iterators to concat in a list. So let's reuse a list.
        List<Iterator<IncrementalTransformInput>> iterators = Lists.newArrayListWithCapacity(2);

        Splitter splitter = Splitter.on(File.separatorChar);

        // start with the removed files as they carry the risk of removing incremental mode.
        // If we detect such a case, we stop immediately.
        for (File removedFile : removedFiles) {
            List<String> removedFileSegments = Lists.newArrayList(
                    splitter.split(removedFile.getAbsolutePath()));

            Iterator<IncrementalTransformInput> iterator = getConcatIterator(consumedInputs,
                    referencedInputs, iterators);

            boolean found = false;
            while (iterator.hasNext()) {
                IncrementalTransformInput next = iterator.next();
                if (next.checkRemovedJarFile(
                        Sets.union(transform.getScopes(), transform.getReferencedScopes()),
                        transform.getInputTypes(),
                        removedFile,
                        removedFileSegments)
                        || next.checkRemovedFolderFile(
                                Sets.union(transform.getScopes(), transform.getReferencedScopes()),
                                transform.getInputTypes(),
                                removedFile,
                                removedFileSegments)) {
                    found = true;
                    break;
                }
            }

            if (!found && !isSecondaryFile(removedFile)) {
                // this deleted file breaks incremental because we cannot figure out where it's
                // coming from and what types/scopes is associated with it.
                return false;
            }
        }

        // now handle the added/changed files.

        for (Map.Entry<File, Status> entry : changedFilesMap.entrySet()) {
            File changedFile = entry.getKey();
            Status changedStatus = entry.getValue();

            // first go through the jars first as it's a faster check.
            Iterator<IncrementalTransformInput> iterator = getConcatIterator(consumedInputs,
                    referencedInputs, iterators);
            boolean found = false;
            while (iterator.hasNext()) {
                if (iterator.next().checkForJar(changedFile, changedStatus)) {
                    // we can skip to the next changed file.
                    found = true;
                    break;
                }
            }

            if (found) {
                continue;
            }

            // now go through the folders. First get a segment list for the path.
            iterator = getConcatIterator(consumedInputs,
                    referencedInputs, iterators);
            List<String> changedSegments = Lists.newArrayList(
                    splitter.split(changedFile.getAbsolutePath()));

            while (iterator.hasNext()) {
                if (iterator.next().checkForFolder(changedFile, changedSegments, changedStatus)) {
                    // we can skip to the next changed file.
                    break;
                }
            }
        }

        return true;
    }

    @NonNull
    private static Iterator<IncrementalTransformInput> getConcatIterator(
            @NonNull List<IncrementalTransformInput> consumedInputs,
            @NonNull List<IncrementalTransformInput> referencedInputs,
            List<Iterator<IncrementalTransformInput>> iterators) {
        iterators.clear();
        iterators.add(consumedInputs.iterator());
        iterators.add(referencedInputs.iterator());
        return Iterators.concat(iterators.iterator());
    }

    @NonNull
    private static List<TransformInput> convertToImmutable(
            @NonNull List<IncrementalTransformInput> inputs) {
        return inputs.stream()
                .map(IncrementalTransformInput::asImmutable)
                .collect(Collectors.toList());
    }

    public  interface  ConfigActionCallback<T extends Transform> {
        void callback(@NonNull T transform, @NonNull TransformTask task);
    }

    public static class ConfigAction<T extends Transform> implements TaskConfigAction<TransformTask> {

        @NonNull
        private final String variantName;
        @NonNull
        private final String taskName;
        @NonNull
        private final T transform;
        @NonNull
        private Collection<TransformStream> consumedInputStreams;
        @NonNull
        private Collection<TransformStream> referencedInputStreams;
        @Nullable
        private IntermediateStream outputStream;
        @Nullable
        private final ConfigActionCallback<T> configActionCallback;

        ConfigAction(
                @NonNull String variantName,
                @NonNull String taskName,
                @NonNull T transform,
                @NonNull Collection<TransformStream> consumedInputStreams,
                @NonNull Collection<TransformStream> referencedInputStreams,
                @Nullable IntermediateStream outputStream,
                @Nullable ConfigActionCallback<T> configActionCallback) {
            this.variantName = variantName;
            this.taskName = taskName;
            this.transform = transform;
            this.consumedInputStreams = consumedInputStreams;
            this.referencedInputStreams = referencedInputStreams;
            this.outputStream = outputStream;
            this.configActionCallback = configActionCallback;
        }

        @NonNull
        @Override
        public String getName() {
            return taskName;
        }

        @NonNull
        @Override
        public Class<TransformTask> getType() {
            return TransformTask.class;
        }

        @Override
        public void execute(@NonNull TransformTask task) {
            task.transform = transform;
            task.consumedInputStreams = consumedInputStreams;
            task.referencedInputStreams = referencedInputStreams;
            task.outputStream = outputStream;
            task.setVariantName(variantName);

            if (configActionCallback != null) {
                configActionCallback.callback(transform, task);
            }
        }
    }
}
