/*
 * RHQ Management Platform
 * Copyright (C) 2005-2014 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

package org.rhq.core.pc.drift;

import static org.rhq.common.drift.FileEntry.addedFileEntry;
import static org.rhq.common.drift.FileEntry.changedFileEntry;
import static org.rhq.common.drift.FileEntry.removedFileEntry;
import static org.rhq.core.domain.drift.DriftChangeSetCategory.COVERAGE;
import static org.rhq.core.domain.drift.DriftChangeSetCategory.DRIFT;
import static org.rhq.core.util.file.FileUtil.copyFile;
import static org.rhq.core.util.file.FileUtil.forEachFile;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.rhq.common.drift.ChangeSetReader;
import org.rhq.common.drift.ChangeSetWriter;
import org.rhq.common.drift.FileEntry;
import org.rhq.common.drift.Headers;
import org.rhq.core.domain.drift.DriftChangeSetCategory;
import org.rhq.core.domain.drift.DriftDefinition;
import org.rhq.core.domain.drift.Filter;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.core.util.file.FileVisitor;

/**
 * Mechanism to detect and report Drift for active Drift Definitions.
 *
 * @author John Sanda
 */
public class DriftDetector implements Runnable {
    private static final Log log = LogFactory.getLog(DriftDetector.class);

    static final String FILE_CHANGESET_FULL = "changeset.txt";
    static final String FILE_CHANGESET_DELTA = "drift-changeset.txt";
    static final String FILE_SNAPSHOT_PINNED = "snapshot.pinned";

    private final ScheduleQueue scheduleQueue;

    private final ChangeSetManager changeSetMgr;

    private final MessageDigestGenerator digestGenerator = new MessageDigestGenerator(MessageDigestGenerator.SHA_256);

    private final DriftClient driftClient;

    public DriftDetector(ScheduleQueue scheduleQueue, ChangeSetManager changeSetMgr, DriftClient driftClient) {
        this.scheduleQueue = scheduleQueue;
        this.changeSetMgr = changeSetMgr;
        this.driftClient = driftClient;
    }

    @Override
    public void run() {
        log.debug("Starting drift detection...");
        long startTime = System.currentTimeMillis();
        DriftDetectionSchedule schedule;
        while((schedule = scheduleQueue.getNextSchedule()) != null) {
            boolean updateSchedule = true;
            try {
                if (log.isDebugEnabled()) {
                    log.debug("Fetching next schedule from " + scheduleQueue);
                    log.debug("Processing " + schedule);
                }
                if (schedule.getNextScan() > (System.currentTimeMillis() + 100L)) {
                    if (log.isDebugEnabled()) {
                        log.debug("Skipping " + schedule + " because it is too early to do the next detection.");
                    }
                    updateSchedule = false;
                    break; // We're done, rest of the queue is not scheduled yet
                }

                if (!schedule.getDriftDefinition().isEnabled()) {
                    if (log.isDebugEnabled()) {
                        log.debug("Skipping " + schedule + " because the drift definition is disabled.");
                    }
                    updateSchedule = false;
                    break; // We're done, rest of the queue is disabled only
                }

                if (previousSnapshotExists(schedule)) {
                    if (log.isDebugEnabled()) {
                        log.debug("Skipping " + schedule + " because server has not yet acked previous change set");
                    }
                    continue;
                }

                DriftDetectionSummary detectionSummary = new DriftDetectionSummary();
                detectionSummary.setSchedule(schedule);
                try {
                    if (changeSetMgr.changeSetExists(schedule.getResourceId(), createHeaders(schedule, COVERAGE, 0))) {
                        detectionSummary.setType(DRIFT);
                        generateDriftChangeSet(detectionSummary);
                    } else {
                        detectionSummary.setType(COVERAGE);
                        generateSnapshot(detectionSummary);
                    }

                    if (!detectionSummary.isBaseDirExists()) {
                        driftClient.reportMissingBaseDir(schedule.getResourceId(), schedule.getDriftDefinition());
                    } else if (detectionSummary.isRepeat()) {
                        driftClient.repeatChangeSet(schedule.getResourceId(), schedule.getDriftDefinition().getName(),
                                detectionSummary.getVersion());
                    } else if (changesNeedToBeReported(detectionSummary)) {
                        driftClient.sendChangeSetToServer(detectionSummary);
                    }
                } catch (IOException e) {
                    log.error("Drift detection failed: " + e.getMessage(), e);
                    revertSnapshot(detectionSummary);
                } catch (RuntimeException e) {
                    log.error("Drift detection failed: " + e.getMessage(), e);
                    revertSnapshot(detectionSummary);
                }
            } catch (Throwable t) {
                Throwable cause = t.getCause();
                String message = (null != cause) ? cause.getMessage() : t.getMessage();
                log.error("An unexpected error occurred during drift detection: " + message, t);

            } finally {
                try {
                    scheduleQueue.deactivateSchedule(updateSchedule);
                } catch (Throwable t) {
                    Throwable cause = t.getCause();
                    String message = (null != cause) ? cause.getMessage() : t.getMessage();
                    log.error("An unexpected error occurred while deactivating schedule: " + message, t);
                }
            }
        }
        long endTime = System.currentTimeMillis();
        log.debug("Finished drift detection in " + (endTime - startTime) + " ms");
    }

