/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.karaf.features.internal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.karaf.features.BootFinished;
import org.apache.karaf.features.Feature;
import org.apache.karaf.features.FeaturesService;
import org.apache.karaf.features.FeaturesService.Option;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Manages installation of the boot features in the background
 */
public class BootFeaturesInstaller {
	private static final Logger LOGGER = LoggerFactory.getLogger(BootFeaturesInstaller.class);

    public static String VERSION_PREFIX = "version=";

    private final BundleContext bundleContext;
    private final FeaturesService featuresService;
    private final String boot;
    private final boolean bootAsynchronous;

    /**
     * 
     * @param featuresService
     * @param boot list of boot features separated by comma. Optionally contains ;version=x.x.x to specify a specific feature version
     */
    public BootFeaturesInstaller(BundleContext bundleContext, FeaturesService featuresService, String boot, boolean bootAsynchronous) {
		this.bundleContext = bundleContext;
        this.featuresService = featuresService;
		this.boot = boot;
        this.bootAsynchronous = bootAsynchronous;
	}
    
    /**
     * Install boot features
     * @throws Exception
     */
    public void start() throws Exception {
        if (boot != null) {
            if (bootAsynchronous) {
                new Thread() {
                    public void run() {
                        installBootFeatures();
                        publishBootFinished();
                    }
                }.start();
            } else {
                installBootFeatures();
                publishBootFinished();
            }
        } else {
            publishBootFinished();
        }
    }
    
	void installBootFeatures() {
	    List<Feature> installedFeatures = Arrays.asList(featuresService.listInstalledFeatures());
		List<Set<String>> stagedFeatureNames = parseBootFeatures(boot);
        List<Set<Feature>> stagedFeatures = toFeatureSetList(stagedFeatureNames);

        try {
            for (Set<Feature> features : stagedFeatures) {
                features.removeAll(installedFeatures);
                featuresService.installFeatures(features, EnumSet.of(Option.NoAutoRefreshBundles, Option.NoCleanIfFailure, Option.ContinueBatchOnFailure));
            }
        } catch (Exception e) {
            LOGGER.error("Error installing boot features", e);
        }
	}
	
	private List<Set<Feature>> toFeatureSetList(List<Set<String>> stagedFeatures) {
	    ArrayList<Set<Feature>> result = new ArrayList<Set<Feature>>();
	    for (Set<String> features : stagedFeatures) {
	        HashSet<Feature> featureSet = new LinkedHashSet<Feature>();
            for (String featureName : features) {
                try {
                    Feature feature = getFeature(featureName);
                    if (feature == null) {
                        LOGGER.error("Error Boot feature " + featureName + " not found");
                    } else {
                        featureSet.add(feature);
                    }
                } catch (Exception e) {
                    LOGGER.error("Error getting feature for feature string " + featureName, e);
                }
            }
            result.add(featureSet);
        }
        return result;
	}
	
	/**
	 * 
	 * @param featureSt either feature name or <featurename>;version=<version>
	 * @return feature matching the feature string
	 * @throws Exception
	 */
    private Feature getFeature(String featureSt) throws Exception {
        String[] parts = featureSt.trim().split(";");
        String featureName = parts[0];
        String featureVersion = null;
        for (String part : parts) {
            // if the part starts with "version=" it contains the version info
            if (part.startsWith(VERSION_PREFIX)) {
                featureVersion = part.substring(VERSION_PREFIX.length());
            }
        }
        if (featureVersion == null) {
            // no version specified - use default version
            featureVersion = org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION;
        }
        return featuresService.getFeature(featureName, featureVersion);
    }
    
    protected List<Set<String>> parseBootFeatures(String bootFeatures) {
        Pattern pattern = Pattern.compile("(\\s*\\(([^)]+))\\s*\\)\\s*,\\s*|.+");
        Matcher matcher = pattern.matcher(bootFeatures);
        List<Set<String>> result = new ArrayList<Set<String>>();
        while (matcher.find()) {
            String group = matcher.group(2) != null ? matcher.group(2) : matcher.group();
            result.add(parseFeatureList(group));
        }
        return result;
    }

    private Set<String> parseFeatureList(String group) {
        HashSet<String> features = new LinkedHashSet<String>();
        for (String feature : Arrays.asList(group.trim().split("\\s*,\\s*"))) {
            if (feature.length() > 0) {
                features.add(feature);
            }
        }
        return features;
    }

    private void publishBootFinished() {
        if (bundleContext != null) {
            BootFinished bootFinished = new BootFinished() {};
            bundleContext.registerService(BootFinished.class, bootFinished, new Hashtable<String, String>());
        }
    }

}
