/*
 * 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.profile.assembly;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.jar.Attributes;
import java.util.jar.JarFile;

import org.apache.felix.utils.properties.Properties;
import org.apache.karaf.features.BundleInfo;
import org.apache.karaf.features.DeploymentEvent;
import org.apache.karaf.features.FeatureEvent;
import org.apache.karaf.features.FeaturesService;
import org.apache.karaf.features.internal.download.DownloadManager;
import org.apache.karaf.features.internal.download.Downloader;
import org.apache.karaf.features.internal.model.Config;
import org.apache.karaf.features.internal.model.ConfigFile;
import org.apache.karaf.features.internal.model.Feature;
import org.apache.karaf.features.internal.model.Features;
import org.apache.karaf.features.internal.model.Library;
import org.apache.karaf.features.internal.service.Deployer;
import org.apache.karaf.features.internal.service.FeaturesProcessor;
import org.apache.karaf.features.internal.service.State;
import org.apache.karaf.features.internal.service.StaticInstallSupport;
import org.apache.karaf.features.internal.util.MapUtils;
import org.apache.karaf.util.maven.Parser;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.startlevel.BundleStartLevel;
import org.osgi.framework.wiring.BundleRevision;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Callback through which {@link Deployer} will interact with the distribution that's being assembled.
 */
public class AssemblyDeployCallback extends StaticInstallSupport implements Deployer.DeployCallback {

    private static final Logger LOGGER = LoggerFactory.getLogger(Builder.class);

    private final DownloadManager manager;
    private final Builder builder;
    private final Path homeDirectory;
    private final int defaultStartLevel;
    private final Path etcDirectory;
    private final Path systemDirectory;
    private final Deployer.DeploymentState dstate;
    private final AtomicLong nextBundleId = new AtomicLong(0);
    private final FeaturesProcessor processor;

    private final Map<String, Bundle> bundles = new HashMap<>();

    /**
     * Create a {@link Deployer.DeployCallback} performing actions on runtime with single system bundle installed
     * and with access to all non-blacklisted features.
     * @param manager
     * @param builder
     * @param systemBundle
     * @param repositories
     * @param processor
     */
    public AssemblyDeployCallback(DownloadManager manager, Builder builder, BundleRevision systemBundle, Collection<Features> repositories,
                                  FeaturesProcessor processor) {
        this.manager = manager;
        this.builder = builder;
        this.homeDirectory = builder.homeDirectory;
        this.etcDirectory = homeDirectory.resolve("etc");
        this.systemDirectory = homeDirectory.resolve("system");
        this.defaultStartLevel = builder.defaultStartLevel;
        this.processor = processor;

        dstate = new Deployer.DeploymentState();
        dstate.bundles = new HashMap<>();
        dstate.bundlesPerRegion = new HashMap<>();
        dstate.filtersPerRegion = new HashMap<>();
        dstate.state = new State();

        MapUtils.addToMapSet(dstate.bundlesPerRegion, FeaturesService.ROOT_REGION, 0l);
        dstate.bundles.put(0l, systemBundle.getBundle());

        Collection<org.apache.karaf.features.Feature> features = new LinkedList<>();
        for (Features repo : repositories) {
            if (repo.isBlacklisted()) {
                continue;
            }
            for (Feature f : repo.getFeature()) {
                if (!f.isBlacklisted()) {
                    features.add(f);
                }
            }
        }
        dstate.partitionFeatures(features);
    }

    /**
     * Get startup bundles with related start-level
     * @return
     */
    public Map<String, Integer> getStartupBundles() {
        Map<String, Integer> startup = new HashMap<>();
        for (Map.Entry<String, Bundle> bundle : bundles.entrySet()) {
            int level = bundle.getValue().adapt(BundleStartLevel.class).getStartLevel();
            if (level <= 0) {
                level = defaultStartLevel;
            }
            startup.put(bundle.getKey(), level);
        }
        return startup;
    }

    public Deployer.DeploymentState getDeploymentState() {
        return dstate;
    }

    @Override
    public void saveState(State state) {
        dstate.state.replace(state);
    }

    @Override
    public void persistResolveRequest(Deployer.DeploymentRequest request) {
    }

    @Override
    public void installConfigs(org.apache.karaf.features.Feature feature) throws IOException {
        assertNotBlacklisted(feature);
        // Install
        Downloader downloader = manager.createDownloader();
        for (Config config : ((Feature) feature).getConfig()) {
            Path configFile = etcDirectory.resolve(config.getName() + ".cfg");
            if (Files.exists(configFile) && !config.isAppend()) {
                LOGGER.info("      not changing existing config file: {}", homeDirectory.relativize(configFile));
                continue;
            }
            if (config.isExternal()) {
                downloader.download(config.getValue().trim(), provider -> {
                    Path input = provider.getFile().toPath();
                    byte[] data = Files.readAllBytes(input);
                    if (config.isAppend()) {
                        LOGGER.info("      appending to config file: {}", homeDirectory.relativize(configFile));
                        Files.write(configFile, data, StandardOpenOption.APPEND);
                    } else {
                        LOGGER.info("      adding config file: {}", homeDirectory.relativize(configFile));
                        Files.write(configFile, data);
                    }
                });
            } else {
                byte[] data = config.getValue().getBytes();
                if (config.isAppend()) {
                    LOGGER.info("      appending to config file: {}", homeDirectory.relativize(configFile));
                    Files.write(configFile, data, StandardOpenOption.APPEND);
                } else {
                    LOGGER.info("      adding config file: {}", homeDirectory.relativize(configFile));
                    Files.write(configFile, data);
                }
            }
        }
        for (final ConfigFile configFile : ((Feature) feature).getConfigfile()) {
            String path = configFile.getFinalname();
            if (path.startsWith("/")) {
                path = path.substring(1);
            }
            path = substFinalName(path);
            final Path output = homeDirectory.resolve(path);
            final String finalPath = path;
            if (configFile.isOverride() || !Files.exists(output)) {
                downloader.download(configFile.getLocation(), provider -> {
                    Path input = provider.getFile().toPath();
                    if (configFile.isOverride()) {
                        LOGGER.info("      overwriting config file: {}", finalPath);
                    } else {
                        LOGGER.info("      adding config file: {}", finalPath);
                    }
                    Files.copy(input, output, StandardCopyOption.REPLACE_EXISTING);
                });
            }
        }
    }