    private boolean changesNeedToBeReported(DriftDetectionSummary detectionSummary) {
        return detectionSummary.getType() == COVERAGE || detectionSummary.getDriftChangeSet() != null;
    }

    private boolean previousSnapshotExists(DriftDetectionSchedule schedule) {
        File snapshot = changeSetMgr.findChangeSet(schedule.getResourceId(), schedule.getDriftDefinition().getName(),
            COVERAGE);
        File previousSnapshot = new File(snapshot.getParentFile(), snapshot.getName() + ".previous");
        return previousSnapshot.exists();
    }

    private void generateDriftChangeSet(DriftDetectionSummary summary) throws IOException {
        final DriftDetectionSchedule schedule = summary.getSchedule();

        log.debug("Generating drift change set for " + schedule);

        boolean isPinned = schedule.getDriftDefinition().isPinned();
        final File basedir = new File(basedir(schedule.getResourceId(), schedule.getDriftDefinition()));

        File currentFullSnapshot = changeSetMgr.findChangeSet(schedule.getResourceId(), schedule.getDriftDefinition()
            .getName(), COVERAGE);

        // unless pinned use the current full snapshot file for the definition, otherwise use the pinned snapshot
        File snapshotFile = isPinned ? new File(currentFullSnapshot.getParentFile(), FILE_SNAPSHOT_PINNED)
            : currentFullSnapshot;

        // get a Set of all files in the detection, consider them initially new files, and we'll knock the
        // list down as we go.  As we build up FileEntries in memory this Set will shrink.  It's marginally
        // less memory than if we had both in memory at the same time.
        final Set<File> newFiles = new HashSet<File>(1000);

        // If the basedir is still valid we need to do a directory tree scan to look for newly added files
        if (basedir.isDirectory()) {
            DriftDefinition driftDef = schedule.getDriftDefinition();
            List<Filter> includes = driftDef.getIncludes();
            List<Filter> excludes = driftDef.getExcludes();

            for (File dir : getScanDirectories(basedir, includes)) {
                forEachFile(dir, new FilterFileVisitor(basedir, includes, excludes, new FileVisitor() {
                    @Override
                    public void visit(File file) {
                        if (file.isFile()) {
                            if (file.canRead()) {
                                newFiles.add(file);
                            } else if (log.isDebugEnabled()) {
                                log.debug("Skipping " + file.getPath() + " as new file since it is not readable.");
                            }
                        } else if (log.isDebugEnabled()) {
                            log.debug("Skipping " + file.getPath() + " as new file since it is not a normal file.");
                        }
                    }
                }));
            }
        }

        final List<FileEntry> unchangedEntries = new LinkedList<FileEntry>();
        final List<FileEntry> changedEntries = new LinkedList<FileEntry>();
        final List<FileEntry> removedEntries = new LinkedList<FileEntry>();
        final List<FileEntry> addedEntries = new LinkedList<FileEntry>();
        // for pinned snapshots, keep track of the original FileEntry objects for changed entries. These
        // are used if we re-write the pinned snapshot file.
        final List<FileEntry> changedPinnedEntries = isPinned ? new LinkedList<FileEntry>() : null;

        try {
            ChangeSetReader snapshotReader = null;
            int newVersion;
            boolean updateSnapshot = false;
            try {
                snapshotReader = changeSetMgr.getChangeSetReader(snapshotFile);

                if (!basedir.exists()) {
                    log.warn("The base directory [" + basedir.getAbsolutePath() + "] for " + schedule
                        + " does not exist.");
                }

                if (isPinned) {
                    // If pinned we compare against the pinned snapshot but we need to know the current snapshot version,
                    // get it from the current full snapshot.
                    ChangeSetReader currentFullSnapshotReader = null;
                    try {
                        currentFullSnapshotReader = changeSetMgr.getChangeSetReader(currentFullSnapshot);
                        newVersion = currentFullSnapshotReader.getHeaders().getVersion() + 1;
                    } finally {
                        if (null != currentFullSnapshotReader) {
                            currentFullSnapshotReader.close();
                        }
                    }
                } else {
                    newVersion = snapshotReader.getHeaders().getVersion() + 1;
                }

                // First look for files that have either been changed or removed
                updateSnapshot = scanSnapshot(schedule, basedir, snapshotReader, newFiles, unchangedEntries,
                    changedEntries, removedEntries, changedPinnedEntries);

            } finally {
                if (null != snapshotReader) {
                    snapshotReader.close();
                }
            }

            // if necessary, re-write the pinned snapshot file because we've updated timestamp/filesize info, which
            // on subsequent detection runs will help us avoid SHA generation. It must maintain the same entries.
            if (isPinned && updateSnapshot) {
                changedPinnedEntries.addAll(unchangedEntries);

                backupAndDeleteCurrentSnapshot(snapshotFile);
                updatePinnedSnapshot(schedule, snapshotFile, changedPinnedEntries);
            }

            // add new files to the snapshotEntries and deltaEntries
            for (File file : newFiles) {
                try {
                    if (log.isInfoEnabled()) {
                        log.info("Detected added file for " + schedule + " --> " + file.getAbsolutePath());
                    }

                    FileEntry addedFileEntry = getAddedFileEntry(basedir, file);
                    if (null != addedFileEntry) {
                        addedEntries.add(addedFileEntry);
                    }

                } catch (Throwable t) {
                    // report the error but keep going, perhaps it is specific to a single file, try to
                    // finish the change set generation.
                    log.error(
                        "An unexpected error occurred while generating a drift change set for file " + file.getPath()
                            + " in schedule " + schedule + ". Skipping file.", t);
                }
            }

            // The new snapshot contains all changed, unchanged and added files. Not removed files.
            final List<FileEntry> snapshotEntries = new LinkedList<FileEntry>(unchangedEntries);
            snapshotEntries.addAll(changedEntries);
            snapshotEntries.addAll(addedEntries);

            // The snapshot delta contains all changed, added and removed files.
            final List<FileEntry> deltaEntries = new LinkedList<FileEntry>(changedEntries);
            deltaEntries.addAll(removedEntries);
            deltaEntries.addAll(addedEntries);

            if (deltaEntries.isEmpty()) {
                File newSnapshot = currentFullSnapshot;

                if (!isPinned) {
                    // If unpinned and there is no detected drift then we generally don't need to add/update any files.
                    // But, if we have timestamp/filesize updates then we want to replace the current snapshot with
                    // the updated entries, so we can avoid SHA generation on subsequent runs.
                    if (updateSnapshot) {
                        currentFullSnapshot.delete();
                        newSnapshot = updateCurrentSnapshot(schedule, snapshotEntries, newVersion - 1);
                    }
                } else {
                    // If pinned and returning to compliance (meaning no drift now but the previous snapshot did have drift)
                    // then we need to reset the current snapshot to match the pinned snapshot. Note though that we
                    // increment the snapshot version in order to let the server know about the state change.
                    if (newVersion > 1
                        && !isPreviousChangeSetEmpty(schedule.getResourceId(), schedule.getDriftDefinition())) {
                        currentFullSnapshot.delete();
                        newSnapshot = updateCurrentSnapshot(schedule, snapshotEntries, newVersion);

                        updateDeltaSnapshot(summary, schedule, deltaEntries, newVersion, currentFullSnapshot,
                            newSnapshot);
                    }
                }

                summary.setNewSnapshot(newSnapshot);

            } else {
                // if there is drift, but we're pinned and the drift is the same as the previous detection, just
                // mark it as a repeat to indicate that we're out of compliance but not in any new way.
                if (isPinned && newVersion > 1 && isSameAsPreviousChangeSet(deltaEntries, currentFullSnapshot)) {
                    summary.setVersion(newVersion - 1);
                    summary.setRepeat(true);

                    return;
                }

                // otherwise, generate a new current snapshot, and a snapshot delta reflecting the latest drift
                File oldSnapshot = backupAndDeleteCurrentSnapshot(currentFullSnapshot);
                File newSnapshot = updateCurrentSnapshot(schedule, snapshotEntries, newVersion);

                updateDeltaSnapshot(summary, schedule, deltaEntries, newVersion, oldSnapshot, newSnapshot);
            }
        } finally {
            // Help out the garbage collector by clearing all of our collections
            safeClear(newFiles, unchangedEntries, changedEntries, changedPinnedEntries);
        }
    }

