/*
 * Copyright 2010 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.api.internal.changedetection.state;

import com.google.common.collect.ImmutableMap;
import org.gradle.api.internal.TaskExecutionHistory;
import org.gradle.internal.nativeintegration.filesystem.FileType;

import java.util.HashMap;
import java.util.Map;

/**
 * Takes a snapshot of the output files of a task.
 */
public class OutputFilesSnapshotter {
    public TaskExecutionHistory.OverlappingOutputs detectOverlappingOutputs(final String propertyName, final FileCollectionSnapshot previousExecution, FileCollectionSnapshot beforeExecution) {
        Map<String, NormalizedFileSnapshot> previousSnapshots = previousExecution.getSnapshots();
        Map<String, NormalizedFileSnapshot> beforeSnapshots = beforeExecution.getSnapshots();

        for (Map.Entry<String, NormalizedFileSnapshot> beforeSnapshot : beforeSnapshots.entrySet()) {
            final String path = beforeSnapshot.getKey();
            NormalizedFileSnapshot fileSnapshot = beforeSnapshot.getValue();
            NormalizedFileSnapshot previousSnapshot = previousSnapshots.get(path);
            // Missing files or just directories can be ignored
            // It would be nice to consider directories too, but we can't distinguish between an existing _root_ directory of an output property
            // and a directory inside the root directory.
            if (fileSnapshot.getSnapshot().getType() == FileType.RegularFile) {
                if (createdSincePreviousExecution(previousSnapshot) || changedSincePreviousExecution(fileSnapshot, previousSnapshot)) {
                    return new TaskExecutionHistory.OverlappingOutputs(propertyName, fileSnapshot.getNormalizedPath());
                }
            }
        }
        return null;
    }

    private boolean changedSincePreviousExecution(NormalizedFileSnapshot fileSnapshot, NormalizedFileSnapshot previousSnapshot) {
        // _changed_ since last execution, possibly by another task
        return !previousSnapshot.getSnapshot().isContentUpToDate(fileSnapshot.getSnapshot());
    }

    private boolean createdSincePreviousExecution(NormalizedFileSnapshot previousSnapshot) {
        // created since last execution, possibly by another task
        return previousSnapshot == null;
    }

    /**
     * Returns a new snapshot that filters out entries that should not be considered outputs of the task.
     */
    public FileCollectionSnapshot createOutputSnapshot(
        FileCollectionSnapshot afterPreviousExecution,
        FileCollectionSnapshot beforeExecution,
        FileCollectionSnapshot afterExecution
    ) {
        FileCollectionSnapshot filesSnapshot;
        Map<String, NormalizedFileSnapshot> afterSnapshots = afterExecution.getSnapshots();
        if (!beforeExecution.getSnapshots().isEmpty() && !afterSnapshots.isEmpty()) {
            Map<String, NormalizedFileSnapshot> beforeSnapshots = beforeExecution.getSnapshots();
            Map<String, NormalizedFileSnapshot> afterPreviousSnapshots = afterPreviousExecution != null ? afterPreviousExecution.getSnapshots() : new HashMap<String, NormalizedFileSnapshot>();
            int newEntryCount = 0;
            ImmutableMap.Builder<String, NormalizedFileSnapshot> outputEntries = ImmutableMap.builder();

            for (Map.Entry<String, NormalizedFileSnapshot> entry : afterSnapshots.entrySet()) {
                final String path = entry.getKey();
                NormalizedFileSnapshot fileSnapshot = entry.getValue();
                if (isOutputEntry(path, fileSnapshot, beforeSnapshots, afterPreviousSnapshots)) {
                    outputEntries.put(entry.getKey(), fileSnapshot);
                    newEntryCount++;
                }
            }
            // Are all files snapshot after execution accounted for as new entries?
            if (newEntryCount == afterSnapshots.size()) {
                filesSnapshot = afterExecution;
            } else {
                filesSnapshot = new DefaultFileCollectionSnapshot(outputEntries.build(), TaskFilePropertyCompareStrategy.OUTPUT, true);
            }
        } else {
            filesSnapshot = afterExecution;
        }
        return filesSnapshot;
    }

    /**
     * Decide whether an entry should be considered to be part of the output. Entries that are considered outputs are:
     * <ul>
     *     <li>an entry that did not exist before the execution, but exists after the execution</li>
     *     <li>an entry that did exist before the execution, and has been changed during the execution</li>
     *     <li>an entry that did wasn't changed during the execution, but was already considered an output during the previous execution</li>
     * </ul>
     */
    private static boolean isOutputEntry(String path, NormalizedFileSnapshot fileSnapshot, Map<String, NormalizedFileSnapshot> beforeSnapshots, Map<String, NormalizedFileSnapshot> afterPreviousSnapshots) {
        NormalizedFileSnapshot beforeSnapshot = beforeSnapshots.get(path);
        // Was it created during execution?
        if (beforeSnapshot == null) {
            return true;
        }
        // Was it updated during execution?
        if (!fileSnapshot.getSnapshot().isContentAndMetadataUpToDate(beforeSnapshot.getSnapshot())) {
            return true;
        }
        // Did we already consider it as an output after the previous execution?
        if (afterPreviousSnapshots.containsKey(path)) {
            return true;
        }
        return false;
    }
}
