/*
 * Copyright 2018 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.internal.snapshot;

import org.gradle.internal.file.FileMetadata.AccessType;
import org.gradle.internal.hash.HashCode;
import org.gradle.internal.hash.Hasher;
import org.gradle.internal.hash.Hashing;

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

import static org.gradle.internal.snapshot.MerkleDirectorySnapshotBuilder.EmptyDirectoryHandlingStrategy.EXCLUDE_EMPTY_DIRS;

public class MerkleDirectorySnapshotBuilder {
    private static final HashCode DIR_SIGNATURE = Hashing.signature("DIR");

    private final Deque<Level> stack = new ArrayDeque<>();
    private final boolean sortingRequired;

    public static MerkleDirectorySnapshotBuilder sortingRequired() {
        return new MerkleDirectorySnapshotBuilder(true);
    }

    public static MerkleDirectorySnapshotBuilder noSortingRequired() {
        return new MerkleDirectorySnapshotBuilder(false);
    }

    private MerkleDirectorySnapshotBuilder(boolean sortingRequired) {
        this.sortingRequired = sortingRequired;
        stack.addLast(new Result());
    }

    public void preVisitDirectory(CompleteDirectorySnapshot directorySnapshot, EmptyDirectoryHandlingStrategy emptyDirectoryHandlingStrategy) {
        preVisitDirectory(directorySnapshot.getAccessType(), directorySnapshot.getAbsolutePath(), directorySnapshot.getName(), emptyDirectoryHandlingStrategy);
    }

    public void preVisitDirectory(AccessType accessType, String absolutePath, String name, EmptyDirectoryHandlingStrategy emptyDirectoryHandlingStrategy) {
        stack.addLast(new Directory(accessType, absolutePath, name, emptyDirectoryHandlingStrategy));
    }

    public void visitLeafElement(FileSystemLeafSnapshot snapshot) {
        collectEntry(snapshot);
    }

    public boolean postVisitDirectory() {
        CompleteFileSystemLocationSnapshot snapshot = stack.removeLast().fold();
        if (snapshot == null) {
            return false;
        }
        collectEntry(snapshot);
        return true;
    }

    private void collectEntry(CompleteFileSystemLocationSnapshot snapshot) {
        Level level = stack.peekLast();
        if (level == null) {
            throw new IllegalStateException("Outside of root");
        }
        level.collectEntry(snapshot);
    }

    @Nullable
    public CompleteFileSystemLocationSnapshot getResult() {
        assert stack.size() == 1;
        return stack.getLast().fold();
    }

    public enum EmptyDirectoryHandlingStrategy {
        INCLUDE_EMPTY_DIRS,
        EXCLUDE_EMPTY_DIRS
    }

    private interface Level {
        void collectEntry(CompleteFileSystemLocationSnapshot snapshot);

        @Nullable
        CompleteFileSystemLocationSnapshot fold();
    }

    private static class Result implements Level {
        private CompleteFileSystemLocationSnapshot result;

        @Override
        public void collectEntry(CompleteFileSystemLocationSnapshot snapshot) {
            assert result == null;
            result = snapshot;
        }

        @Override
        public CompleteFileSystemLocationSnapshot fold() {
            return result;
        }
    }

    private class Directory implements Level {
        private final AccessType accessType;
        private final String absolutePath;
        private final String name;
        private final List<CompleteFileSystemLocationSnapshot> children;
        private final EmptyDirectoryHandlingStrategy emptyDirectoryHandlingStrategy;

        public Directory(AccessType accessType, String absolutePath, String name, EmptyDirectoryHandlingStrategy emptyDirectoryHandlingStrategy) {
            this.accessType = accessType;
            this.absolutePath = absolutePath;
            this.name = name;
            this.children = new ArrayList<>();
            this.emptyDirectoryHandlingStrategy = emptyDirectoryHandlingStrategy;
        }

        @Override
        public void collectEntry(CompleteFileSystemLocationSnapshot snapshot) {
            children.add(snapshot);
        }

        @Override
        public CompleteDirectorySnapshot fold() {
            if (emptyDirectoryHandlingStrategy == EXCLUDE_EMPTY_DIRS && children.isEmpty()) {
                return null;
            }
            if (sortingRequired) {
                children.sort(CompleteFileSystemLocationSnapshot.BY_NAME);
            }
            Hasher hasher = Hashing.newHasher();
            hasher.putHash(DIR_SIGNATURE);
            for (CompleteFileSystemLocationSnapshot child : children) {
                hasher.putString(child.getName());
                hasher.putHash(child.getHash());
            }
            return new CompleteDirectorySnapshot(absolutePath, name, accessType, hasher.hash(), children);
        }
    }
}

@@ -1246,16 +1246,29 @@
 que%3C
-Level%3E s
+Directory%3E directoryS
 tack
@@ -1332,16 +1332,71 @@
 equired;
+%0A    private CompleteFileSystemLocationSnapshot result;
 %0A%0A    pu
@@ -1780,45 +1780,8 @@
 ed;%0A
-        stack.addLast(new Result());%0A
     
@@ -2258,17 +2258,26 @@
         
-s
+directoryS
 tack.add
@@ -2571,17 +2571,26 @@
 pshot = 
-s
+directoryS
 tack.rem
@@ -2824,23 +2824,40 @@
     
-Level level = s
+Directory directory = directoryS
 tack
@@ -2885,15 +2885,19 @@
 if (
-level =
+directory !
 = nu
@@ -2918,57 +2918,39 @@
     
-throw new IllegalStateException(%22Outside of ro
+directory.collectEntry(snapsh
 ot
-%22
 );%0A 
@@ -2949,32 +2949,39 @@
 shot);%0A        %7D
+ else %7B
 %0A        level.c
@@ -2977,37 +2977,75 @@
     
-level.collectEntry(snapshot);
+    assert result == null;%0A            result = snapshot;%0A        %7D
 %0A   
@@ -3134,71 +3134,21 @@
     
-assert stack.size() == 1;%0A        return stack.getLast().fold()
+return result
 ;%0A  
@@ -3279,626 +3279,23 @@
 ate 
-interface Level %7B%0A        void collectEntry(CompleteFileSystemLocationSnapshot snapshot);%0A%0A        @Nullable%0A        CompleteFileSystemLocationSnapshot fold();%0A    %7D%0A%0A    private static class Result implements Level %7B%0A        private CompleteFileSystemLocationSnapshot result;%0A%0A        @Override%0A        public void collectEntry(CompleteFileSystemLocationSnapshot snapshot) %7B%0A            assert result == null;%0A            result = snapshot;%0A        %7D%0A%0A        @Override%0A        public CompleteFileSystemLocationSnapshot fold() %7B%0A            return result;%0A        %7D%0A    %7D%0A%0A    private class Directory implements Level
+class Directory
  %7B%0A 
@@ -3980,34 +3980,16 @@
      %7D%0A%0A
-        @Override%0A
         
@@ -4120,15 +4120,15 @@
    @
-Overrid
+Nullabl
 e%0A  

Simplify Merkle builder internals