    /**
     * File.canRead() is basically a security check and does not guarantee that the file contents can truly be read.
     * Certain files, like socket files on linux, can not be processed and it's not known until actually trying to
     * construct a FileInputStream, as is done when we actually try to generate the digest. These files will generate
     * a FileNotFoundException. This method will catch, log and suppress that issue, and return null
     * indicating the file is not suitable for drift detection.
     *
     * @param basedir the drift def base directory
     * @param file the new file to add
     * @return the new FileEntry, or null if this file is not appropriate for drift detection (typically if the
     * underlying file does not support the needed File operations.
     * @throws IOException if something unexpected occurs, outside of the FileNotFoundException it looks for.
     */
    private FileEntry getAddedFileEntry(File basedir, File file) throws IOException {
        FileEntry result = null;

        try {
            String sha256 = sha256(file);
            String relativePath = relativePath(basedir, file);
            long lastModified = file.lastModified();
            long length = file.length();

            result = addedFileEntry(relativePath, sha256, lastModified, length);

        } catch (FileNotFoundException e) {
            if (log.isDebugEnabled()) {
                log.debug("Skipping " + file.getPath() + " since it is missing or is not a physically readable file.");
            }
        }

        return result;
    }

    static private void safeClear(Collection<?>... collections) {
        if (null == collections) {
            return;
        }
        for (Collection<?> c : collections) {
            if (null != c) {
                c.clear();
            }
        }
    }

