/*
 * Copyright 2009 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.changedetection.state;

import org.gradle.api.changedetection.digest.*;
import org.gradle.api.changedetection.ChangeProcessor;
import org.gradle.api.GradleException;
import org.gradle.api.io.IoFactory;
import org.gradle.util.ThreadUtils;
import org.gradle.util.Clock;
import org.gradle.util.queues.BlockingQueueItemProducer;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.*;
import java.util.concurrent.*;

/**
 * @author Tom Eyckmans
 */
class DefaultDirectoryStateChangeDetecter implements DirectoryStateChangeDetecter {

    private final File directoryToProcess;
    private final IoFactory ioFactory;
    private final DirectoryStateBuilder directoryStateBuilder;
    private final DigesterCache digesterCache;
    private final DigesterUtil digesterUtil;
    private final DirectoryListFileCreator directoryListFileCreator;
    private final StateFileUtil stateFileUtil;
    private final BlockingQueue<StateChangeEvent> stateChangeEventQueue;
    private final BlockingQueueItemProducer<StateChangeEvent> changeProcessorEventProducer;
    private final List<DirectoryStateDigestComparator> directoryStateDigestComparators;
    private ExecutorService threadPool;
    private final StateFileChangeListenerUtil stateFileChangeListenerUtil;
    private final StateComparator stateComparator;

    DefaultDirectoryStateChangeDetecter(
            final File directoryToProcess, IoFactory ioFactory, DirectoryStateBuilder directoryStateBuilder, DigesterCache digesterCache, DigesterUtil digesterUtil, DirectoryListFileCreator directoryListFileCreator, StateFileUtil stateFileUtil, BlockingQueue<StateChangeEvent> stateChangeEventQueue, BlockingQueueItemProducer<StateChangeEvent> changeProcessorEventProducer, List<DirectoryStateDigestComparator> directoryStateDigestComparators, StateFileChangeListenerUtil stateFileChangeListenerUtil, StateComparator stateComparator) {
        if ( directoryToProcess == null ) throw new IllegalArgumentException("directoryToProcess is null!");
        if ( !directoryToProcess.exists() ) throw new IllegalArgumentException("directoryToProcess does not exists!");
        if ( !directoryToProcess.isDirectory() ) throw new IllegalArgumentException("directoryToProcess is not a directory!");

        this.directoryToProcess = directoryToProcess;
        this.ioFactory = ioFactory;
        this.directoryStateBuilder = directoryStateBuilder;
        this.digesterCache = digesterCache;
        this.digesterUtil = digesterUtil;
        this.directoryListFileCreator = directoryListFileCreator;
        this.stateFileUtil = stateFileUtil;
        this.stateChangeEventQueue = stateChangeEventQueue;
        this.changeProcessorEventProducer = changeProcessorEventProducer;
        this.directoryStateDigestComparators = directoryStateDigestComparators;
        this.stateFileChangeListenerUtil = stateFileChangeListenerUtil;
        this.stateComparator = stateComparator;
    }

    public File getDirectoryToProcess() {
        return directoryToProcess;
    }

