/*
 * 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.region.persist.internal;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import org.apache.karaf.region.persist.RegionsPersistence;
import org.apache.karaf.region.persist.internal.model.FilterAttributeType;
import org.apache.karaf.region.persist.internal.model.FilterBundleType;
import org.apache.karaf.region.persist.internal.model.FilterNamespaceType;
import org.apache.karaf.region.persist.internal.model.FilterPackageType;
import org.apache.karaf.region.persist.internal.model.FilterType;
import org.apache.karaf.region.persist.internal.model.RegionBundleType;
import org.apache.karaf.region.persist.internal.model.RegionType;
import org.apache.karaf.region.persist.internal.model.RegionsType;
import org.apache.karaf.region.persist.internal.util.ManifestHeaderProcessor;
import org.eclipse.equinox.region.Region;
import org.eclipse.equinox.region.RegionDigraph;
import org.eclipse.equinox.region.RegionFilterBuilder;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleRevision;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RegionsPersistenceImpl implements RegionsPersistence {

    private static final Logger log = LoggerFactory.getLogger(RegionsPersistenceImpl.class);

    private JAXBContext jaxbContext;
    private RegionDigraph regionDigraph;
    private Region kernel;
    private Bundle digraphBundle;
    private Bundle framework;

    public RegionsPersistenceImpl(RegionDigraph regionDigraph, Bundle digraphBundle, Bundle framework) throws JAXBException, BundleException, IOException, InvalidSyntaxException {
        log.info("Loading region digraph persistence");
        this.digraphBundle = digraphBundle;
        this.framework = framework;
        this.regionDigraph = regionDigraph;
        kernel = regionDigraph.getRegion(0);
        jaxbContext = JAXBContext.newInstance(RegionsType.class);
        load();
    }

    @Override
    public void install(Bundle b, String regionName) throws BundleException {
        Region region = regionDigraph.getRegion(regionName);
        if (region == null) {
            region = regionDigraph.createRegion(regionName);
        }
        kernel.removeBundle(b);
        region.addBundle(b);
    }

    @Override
    public void save() throws BundleException, IOException {
        File digraphFile = digraphBundle.getBundleContext().getDataFile("digraph");
        FileOutputStream digraphStream = new FileOutputStream(digraphFile);
        try  {
            regionDigraph.getRegionDigraphPersistence().save(
                    regionDigraph,
                    digraphStream

            );
        } finally {
            digraphStream.close();
        }
    }

    void save(RegionsType regionsType, Writer out) throws JAXBException {
        Marshaller marshaller = jaxbContext.createMarshaller();
        marshaller.marshal(regionsType, out);
    }

    void load() throws IOException, BundleException, JAXBException, InvalidSyntaxException {
        if (this.regionDigraph.getRegions().size() <= 1) {
            File etc = new File(System.getProperty("karaf.etc"));
            File regionsConfig = new File(etc, "regions-config.xml");
            if (regionsConfig.exists()) {
                log.info("initializing region digraph from etc/regions-config.xml");
                Reader in = new FileReader(regionsConfig);
                try {
                        load(this.regionDigraph, in);
                    } finally {
                        in.close();
                    }
            } else {
                log.info("no regions config file");
            }
        }

    }

    void  load(RegionDigraph regionDigraph, Reader in) throws JAXBException, BundleException, InvalidSyntaxException {
        RegionsType regionsType = load(in);
        load(regionsType, regionDigraph);
    }

    RegionsType load(Reader in) throws JAXBException {
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        return (RegionsType) unmarshaller.unmarshal(in);
    }

    void load(RegionsType regionsType, RegionDigraph regionDigraph) throws BundleException, InvalidSyntaxException {
        BundleContext frameworkContext = framework.getBundleContext();
        for (RegionType regionType: regionsType.getRegion()) {
            String name = regionType.getName();
            log.debug("Creating region: " + name);
            Region region = regionDigraph.createRegion(name);
            for (RegionBundleType bundleType: regionType.getBundle()) {
                if (bundleType.getId() != null) {
                    region.addBundle(bundleType.getId());
                } else {
                    Bundle b = frameworkContext.getBundle(bundleType.getLocation());
                    region.addBundle(b);
                }
            }
        }
        for (FilterType filterType: regionsType.getFilter()) {
            Region from = regionDigraph.getRegion(filterType.getFrom());
            Region to = regionDigraph.getRegion(filterType.getTo());
            log.debug("Creating filter between " + from.getName() + " to " + to.getName());
            RegionFilterBuilder builder = regionDigraph.createRegionFilterBuilder();
            for (FilterBundleType bundleType: filterType.getBundle()) {
                String symbolicName = bundleType.getSymbolicName();
                String version = bundleType.getVersion();
                if (bundleType.getId() != null) {
                    Bundle b = frameworkContext.getBundle(bundleType.getId());
                    symbolicName = b.getSymbolicName();
                    version = b.getVersion().toString();
                }
                String namespace = BundleRevision.BUNDLE_NAMESPACE;
                List<FilterAttributeType> attributeTypes = bundleType.getAttribute();
                buildFilter(symbolicName, version, namespace, attributeTypes, builder);
            }
            for (FilterPackageType packageType: filterType.getPackage()) {
                String packageName = packageType.getName();
                String version = packageType.getVersion();
                String namespace = BundleRevision.PACKAGE_NAMESPACE;
                List<FilterAttributeType> attributeTypes = packageType.getAttribute();
                buildFilter(packageName, version, namespace, attributeTypes, builder);
            }
            if (to == kernel) {
                //add framework exports
                BundleRevision rev = framework.adapt(BundleRevision.class);
                List<BundleCapability> caps = rev.getDeclaredCapabilities(BundleRevision.PACKAGE_NAMESPACE);
                for (BundleCapability cap : caps) {
                    String filter = ManifestHeaderProcessor.generateFilter(filter(cap.getAttributes()));
                    builder.allow(BundleRevision.PACKAGE_NAMESPACE, filter);
                }
            }
            //TODO explicit services?
            for (FilterNamespaceType namespaceType: filterType.getNamespace()) {
                String namespace = namespaceType.getName();
                HashMap<String, Object> attributes = new HashMap<String, Object>();
                for (FilterAttributeType attributeType: namespaceType.getAttribute()) {
                    attributes.put(attributeType.getName(), attributeType.getValue());
                }
                String filter = ManifestHeaderProcessor.generateFilter(attributes);
                builder.allow(namespace, filter);
            }
            regionDigraph.connect(from, builder.build(), to);
        }
    }

    private Map<String, Object> filter(Map<String, Object> attributes) {
        Map<String, Object> result = new HashMap<String, Object>(attributes);
        result.remove("bundle-version");
        result.remove("bundle-symbolic-name");
        return result;
    }

    private void buildFilter(String packageName, String version, String namespace, List<FilterAttributeType> attributeTypes, RegionFilterBuilder builder) throws InvalidSyntaxException {
        HashMap<String, Object> attributes = new HashMap<String, Object>();
        if (namespace != null) {
            attributes.put(namespace, packageName);
        }
        if (version != null) {
            attributes.put("version", version);
        }
        for (FilterAttributeType attributeType: attributeTypes) {
            attributes.put(attributeType.getName(), attributeType.getValue());
        }
        String filter = ManifestHeaderProcessor.generateFilter(attributes);
        builder.allow(namespace, filter);
    }

}