    private static Set<File> getScanDirectories(final File basedir, List<Filter> includes) {

        Set<File> directories = new HashSet<File>();

        if (null == includes || includes.isEmpty()) {
            directories.add(basedir);
        } else {
            for (Filter filter : includes) {
                String path = filter.getPath();
                if (".".equals(path)) {
                    directories.add(basedir);
                } else {
                    directories.add(new File(basedir, path));
                }
            }
        }

        return directories;
    }

    /**
     * Process the entries for the snapshotReader. Each entry will be placed in one of the various Lists depending
     * on what bucket it fall into.
     * @return true if unchangedEntries (meaning no drift) had timestamp/filesize info updated, in which case the
     * snapshot should be re-written to disk even if there was no drift.
     * @throws IOException
     */
    private boolean scanSnapshot(DriftDetectionSchedule schedule, File basedir, ChangeSetReader snapshotReader,
        Set<File> newFiles, List<FileEntry> unchangedEntries, List<FileEntry> changedEntries,
        List<FileEntry> removedEntries, List<FileEntry> changedPinnedEntries) throws IOException {

        boolean result = false;

        for (FileEntry entry : snapshotReader) {
            File file = new File(basedir, entry.getFile());
            newFiles.remove(file);

            if (!(file.exists() && file.canRead())) {
                // The file has been deleted or is no longer readable, since the last scan
                if (log.isDebugEnabled()) {
                    log.debug("Detected " + (file.exists() ? "unreadable" : "deleted") + " file for " + schedule
                        + " --> " + file.getAbsolutePath());
                }
                removedEntries.add(removedFileEntry(entry.getFile(), entry.getNewSHA()));

                if (null != changedPinnedEntries) {
                    changedPinnedEntries.add(entry);
                }
            } else {
                String currentSHA = null;
                boolean isChanged = false;

                // perform a SHA comparison if we are unable to compare size and lastModified or if the
                // size or lastModified test fails.  We may not have size or lastModified values for the
                // entry when the current snapshot was provided by the server, either due to a synch or
                // pinning scenario.  The server does not store that information and will provide -1 for defaults.
                if (entry.getLastModified() == -1 || entry.getSize() == -1
                    || entry.getLastModified() != file.lastModified() || entry.getSize() != file.length()) {

                    currentSHA = sha256(file);
                    isChanged = !entry.getNewSHA().equals(currentSHA);
                }

                if (isChanged) {
                    FileEntry changedEntry = changedFileEntry(entry.getFile(), entry.getNewSHA(), currentSHA,
                        file.lastModified(), file.length());
                    changedEntries.add(changedEntry);

                    if (null != changedPinnedEntries) {
                        changedPinnedEntries.add(entry);
                    }

                } else {
                    if (-1 == entry.getLastModified()) {
                        entry.setLastModified(file.lastModified());
                        result = true;
                    }
                    if (-1 == entry.getSize()) {
                        entry.setSize(file.length());
                        result = true;
                    }
                    unchangedEntries.add(entry);
                }
            }
        }

        return result;
    }