    public void detectChanges(ChangeProcessor changeProcessor) {
        Clock c = new Clock();
        try {
            int lowestLevel = 0;
            try {
                lowestLevel = directoryListFileCreator.createDirectoryListFiles(directoryToProcess);
            }
            catch ( IOException e ) {
                throw new GradleException("failed to create directory list files", e);
            }

            // Calculate the digests of files and directories
            Map<String, DirectoryState> previousLevelDirectoryStates = Collections.unmodifiableMap(new HashMap<String, DirectoryState>());
            Map<String, DirectoryState> currentLevelDirectoryStates = new ConcurrentHashMap<String, DirectoryState>();
            for ( int levelIndex = lowestLevel ; levelIndex >= 0 ; levelIndex-- ) {
                final File directoryLevelListFile = stateFileUtil.getDirsListFile(levelIndex);
                threadPool = ThreadUtils.newFixedThreadPool(4);

                if ( directoryLevelListFile.exists() ) {
                    final File stateFile = stateFileUtil.getNewDirsStateFile(stateFileUtil.getDirsStateFilename(levelIndex));
                    final StateFileWriter newDirectoriesStateFileWriter = new StateFileWriter(ioFactory, stateFile);

                    BufferedReader directoryListFileReader = null;

                    try {
                        directoryListFileReader = new BufferedReader(new FileReader(directoryLevelListFile));

                        String absoluteDirectoryPath = null;
                        while ( (absoluteDirectoryPath = directoryListFileReader.readLine() ) != null ) {
                            final DirectoryState directoryState = directoryStateBuilder.
                                    directory(new File(absoluteDirectoryPath)).
                                    getDirectoryState();

                            final DirectoryStateDigestCalculator digestCalculator = new DirectoryStateDigestCalculator(directoryState, digesterCache, digesterUtil, this, currentLevelDirectoryStates, previousLevelDirectoryStates, ioFactory);

                            threadPool.submit(digestCalculator);
                        }

                        // each directory level has to be processed before continueing with the next(higher) level
                        ThreadUtils.shutdown(threadPool);

                        final List<DirectoryState> currentLevelDirectoryStatesList = new ArrayList<DirectoryState>(currentLevelDirectoryStates.values());

                        Collections.sort(currentLevelDirectoryStatesList);
                        // if one of the directory state processors failed -> change detection fails
                        // This is a simple not so efficient way, we could fail earlier but that will make everything more complicated
                        
                        for ( final DirectoryState directoryState : currentLevelDirectoryStatesList ) {
                            final Throwable failureCause = directoryState.getFailureCause();
                            if ( failureCause != null )
                                throw new GradleException("Failed to detect changes", failureCause);
                            else {
                                newDirectoriesStateFileWriter.addDigest(directoryState.getRelativePath(), directoryState.getDigest());
                            }
                        }

                    }
                    catch ( IOException e ) {
                        throw new GradleException("failed to detect changes (dirs."+newDirectoriesStateFileWriter.getStateFile().getAbsolutePath()+".state write failed)", e);
                    }
                    finally {
                        IOUtils.closeQuietly(directoryListFileReader);
                        FileUtils.deleteQuietly(directoryLevelListFile);
                        newDirectoriesStateFileWriter.close();
                    }
                }

                previousLevelDirectoryStates = Collections.unmodifiableMap(new HashMap<String, DirectoryState>(currentLevelDirectoryStates));
                currentLevelDirectoryStates = new ConcurrentHashMap<String, DirectoryState>();
            }

            // Compare new and old directory state + notify DirectoryStateChangeDetecterListener
            try {
                boolean keepComparing = true;
                int currentLevel = 0;

                final StateChangeEventDispatcher stateChangeEventDispatcher = new StateChangeEventDispatcher(stateChangeEventQueue, 100L, TimeUnit.MILLISECONDS, changeProcessor);
                final Thread changeProcessorEventThread = new Thread(stateChangeEventDispatcher);
                changeProcessorEventThread.start();
                
                threadPool = ThreadUtils.newFixedThreadPool(4);

                while ( keepComparing && currentLevel <= lowestLevel) {
                    keepComparing = stateComparator.compareState(this, currentLevel);

                    currentLevel++;
                }

                ThreadUtils.shutdown(threadPool);

                while ( !stateChangeEventQueue.isEmpty() ) {
                    Thread.yield();
                }
                stateChangeEventDispatcher.stopConsuming();

                ThreadUtils.join(changeProcessorEventThread);

                for (DirectoryStateDigestComparator directoryStateDigestComparator : directoryStateDigestComparators) {
                    final Throwable failureCause = directoryStateDigestComparator.getFailureCause();
                    if ( failureCause != null )
                        throw new GradleException("failed to compare directory state", failureCause);
                }
            }
            catch ( IOException e ) {
                throw new GradleException("failed to compare new and old state", e);
            }

            // Remove old directory state
            try {
                FileUtils.deleteDirectory(stateFileUtil.getOldDirectoryStateDir());
            }
            catch ( IOException e ) {
                throw new GradleException("failed to clean old state", e);
            }
            // Move new to old directory state
            try {
                FileUtils.moveDirectory(stateFileUtil.getNewDirectoryStateDir(), stateFileUtil.getOldDirectoryStateDir());
            }
            catch ( IOException e ) {
                throw new GradleException("failed to transfer current state to old state", e);
            }
        }
        finally {
            System.out.println(c.getTime());
        }
    }

    void submitDirectoryStateDigestComparator(final DirectoryStateDigestComparator directoryStateDigestComparator) {
        threadPool.submit(directoryStateDigestComparator);
        directoryStateDigestComparators.add(directoryStateDigestComparator);
    }

    BlockingQueueItemProducer<StateChangeEvent> getChangeProcessorEventProducer() {
        return changeProcessorEventProducer;
    }

    StateFileUtil getStateFileUtil() {
        return stateFileUtil;
    }

    public StateFileChangeListenerUtil getStateFileChangeListenerUtil() {
        return stateFileChangeListenerUtil;
    }
}
