/*
 * 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.List;
import java.util.Map;
import java.util.Set;
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.DeploymentEvent;
import org.apache.karaf.features.FeatureEvent;
import org.apache.karaf.features.FeaturesService;
import org.apache.karaf.features.internal.download.DownloadCallback;
import org.apache.karaf.features.internal.download.StreamProvider;
import org.apache.karaf.features.internal.model.Library;
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.service.Blacklist;
import org.apache.karaf.features.internal.service.Deployer;
import org.apache.karaf.features.internal.service.State;
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.InvalidSyntaxException;
import org.osgi.framework.startlevel.BundleStartLevel;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.resource.Resource;
import org.osgi.resource.Wire;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AssemblyDeployCallback 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 Map<String, Bundle> bundles = new HashMap<>();

    public AssemblyDeployCallback(DownloadManager manager, Builder builder, BundleRevision systemBundle, Collection<Features> repositories) throws Exception {
        this.manager = manager;
        this.builder = builder;
        this.homeDirectory = builder.homeDirectory;
        this.etcDirectory = homeDirectory.resolve("etc");
        this.systemDirectory = homeDirectory.resolve("system");
        this.defaultStartLevel = builder.defaultStartLevel;
        dstate = new Deployer.DeploymentState();
        dstate.bundles = new HashMap<>();
        dstate.features = 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());
        for (Features repo : repositories) {
            for (Feature f : repo.getFeature()) {
                dstate.features.put(f.getId(), f);
            }
        }
    }

    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 print(String message, boolean verbose) {
    }

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

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

    @Override
    public void installFeature(org.apache.karaf.features.Feature feature) throws IOException, InvalidSyntaxException {
        // Check blacklist
        if (Blacklist.isFeatureBlacklisted(builder.getBlacklistedFeatures(), feature.getName(), feature.getVersion())) {
            if (builder.getBlacklistPolicy() == Builder.BlacklistPolicy.Fail) {
                throw new RuntimeException("Feature " + feature.getId() + " is blacklisted");
            }
        }
        // Install
        for (Config config : ((Feature) feature).getConfig()) {
            Path configFile = etcDirectory.resolve(config.getName() + ".cfg");
            LOGGER.info("      adding config file: {}", homeDirectory.relativize(configFile));
            if (!Files.exists(configFile)) {
                Files.write(configFile, config.getValue().getBytes());
            } else if (config.isAppend()) {
                Files.write(configFile, config.getValue().getBytes(), StandardOpenOption.APPEND);
            }
        }
        Downloader downloader = manager.createDownloader();
        for (final ConfigFile configFile : ((Feature) feature).getConfigfile()) {
            downloader.download(configFile.getLocation(), new DownloadCallback() {
                @Override
                public void downloaded(StreamProvider provider) throws Exception {
                    Path input = provider.getFile().toPath();
                    String path = configFile.getFinalname();
                    if (path.startsWith("/")) {
                        path = path.substring(1);
                    }
                    path = substFinalName(path);
                    Path output = homeDirectory.resolve(path);
                    LOGGER.info("      adding config file: {}", path);
                    Files.copy(input, output, StandardCopyOption.REPLACE_EXISTING);
                }
            });
        }
        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);
        }
    }

    @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 (Blacklist.isBundleBlacklisted(builder.getBlacklistedBundles(), 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 updateBundle(Bundle bundle, String uri, InputStream is) throws BundleException {
        throw new UnsupportedOperationException();
    }

    @Override
    public void uninstall(Bundle bundle) throws BundleException {
        throw new UnsupportedOperationException();
    }

    @Override
    public void startBundle(Bundle bundle) throws BundleException {
    }

    @Override
    public void stopBundle(Bundle bundle, int options) throws BundleException {
    }

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

    @Override
    public void refreshPackages(Collection<Bundle> bundles) throws InterruptedException {
    }

    @Override
    public void resolveBundles(Set<Bundle> bundles, Map<Resource, List<Wire>> wiring, Map<Resource, Bundle> resToBnd) {
    }

    @Override
    public void replaceDigraph(Map<String, Map<String, Map<String, Set<String>>>> policies, Map<String, Set<Long>> bundles) throws BundleException, InvalidSyntaxException {
    }

    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;
    }
}