    private boolean isPreviousChangeSetEmpty(int resourceId, DriftDefinition definition) throws IOException {
        File changeSet = changeSetMgr.findChangeSet(resourceId, definition.getName(), DRIFT);
        if (!changeSet.exists()) {
            return true;
        }
        ChangeSetReader reader = null;
        boolean isEmpty;
        try {
            reader = changeSetMgr.getChangeSetReader(changeSet);
            isEmpty = true;

            for (FileEntry entry : reader) {
                isEmpty = false;
                break;
            }
        } finally {
            if (reader != null) {
                reader.close();
            }
        }

        return isEmpty;
    }

    private void updateDeltaSnapshot(DriftDetectionSummary summary, DriftDetectionSchedule schedule,
        List<FileEntry> deltaEntries, int newVersion, File oldSnapshot, File newSnapshot) throws IOException {

        ChangeSetWriter deltaWriter = null;

        try {
            Headers deltaHeaders = createHeaders(schedule, DRIFT, newVersion);
            File driftChangeSet = changeSetMgr.findChangeSet(schedule.getResourceId(), schedule.getDriftDefinition()
                .getName(), DRIFT);
            deltaWriter = changeSetMgr.getChangeSetWriter(driftChangeSet, deltaHeaders);

            summary.setDriftChangeSet(driftChangeSet);
            summary.setNewSnapshot(newSnapshot);
            summary.setOldSnapshot(oldSnapshot);

            for (FileEntry entry : deltaEntries) {
                deltaWriter.write(entry);
            }
        } finally {
            if (deltaWriter != null) {
                deltaWriter.close();
            }
        }
    }

    private File backupAndDeleteCurrentSnapshot(File currentSnapshot) throws IOException {
        File oldSnapshot = new File(currentSnapshot.getParentFile(), currentSnapshot.getName() + ".previous");
        copyFile(currentSnapshot, oldSnapshot);
        currentSnapshot.delete();
        return oldSnapshot;
    }

    private File updateCurrentSnapshot(DriftDetectionSchedule schedule, List<FileEntry> snapshotEntries, int newVersion)
        throws IOException {

        ChangeSetWriter newSnapshotWriter = null;

        try {
            Headers snapshotHeaders = createHeaders(schedule, COVERAGE, newVersion);
            File newSnapshot = changeSetMgr.findChangeSet(schedule.getResourceId(), schedule.getDriftDefinition()
                .getName(), COVERAGE);
            newSnapshotWriter = changeSetMgr.getChangeSetWriter(schedule.getResourceId(), snapshotHeaders);

            for (FileEntry entry : snapshotEntries) {
                newSnapshotWriter.write(entry);
            }
            return newSnapshot;
        } finally {
            if (null != newSnapshotWriter) {
                newSnapshotWriter.close();
            }
        }
    }

