/*
 * Copyright 2015 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.file.collections.jdk7;

import org.gradle.api.GradleException;
import org.gradle.api.file.FileTreeElement;
import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.file.FileVisitor;
import org.gradle.api.file.RelativePath;
import org.gradle.api.internal.cache.StringInterner;
import org.gradle.api.internal.file.FileVisitDetailsWithAttributes;
import org.gradle.api.internal.file.collections.DirectoryWalker;
import org.gradle.api.specs.Spec;
import org.gradle.internal.nativeintegration.filesystem.FileSystem;

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Deque;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicBoolean;

public class Jdk7DirectoryWalker implements DirectoryWalker {
    private final StringInterner relativePathStringInterner = new StringInterner();

    static boolean isAllowed(FileTreeElement element, Spec<FileTreeElement> spec) {
        return spec.isSatisfiedBy(element);
    }

    @Override
    public void walkDir(final File rootDir, final RelativePath rootPath, final FileVisitor visitor, final Spec<FileTreeElement> spec, final AtomicBoolean stopFlag, final FileSystem fileSystem, final boolean postfix) {
        final Deque<FileVisitDetails> directoryDetailsHolder = new LinkedList<FileVisitDetails>();

        try {
            Files.walkFileTree(rootDir.toPath(), EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new java.nio.file.FileVisitor<Path>() {
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    FileVisitDetails details = getFileVisitDetails(dir, attrs, true);
                    if (directoryDetailsHolder.size()==0 || isAllowed(details, spec)) {
                        directoryDetailsHolder.push(details);
                        if (directoryDetailsHolder.size() > 1 && !postfix) {
                            visitor.visitDir(details);
                        }
                        return checkStopFlag();
                    } else {
                        return FileVisitResult.SKIP_SUBTREE;
                    }
                }

                private FileVisitResult checkStopFlag() {
                    return stopFlag.get() ? FileVisitResult.TERMINATE : FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    if (attrs.isSymbolicLink()) {
                        // when FileVisitOption.FOLLOW_LINKS, we only get here when link couldn't be followed
                        throw new GradleException(String.format("Could not list contents of '%s'. Couldn't follow symbolic link.", file));
                    }
                    FileVisitDetails details = getFileVisitDetails(file, attrs, false);
                    if (isAllowed(details, spec)) {
                        visitor.visitFile(details);
                    }
                    return checkStopFlag();
                }

                private FileVisitDetails getFileVisitDetails(Path file, BasicFileAttributes attrs, boolean isDirectory) {
                    File child = file.toFile();
                    FileVisitDetails dirDetails = directoryDetailsHolder.peek();
                    RelativePath childPath = dirDetails != null ? dirDetails.getRelativePath().append(!isDirectory, relativePathStringInterner.intern(child.getName())) : rootPath;
                    return new FileVisitDetailsWithAttributes(child, childPath, stopFlag, fileSystem, fileSystem, isDirectory, attrs.lastModifiedTime().toMillis(), attrs.size());
                }

                @Override
                public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                    if (exc != null && !(exc instanceof FileSystemLoopException)) {
                        throw new GradleException(String.format("Could not read path '%s'.", file), exc);
                    }
                    return checkStopFlag();
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    if (exc != null) {
                        if (!(exc instanceof FileSystemLoopException)) {
                            throw new GradleException(String.format("Could not read directory path '%s'.", dir), exc);
                        }
                    } else {
                        if (postfix) {
                            FileVisitDetails details = directoryDetailsHolder.peek();
                            if (directoryDetailsHolder.size() > 1 && details != null) {
                                visitor.visitDir(details);
                            }
                        }
                    }
                    directoryDetailsHolder.pop();
                    return checkStopFlag();
                }
            });
        } catch (IOException e) {
            throw new GradleException(String.format("Could not list contents of directory '%s'.", rootDir), e);
        }
    }
}
