/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2013, 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.installation;

import static org.jboss.as.patching.Constants.ADD_ONS;
import static org.jboss.as.patching.Constants.LAYERS;

import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.jboss.as.patching.Constants;
import org.jboss.as.patching.DirectoryStructure;
import org.jboss.as.patching.logging.PatchLogger;
import org.jboss.as.patching.runner.PatchUtils;
import org.jboss.as.version.ProductConfig;

/**
 * @author Emanuel Muckenhuber
 */
class LayersFactory {

    /**
     * Load the available layers.
     *
     * @param image             the installed image
     * @param productConfig     the product config to establish the identity
     * @param moduleRoots       the module roots
     * @param bundleRoots       the bundle roots
     * @return the layers
     * @throws IOException
     */
    static InstalledIdentity load(final InstalledImage image, final ProductConfig productConfig, final List<File> moduleRoots, final List<File> bundleRoots) throws IOException {

        // build the identity information
        final String productVersion = productConfig.resolveVersion();
        final String productName = productConfig.resolveName();
        final Identity identity = new AbstractLazyIdentity() {
            @Override
            public String getName() {
                return productName;
            }

            @Override
            public String getVersion() {
                return productVersion;
            }

            @Override
            public InstalledImage getInstalledImage() {
                return image;
            }
        };

        final Properties properties = PatchUtils.loadProperties(identity.getDirectoryStructure().getInstallationInfo());
        final List<String> allPatches = PatchUtils.readRefs(properties, Constants.ALL_PATCHES);

        // Step 1 - gather the installed layers data
        final InstalledConfiguration conf = createInstalledConfig(image);
        // Step 2 - process the actual module and bundle roots
        final ProcessedLayers processedLayers = process(conf, moduleRoots, bundleRoots);
        final InstalledConfiguration config = processedLayers.getConf();

        // Step 3 - create the actual config objects
        // Process layers
        final InstalledIdentityImpl installedIdentity = new InstalledIdentityImpl(identity, allPatches, image);
        for (final LayerPathConfig layer : processedLayers.getLayers().values()) {
            final String name = layer.name;
            installedIdentity.putLayer(name, createPatchableTarget(name, layer, config.getLayerMetadataDir(name), image));
        }
        // Process add-ons
        for (final LayerPathConfig addOn : processedLayers.getAddOns().values()) {
            final String name = addOn.name;
            installedIdentity.putAddOn(name, createPatchableTarget(name, addOn, config.getAddOnMetadataDir(name), image));
        }
        return installedIdentity;
    }

    /**
     * Process the module and bundle roots and cross check with the installed information.
     *
     * @param conf        the installed configuration
     * @param moduleRoots the module roots
     * @param bundleRoots the bundle roots
     * @return the processed layers
     * @throws IOException
     */
    static ProcessedLayers process(final InstalledConfiguration conf, final List<File> moduleRoots, final List<File> bundleRoots) throws IOException {
        final ProcessedLayers layers = new ProcessedLayers(conf);
        // Process module roots
        final LayerPathSetter moduleSetter = new LayerPathSetter() {
            @Override
            public boolean setPath(final LayerPathConfig pending, final File root) {
                if (pending.modulePath == null) {
                    pending.modulePath = root;
                    return true;
                }
                return false;
            }
        };
        for (final File moduleRoot : moduleRoots) {
            processRoot(moduleRoot, layers, moduleSetter);
        }
        // Process bundle root
        final LayerPathSetter bundleSetter = new LayerPathSetter() {
            @Override
            public boolean setPath(LayerPathConfig pending, File root) {
                if (pending.bundlePath == null) {
                    pending.bundlePath = root;
                    return true;
                }
                return false;
            }
        };
        for (final File bundleRoot : bundleRoots) {
            processRoot(bundleRoot, layers, bundleSetter);
        }
//        if (conf.getInstalledLayers().size() != layers.getLayers().size()) {
//            throw processingError("processed layers don't match expected %s, but was %s", conf.getInstalledLayers(), layers.getLayers().keySet());
//        }
//        if (conf.getInstalledAddOns().size() != layers.getAddOns().size()) {
//            throw processingError("processed add-ons don't match expected %s, but was %s", conf.getInstalledAddOns(), layers.getAddOns().keySet());
//        }
        return layers;
    }

    /**
     * Process a module or bundle root.
     *
     * @param root   the root
     * @param layers the processed layers
     * @param setter the bundle or module path setter
     * @throws IOException
     */
    static void processRoot(final File root, final ProcessedLayers layers, final LayerPathSetter setter) throws IOException {
        final LayersConfig layersConfig = LayersConfig.getLayersConfig(root);
        // Process layers
        final File layersDir = new File(root, layersConfig.getLayersPath());
        if (!layersDir.exists()) {
            if (layersConfig.isConfigured()) {
                // Bad config from user
                throw PatchLogger.ROOT_LOGGER.installationNoLayersConfigFound(layersDir.getAbsolutePath());
            }
            // else this isn't a root that has layers and add-ons
        } else {
            // check for a valid layer configuration
            for (final String layer : layersConfig.getLayers()) {
                File layerDir = new File(layersDir, layer);
                if (!layerDir.exists()) {
                    if (layersConfig.isConfigured()) {
                        // Bad config from user
                        throw PatchLogger.ROOT_LOGGER.installationMissingLayer(layer, layersDir.getAbsolutePath());
                    }
                    // else this isn't a standard layers and add-ons structure
                    return;
                }
                layers.addLayer(layer, layerDir, setter);
            }
        }
        // Finally process the add-ons
        final File addOnsDir = new File(root, layersConfig.getAddOnsPath());
        final File[] addOnsList = addOnsDir.listFiles();
        if (addOnsList != null) {
            for (final File addOn : addOnsList) {
                layers.addAddOn(addOn.getName(), addOn, setter);
            }
        }
    }