    private File updatePinnedSnapshot(DriftDetectionSchedule schedule, File pinnedSnapshot,
        List<FileEntry> snapshotEntries) throws IOException {

        ChangeSetWriter newSnapshotWriter = null;

        try {
            Headers snapshotHeaders = createHeaders(schedule, COVERAGE, 0);
            newSnapshotWriter = changeSetMgr.getChangeSetWriter(pinnedSnapshot, snapshotHeaders);

            for (FileEntry entry : snapshotEntries) {
                newSnapshotWriter.write(entry);
            }

            return pinnedSnapshot;

        } finally {
            if (null != newSnapshotWriter) {
                newSnapshotWriter.close();
            }
        }
    }

    private boolean isSameAsPreviousChangeSet(List<FileEntry> entries, File currentSnapsotFile) throws IOException {
        HashMap<String, FileEntry> entriesMap = new HashMap<String, FileEntry>();
        for (FileEntry e : entries) {
            entriesMap.put(e.getFile(), e);
        }

        ChangeSetReader reader = null;
        try {
            File deltaChangeSet = new File(currentSnapsotFile.getParentFile(), FILE_CHANGESET_DELTA);
            reader = changeSetMgr.getChangeSetReader(deltaChangeSet);

            int numEntries = 0;
            for (FileEntry entry : reader) {
                FileEntry newEntry = entriesMap.get(entry.getFile());
                if (newEntry == null) {
                    return false;
                }
                if (entry.getType() != newEntry.getType()) {
                    return false;
                }
                switch (entry.getType()) {
                case FILE_ADDED:
                    if (!entry.getNewSHA().equals(newEntry.getNewSHA())) {
                        return false;
                    }
                case FILE_CHANGED:
                    if (!entry.getNewSHA().equals(newEntry.getNewSHA())
                        || !entry.getOldSHA().equals(newEntry.getOldSHA())) {
                        return false;
                    }
                default: // FILE_REMOVED
                    if (!entry.getOldSHA().equals(newEntry.getOldSHA())) {
                        return false;
                    }
                }
                numEntries++;
            }

            return numEntries == entriesMap.size();
        } finally {
            if (reader != null) {
                reader.close();
            }
        }
    }