    @Override
    public void installLibraries(org.apache.karaf.features.Feature feature) throws IOException {
        assertNotBlacklisted(feature);
        Downloader downloader = manager.createDownloader();
        List<String> libraries = new ArrayList<>();
        for (Library library : ((Feature) feature).getLibraries()) {
            String lib = library.getLocation() +
                    ";type:=" + library.getType() +
                    ";export:=" + library.isExport() +
                    ";delegate:=" + library.isDelegate();
            libraries.add(lib);
        }
        if (!libraries.isEmpty()) {
            Path configPropertiesPath = etcDirectory.resolve("config.properties");
            Properties configProperties = new Properties(configPropertiesPath.toFile());
            builder.downloadLibraries(downloader, configProperties, libraries, "   ");
        }
        try {
            downloader.await();
        } catch (Exception e) {
            throw new IOException("Error downloading configuration files", e);
        }
    }
    
    private void assertNotBlacklisted(org.apache.karaf.features.Feature feature) {
        if (feature.isBlacklisted()) {
            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
                throw new RuntimeException("Feature " + feature.getId() + " is blacklisted");
            }
        }
    }

    @Override
    public void callListeners(DeploymentEvent deployEvent) {
    }

    @Override
    public void callListeners(FeatureEvent featureEvent) {
    }

    @Override
    public Bundle installBundle(String region, String uri, InputStream is) throws BundleException {
        // Check blacklist
        if (processor.isBundleBlacklisted(uri)) {
            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
                throw new RuntimeException("Bundle " + uri + " is blacklisted");
            }
        }

        // Install
        LOGGER.info("      adding maven artifact: " + uri);
        try {
            String regUri;
            String path;
            if (uri.startsWith("mvn:")) {
                regUri = uri;
                path = Parser.pathFromMaven(uri);
            } else {
                uri = uri.replaceAll("[^0-9a-zA-Z.\\-_]+", "_");
                if (uri.length() > 256) {
                    //to avoid the File name too long exception
                    uri = uri.substring(0, 255);
                }
                path = "generated/" + uri;
                regUri = "file:" + path;
            }
            final Path bundleSystemFile = systemDirectory.resolve(path);
            Files.createDirectories(bundleSystemFile.getParent());
            Files.copy(is, bundleSystemFile, StandardCopyOption.REPLACE_EXISTING);

            Hashtable<String, String> headers = new Hashtable<>();
            try (JarFile jar = new JarFile(bundleSystemFile.toFile())) {
                Attributes attributes = jar.getManifest().getMainAttributes();
                for (Map.Entry<Object, Object> attr : attributes.entrySet()) {
                    headers.put(attr.getKey().toString(), attr.getValue().toString());
                }
            }
            BundleRevision revision = new FakeBundleRevision(headers, uri, nextBundleId.incrementAndGet());
            Bundle bundle = revision.getBundle();
            MapUtils.addToMapSet(dstate.bundlesPerRegion, region, bundle.getBundleId());
            dstate.bundles.put(bundle.getBundleId(), bundle);

            bundles.put(regUri, bundle);
            return bundle;
        } catch (IOException e) {
            throw new BundleException("Unable to install bundle", e);
        }
    }

    @Override
    public void setBundleStartLevel(Bundle bundle, int startLevel) {
        bundle.adapt(BundleStartLevel.class).setStartLevel(startLevel);
    }

    @Override
    public void bundleBlacklisted(BundleInfo bundleInfo) {
        LOGGER.info("      skipping blacklisted bundle: {}", bundleInfo.getLocation());
    }

    private String substFinalName(String finalname) {
        final String markerVarBeg = "${";
        final String markerVarEnd = "}";

        boolean startsWithVariable = finalname.startsWith(markerVarBeg) && finalname.contains(markerVarEnd);
        if (startsWithVariable) {
            String marker = finalname.substring(markerVarBeg.length(), finalname.indexOf(markerVarEnd));
            switch (marker) {
            case "karaf.base":
                return this.homeDirectory + finalname.substring(finalname.indexOf(markerVarEnd)+markerVarEnd.length());
            case "karaf.etc":
                return this.etcDirectory + finalname.substring(finalname.indexOf(markerVarEnd)+markerVarEnd.length());
            default:
                break;
            }
        }
        return finalname;
    }

}