    /**
     * Create the actual patchable target.
     *
     * @param name     the layer name
     * @param layer    the layer path config
     * @param metadata the metadata location for this target
     * @param image    the installed image
     * @return the patchable target
     * @throws IOException
     */
    static AbstractLazyPatchableTarget createPatchableTarget(final String name, final LayerPathConfig layer, final File metadata, final InstalledImage image) throws IOException {
        // patchable target
        return new AbstractLazyPatchableTarget() {

            @Override
            public InstalledImage getInstalledImage() {
                return image;
            }

            @Override
            public File getModuleRoot() {
                return layer.modulePath;
            }

            @Override
            public File getBundleRepositoryRoot() {
                return layer.bundlePath;
            }

            public File getPatchesMetadata() {
                return metadata;
            }

            @Override
            public String getName() {
                return name;
            }
        };
    }

    static class LayerPathConfig {

        File modulePath;
        File bundlePath;

        final String name;

        LayerPathConfig(String name) {
            this.name = name;
        }
    }

    interface LayerPathSetter {

        /**
         * Set the path for the layer.
         *
         * @param pending the pending layer
         * @param root    the root
         * @return {@code true} if the root wasn't set, {@code false} otherwise
         */
        boolean setPath(LayerPathConfig pending, File root);

    }

    /**
     * Resolve the installed layers and add-ons.
     *
     * @param image the installed image
     * @return the installed layers
     */
    static InstalledConfiguration createInstalledConfig(final InstalledImage image) {
        final InstalledConfiguration conf = new InstalledConfiguration(image);
        // Would be nice to have an installed inventory or smth like that
        return conf;
    }

    static class ProcessedLayers {

        private final InstalledConfiguration conf;
        private final Map<String, LayerPathConfig> layers = new LinkedHashMap<String, LayerPathConfig>();
        private final Map<String, LayerPathConfig> addOns = new LinkedHashMap<String, LayerPathConfig>();

        ProcessedLayers(InstalledConfiguration conf) {
            this.conf = conf;
        }

        InstalledConfiguration getConf() {
            return conf;
        }

        Map<String, LayerPathConfig> getLayers() {
            return layers;
        }

        Map<String, LayerPathConfig> getAddOns() {
            return addOns;
        }

        void addLayer(final String name, final File root, final LayerPathSetter setter) {
//            if (!conf.getInstalledLayers().contains(name)) {
//                throw processingError("layer '%s' not configured ", name);
//            }
            LayerPathConfig pending = layers.get(name);
            if (pending == null) {
                pending = new LayerPathConfig(name);
                layers.put(name, pending);
            }
            if (!setter.setPath(pending, root)) {
                // Already set means duplicate
                throw PatchLogger.ROOT_LOGGER.installationDuplicateLayer("layer", name);
            }
        }

        void addAddOn(final String name, final File root, LayerPathSetter setter) {
//            if (!conf.getInstalledAddOns().contains(name)) {
//                throw processingError("add-on '%s' not configured ", name);
//            }
            LayerPathConfig pending = addOns.get(name);
            if (pending == null) {
                pending = new LayerPathConfig(name);
                addOns.put(name, pending);
            }
            if (!setter.setPath(pending, root)) {
                // Already set means duplicate
                throw PatchLogger.ROOT_LOGGER.installationDuplicateLayer("add-on", name);
            }
        }

    }

    static class InstalledConfiguration {

        final File metadata;
        final InstalledImage installedImage;
        final Set<String> installedLayers = new HashSet<String>();
        final Set<String> installedAddOns = new HashSet<String>();

        InstalledConfiguration(final InstalledImage installedImage) {
            this.metadata = installedImage.getInstallationMetadata();
            this.installedImage = installedImage;
        }

        File getLayersMetadataDir() {
            return new File(metadata, LAYERS);
        }

        File getLayerMetadataDir(final String name) {
            return new File(getLayersMetadataDir(), name);
        }

        File getAddOnsMetadataDir() {
            return new File(metadata, ADD_ONS);
        }

        File getAddOnMetadataDir(final String name) {
            return new File(getAddOnsMetadataDir(), name);
        }

        Set<String> getInstalledLayers() {
            return installedLayers;
        }

        Set<String> getInstalledAddOns() {
            return installedAddOns;
        }
    }

    /**
     * @author Emanuel Muckenhuber
     */
    abstract static class AbstractLazyPatchableTarget extends LayerDirectoryStructure implements Layer, AddOn {

        @Override
        File getPatchesMetadata() {
            return getPatchesMetadata(getName());
        }

        @Override
        public DirectoryStructure getDirectoryStructure() {
            return this;
        }

        @Override
        public TargetInfo loadTargetInfo() throws IOException {
            return LayerInfo.loadTargetInfoFromDisk(getDirectoryStructure());
        }

    }

    /**
     * @author Emanuel Muckenhuber
     */
    abstract static class AbstractLazyIdentity extends LayerDirectoryStructure.IdentityDirectoryStructure implements Identity {

        @Override
        public DirectoryStructure getDirectoryStructure() {
            return this;
        }

        @Override
        public TargetInfo loadTargetInfo() throws IOException {
            return LayerInfo.loadTargetInfoFromDisk(getDirectoryStructure());
        }

        @Override
        public File getInstallationInfo() {
            return new File(getPatchesMetadata(), getName() + Constants.DOT_CONF);
        }
    }
}
