/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.patching.validation;

import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.jboss.as.patching.Constants;
import org.jboss.as.patching.logging.PatchLogger;
import org.jboss.as.patching.PatchingException;
import org.jboss.as.patching.installation.InstalledIdentity;
import org.jboss.as.patching.installation.Layer;
import org.jboss.as.patching.installation.PatchableTarget;

/**
 * Locate unreferenced directories and files and clean them up. This walks through the history checking for the entries
 * which are still able to get rolled back. Everything else is considered as garbage, except the current active state.
 *
 * @author Alexey Loubyansky
 */
public class PatchingGarbageLocator {

    private static final PatchLogger log = PatchLogger.ROOT_LOGGER;
    static final FilenameFilter ALL = new FilenameFilter() {
        @Override
        public boolean accept(File dir, String name) {
            return true;
        }
    };

    /**
     * Get the garbage locator.
     *
     * @param installedIdentity the installed identity
     * @return the garbage locator
     */
    public static PatchingGarbageLocator getIninitialized(InstalledIdentity installedIdentity) {
        return new PatchingGarbageLocator(installedIdentity);
    }

    private final InstalledIdentity installedIdentity;

    private Set<String> validHistory;
    private Set<File> referencedOverlayDirectories;

    protected PatchingGarbageLocator(final InstalledIdentity installedIdentity) {
        this.installedIdentity = installedIdentity;
    }

    private void walk() throws PatchingException {
        validHistory = new HashSet<String>();
        referencedOverlayDirectories = new HashSet<File>();
        // Get the active history
        final Set<String> activeHistory = new HashSet<String>();
        try {
            final PatchableTarget.TargetInfo info = installedIdentity.getIdentity().loadTargetInfo();
            activeHistory.addAll(info.getPatchIDs());
            activeHistory.add(info.getCumulativePatchID());
        } catch(IOException e) {
            throw new RuntimeException(e);
        }
        final PatchHistoryIterator.Builder builder = PatchHistoryIterator.Builder.create(installedIdentity);
        builder.addStateHandler(PatchingArtifacts.MODULE_OVERLAY, new PatchingArtifactStateHandler<PatchingFileArtifact.DirectoryArtifactState>() {
            @Override
            public void handleValidatedState(PatchingFileArtifact.DirectoryArtifactState state) {
                referencedOverlayDirectories.add(state.getFile());
            }
        });
        builder.addStateHandler(PatchingArtifacts.BUNDLE_OVERLAY, new PatchingArtifactStateHandler<PatchingFileArtifact.DirectoryArtifactState>() {
            @Override
            public void handleValidatedState(PatchingFileArtifact.DirectoryArtifactState state) {
                referencedOverlayDirectories.add(state.getFile());
            }
        });
        final PatchHistoryValidations.HistoryProcessor processor = new PatchHistoryValidations.HistoryProcessor() {
            boolean failed = false;

            @Override
            protected boolean includeCurrent() {
                return true;
            }

            @Override
            protected boolean canProceed() {
                return !activeHistory.isEmpty() || !failed;
            }

            @Override
            protected <P extends PatchingArtifact.ArtifactState, S extends PatchingArtifact.ArtifactState> boolean handleError(PatchingArtifact<P, S> artifact, S state) {
                // If (specific) parts of the history are is missing we can rollback to this patch, but no further
                failed = true;
                if (artifact == PatchingArtifacts.PATCH_XML
                        || artifact == PatchingArtifacts.ROLLBACK_XML
                        || artifact == PatchingArtifacts.MISC_BACKUP) {
                    return true;
                } else {
                    return false;
                }
            }

            @Override
            protected void processedPatch(String patch) {
                activeHistory.remove(patch);
                validHistory.add(patch);
            }
        };
        // Process
        processor.process(builder.iterator());
    }

    public void reset() {
        validHistory = null;
        referencedOverlayDirectories = null;
    }

    /**
     * Get the inactive history directories.
     *
     * @return the inactive history
     */
    public List<File> getInactiveHistory() throws PatchingException {
        if (validHistory == null) {
            walk();
        }
        final File[] inactiveDirs = installedIdentity.getInstalledImage().getPatchesDir().listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isDirectory() && !validHistory.contains(pathname.getName());
            }
        });
        return inactiveDirs == null ? Collections.<File>emptyList() : Arrays.asList(inactiveDirs);
    }

    /**
     * Get the inactive overlay directories.
     *
     * @return the inactive overlay directories
     */
    public List<File> getInactiveOverlays() throws PatchingException {
        if (referencedOverlayDirectories == null) {
            walk();
        }
        List<File> inactiveDirs = null;
        for (Layer layer : installedIdentity.getLayers()) {
            final File overlaysDir = new File(layer.getDirectoryStructure().getModuleRoot(), Constants.OVERLAYS);
            final File[] inactiveLayerDirs = overlaysDir.listFiles(new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    return pathname.isDirectory() && !referencedOverlayDirectories.contains(pathname);
                }
            });
            if (inactiveLayerDirs != null && inactiveLayerDirs.length > 0) {
                if (inactiveDirs == null) {
                    inactiveDirs = new ArrayList<File>();
                }
                inactiveDirs.addAll(Arrays.asList(inactiveLayerDirs));
            }
        }
        return inactiveDirs == null ? Collections.<File>emptyList() : inactiveDirs;
    }

    /**
     * Delete inactive contents.
     */
    public void deleteInactiveContent() throws PatchingException {
        List<File> dirs = getInactiveHistory();
        if (!dirs.isEmpty()) {
            for (File dir : dirs) {
                deleteDir(dir, ALL);
            }
        }
        dirs = getInactiveOverlays();
        if (!dirs.isEmpty()) {
            for (File dir : dirs) {
                deleteDir(dir, ALL);
            }
        }
    }

    protected static boolean deleteDir(File dir, FilenameFilter filter) {
        boolean success = true;
        final File[] files = dir.listFiles(filter);
        if (files != null) {
            for (int i = 0; i < files.length; i++) {
                File f = files[i];
                if (f.isDirectory()) {
                    // delete the directory and all of its contents.
                    if (!deleteDir(f, filter)) {
                        success = false;
                        log.debugf("Failed to delete dir: %s", f.getAbsolutePath());
                    }
                }
                // delete each file in the directory
                else if (!f.delete()) {
                    success = false;
                    log.debugf("Failed to delete file: %s", f.getAbsolutePath());
                }
            }
        }

        // finally delete the directory
        if (!dir.delete()) {
            success = false;
            log.debugf("Failed to delete dir: %s", dir.getAbsolutePath());
        }
        return success;
    }

}