    private void generateSnapshot(DriftDetectionSummary summary) throws IOException {
        final DriftDetectionSchedule schedule = summary.getSchedule();
        final DriftDefinition driftDef = schedule.getDriftDefinition();
        final File basedir = new File(basedir(schedule.getResourceId(), driftDef));

        if (!basedir.exists()) {
            if (log.isWarnEnabled()) {
                log.warn("The base directory [" + basedir.getAbsolutePath() + "] for " + schedule + " does not "
                    + "exist. You may want review the drift definition and verify that the value of the base "
                    + "directory is in fact correct.");
            }
            summary.setBaseDirExists(false);
            return;
        }

        log.debug("Generating coverage change set for " + schedule);

        File snapshot = changeSetMgr.findChangeSet(schedule.getResourceId(), schedule.getDriftDefinition().getName(),
            COVERAGE);

        ChangeSetWriter writer = null;
        try {
            writer = changeSetMgr.getChangeSetWriter(snapshot, createHeaders(schedule, COVERAGE, 0));

            if (basedir.isDirectory()) {
                doDirectoryScan(schedule, driftDef, basedir, writer);
                writer.close();
                writer = null;
            }
            if (schedule.getDriftDefinition().isPinned()) {
                copyFile(snapshot, new File(snapshot.getParentFile(), FILE_SNAPSHOT_PINNED));
            }
            summary.setNewSnapshot(snapshot);
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }

    private void doDirectoryScan(final DriftDetectionSchedule schedule, DriftDefinition driftDef, final File basedir,
        final ChangeSetWriter writer) {

        List<Filter> includes = driftDef.getIncludes();
        List<Filter> excludes = driftDef.getExcludes();

        for (File dir : getScanDirectories(basedir, includes)) {
            forEachFile(dir, new FilterFileVisitor(basedir, includes, excludes, new FileVisitor() {
                @Override
                public void visit(File file) {
                    try {
                        if (!file.canRead()) {
                            if (log.isDebugEnabled()) {
                                log.debug("Skipping " + file.getPath() + " since we do not have read access.");
                            }
                            return;
                        }

                        if (log.isDebugEnabled()) {
                            log.debug("Adding " + file.getPath() + " to coverage change set for " + schedule);
                        }

                        FileEntry addedFileEntry = getAddedFileEntry(basedir, file);
                        if (null != addedFileEntry) {
                            writer.write(addedFileEntry);
                        }

                    } catch (Throwable t) {
                        // report the error but keep going, perhaps it is specific to a single file, try to
                        // finish the detection.
                        log.error("An unexpected error occurred while generating a coverage change set for file "
                            + file.getPath() + " in schedule " + schedule + ". Skipping file.", t);
                    }
                }
            }));
        }
    }

    private String relativePath(File basedir, File file) {
        if (basedir.equals(file)) {
            return ".";
        }
        String filePath = file.getAbsolutePath();
        String basedirPath = basedir.getAbsolutePath();
        int basedirLen = basedirPath.length();
        if (!basedirPath.endsWith(File.separator)) {
            ++basedirLen;
        }
        return filePath.substring(basedirLen);
    }

    private String sha256(File file) throws IOException {
        return digestGenerator.calcDigestString(file);
    }

    private String basedir(int resourceId, DriftDefinition driftDef) {
        return driftClient.getAbsoluteBaseDirectory(resourceId, driftDef).getAbsolutePath();
    }

    private Headers createHeaders(DriftDetectionSchedule schedule, DriftChangeSetCategory type, int version) {
        Headers headers = new Headers();
        headers.setResourceId(schedule.getResourceId());
        headers.setDriftDefinitionId(schedule.getDriftDefinition().getId());
        headers.setDriftDefinitionName(schedule.getDriftDefinition().getName());
        headers.setBasedir(basedir(schedule.getResourceId(), schedule.getDriftDefinition()));
        headers.setType(type);
        headers.setVersion(version);

        return headers;
    }

    private void revertSnapshot(DriftDetectionSummary summary) throws IOException {
        log.info("Reverting snapshot for " + summary.getSchedule());

        DriftDetectionSchedule scheudle = summary.getSchedule();
        File newSnapshot = changeSetMgr.findChangeSet(scheudle.getResourceId(),
            scheudle.getDriftDefinition().getName(), COVERAGE);

        // We want to delete the snapshot file regardless of whether the drift detection
        // was for an initial coverage change set or for a drift change set. We do not know
        // the state of the snapshot file so we have to delete it. If we have only generated
        // the initial coverage change set, then it will get regenerated.
        newSnapshot.delete();

        if (summary.getType() == DRIFT) {
            File oldSnapshotBackup = summary.getOldSnapshot();
            // If we generated a drift change set, we need to check for a back up of the
            // previous snapshot. We revert to the back up. If no back up is found, we log
            // an error.
            if (oldSnapshotBackup != null && oldSnapshotBackup.exists()) {
                copyFile(oldSnapshotBackup, newSnapshot);
                // We have to delete to the previous version snapshot file; otherwise,
                // subsequent detection runs will be skipped.
                oldSnapshotBackup.delete();
            } else {
                // TODO Should we throw an exception and/or disable detection?
                // If we fall into this else block, that means we were not able to revert
                // to the previous snapshot version, and we may be in an inconsistent state.
                log.error("Cannot revert snapshot to previous version for " + summary.getSchedule()
                    + ". Snapshot back up file not found.");
            }
        }
        deleteZipFiles(newSnapshot.getParentFile());
    }

    private void deleteZipFiles(File dir) {
        File[] files = dir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".zip");
            }
        });
        for (File file : files) {
            file.delete();
        }
    }

}
