/*
 * 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.brooklyn.entity.osgi.karaf;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

import javax.management.ObjectName;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.TabularData;

import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.core.annotation.Effector;
import org.apache.brooklyn.core.annotation.EffectorParam;
import org.apache.brooklyn.core.feed.ConfigToAttributes;
import org.apache.brooklyn.entity.java.JmxSupport;
import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
import org.apache.brooklyn.feed.jmx.JmxAttributePollConfig;
import org.apache.brooklyn.feed.jmx.JmxFeed;
import org.apache.brooklyn.feed.jmx.JmxHelper;
import org.apache.brooklyn.feed.jmx.JmxValueFunctions;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.repeat.Repeater;
import org.osgi.jmx.JmxConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.collect.Maps;

/**
 * This sets up a Karaf OSGi container
 */
public class KarafContainerImpl extends SoftwareProcessImpl implements KarafContainer {
    
    // TODO Better way of setting/overriding defaults for config keys that are defined in super class SoftwareProcess

    private static final Logger LOG = LoggerFactory.getLogger(KarafContainerImpl.class);

    public static final String KARAF_ADMIN = "org.apache.karaf:type=admin,name=%s";
    public static final String KARAF_FEATURES = "org.apache.karaf:type=features,name=%s";

    public static final String OSGI_BUNDLE_STATE = "osgi.core:type=bundleState,version=1.5";
    public static final String OSGI_FRAMEWORK = "osgi.core:type=framework,version=1.5";
    public static final String OSGI_COMPENDIUM = "osgi.compendium:service=cm,version=1.3";

    protected JmxHelper jmxHelper;

    private JmxFeed jmxFeed;
    
    public KarafContainerImpl() {
        super();
    }

    @Override
    public Class<KarafDriver> getDriverInterface() {
        return KarafDriver.class;
    }

    @Override
    public KarafDriver getDriver() {
        return (KarafDriver) super.getDriver();
    }
    
    @Override
    public void init() {
        super.init();
        new JmxSupport(this, null).recommendJmxRmiCustomAgent();
    }
    
    @Override
    protected void postDriverStart() {
        super.postDriverStart();
        uploadPropertyFiles(getConfig(NAMED_PROPERTY_FILES));
        
        jmxHelper = new JmxHelper(this);
        jmxHelper.connect(0); // i.e. don't block
    }
    
    @Override
    protected void connectSensors() {
        super.connectSensors();

        //FIXME should have a better way of setting config -- firstly, not here!
        //preferred style is to have config auto-applied to attributes, and have default values in their definition, not here
        //use of "properties.{user,password}" is non-standard; is that requried? use default jmxUser, jmxPassword flags?
        sensors().set(JMX_CONTEXT, String.format("karaf-%s", getConfig(KARAF_NAME.getConfigKey())));
        
        ConfigToAttributes.apply(this);

        ObjectName karafAdminObjectName = JmxHelper.createObjectName(String.format(KARAF_ADMIN, getConfig(KARAF_NAME.getConfigKey())));
        
        jmxFeed = JmxFeed.builder()
                .entity(this)
                .helper(jmxHelper)
                .period(500, TimeUnit.MILLISECONDS)
                .pollAttribute(new JmxAttributePollConfig<Map>(KARAF_INSTANCES)
                        .objectName(karafAdminObjectName)
                        .attributeName("Instances")
                        .onSuccess(new Function<Object, Map>() {
                            @Override
                            public Map apply(Object input) {
                                return JmxValueFunctions.tabularDataToMap((TabularData)input);
                            }
                        })
                        .onException(new Function<Exception,Map>() {
                                @Override public Map apply(Exception input) {
                                    // If MBean is unreachable, then mark as service-down
                                    if (Boolean.TRUE.equals(getAttribute(SERVICE_UP))) {
                                        LOG.debug("Entity "+this+" is not reachable on JMX");
                                        sensors().set(SERVICE_UP, false);
                                    }
                                    return null;
                                }}))
                .build();

        
        
        // INSTANCES aggregates data for the other sensors.
        subscriptions().subscribe(this, KARAF_INSTANCES, new SensorEventListener<Map>() {
                @Override public void onEvent(SensorEvent<Map> event) {
                    Map<?,?> map = event.getValue();
                    if (map == null) return;
                    
                    sensors().set(SERVICE_UP, "Started".equals(map.get("State")));
                    sensors().set(KARAF_ROOT, (Boolean) map.get("Is Root"));
                    sensors().set(KARAF_JAVA_OPTS, (String) map.get("JavaOpts"));
                    sensors().set(KARAF_INSTALL_LOCATION, (String) map.get("Location"));
                    sensors().set(KARAF_NAME, (String) map.get("Name"));
                    sensors().set(KARAF_PID, (Integer) map.get("Pid"));
                    sensors().set(KARAF_SSH_PORT, (Integer) map.get("Ssh Port"));
                    sensors().set(KARAF_RMI_REGISTRY_PORT, (Integer) map.get("RMI Registry Port"));
                    sensors().set(KARAF_RMI_SERVER_PORT, (Integer) map.get("RMI Server Port"));
                    sensors().set(KARAF_STATE, (String) map.get("State"));
                }});
        
    }

    @Override
    protected void disconnectSensors() {
        super.disconnectSensors();
        if (jmxFeed != null) jmxFeed.stop();
    }
    
    @Override
    protected void preStop() {
        super.preStop();
        
        if (jmxHelper != null) jmxHelper.terminate();
    }

    @Override
    @Effector(description="Updates the OSGi Service's properties, adding (and overriding) the given key-value pairs")
    public void updateServiceProperties(
            @EffectorParam(name="serviceName", description="Name of the OSGi service") String serviceName, 
            Map<String,String> additionalVals) {
        TabularData table = (TabularData) jmxHelper.operation(OSGI_COMPENDIUM, "getProperties", serviceName);
        
        try {
            for (Map.Entry<String, String> entry: additionalVals.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                CompositeData data = new CompositeDataSupport(
                        JmxConstants.PROPERTY_TYPE,
                        MutableMap.of(JmxConstants.KEY, key, JmxConstants.TYPE, "String", JmxConstants.VALUE, value));
                table.remove(data.getAll(new String[] {JmxConstants.KEY}));
                table.put(data);
            }
        } catch (OpenDataException e) {
            throw Exceptions.propagate(e);
        }
        
        LOG.info("Updating monterey-service configuration with changes {}", additionalVals);
        if (LOG.isTraceEnabled()) LOG.trace("Updating monterey-service configuration with new configuration {}", table);
        
        jmxHelper.operation(OSGI_COMPENDIUM, "update", serviceName, table);
    }
    
    @Override
    @Effector(description="Updates the OSGi Service's properties, adding (and overriding) the given key-value pairs")
    public void installFeature(
            @EffectorParam(name="featureName", description="Name of the feature - see org.apache.karaf:type=features#installFeature()") final String featureName) throws Exception {
        
        LOG.info("Installing feature {} via JMX", featureName);

        Repeater.create("Wait for Karaf, to install feature "+featureName)
                .limitIterationsTo(40)
                .every(500, TimeUnit.MILLISECONDS)
                .until(new Callable<Boolean>() {
                        @Override
                        public Boolean call() {
                            jmxHelper.operation(String.format(KARAF_FEATURES, getConfig(KARAF_NAME.getConfigKey())), "installFeature", featureName);
                            return true;
                        }})
                .rethrowException()
                .run();
    }

    @Override
    public Map<Long,Map<String,?>> listBundles() {
        TabularData table = (TabularData) jmxHelper.operation(OSGI_BUNDLE_STATE, "listBundles");
        Map<List<?>, Map<String, Object>> map = JmxValueFunctions.tabularDataToMapOfMaps(table);
        
        Map<Long,Map<String,?>> result = Maps.newLinkedHashMap();
        for (Map.Entry<List<?>, Map<String, Object>> entry : map.entrySet()) {
            result.put((Long)entry.getKey().get(0), entry.getValue());
        }
        return result;
    }
    
    /**
     * throws URISyntaxException If bundle name is not a valid URI
     */
    @Override
    @Effector(description="Deploys the given bundle, returning the bundle id - see osgi.core:type=framework#installBundle()")
    public long installBundle(
            @EffectorParam(name="bundle", description="URI of bundle to be deployed") String bundle) throws URISyntaxException {
        
        // TODO Consider switching to use either:
        //  - org.apache.karaf:type=bundles#install(String), or 
        //  - dropping file into $RUN_DIR/deploy (but that would be async)

        URI uri = new URI(bundle);
        boolean wrap = false;
        if (WRAP_SCHEME.equals(uri.getScheme())) {
            bundle = bundle.substring(WRAP_SCHEME.length() + 1);
            uri = new URI(bundle);
            wrap = true;
        }
        if (FILE_SCHEME.equals(uri.getScheme())) {
            LOG.info("Deploying bundle {} via file copy", bundle);
            File source = new File(uri);
            String target = getDriver().getRunDir() + "/" + source.getName();
            getDriver().copyResource(source, target);
            return (Long) jmxHelper.operation(OSGI_FRAMEWORK, "installBundle", (wrap ? WRAP_SCHEME + ":" : "") + FILE_SCHEME + "://" + target);
        } else {
            LOG.info("Deploying bundle {} via JMX", bundle);
            return (Long) jmxHelper.operation(OSGI_FRAMEWORK, "installBundle", (wrap ? WRAP_SCHEME + ":" : "") + bundle);
        }
    }

    @Override
    @Effector(description="Undeploys the bundle with the given id")
    public void uninstallBundle(
            @EffectorParam(name="bundleId", description="Id of the bundle") Long bundleId) {
        
        // TODO Consider switching to use either:
        //  - org.apache.karaf:type=bundles#install(String), or 
        //  - dropping file into $RUN_DIR/deploy (but that would be async)

        jmxHelper.operation(OSGI_FRAMEWORK, "uninstallBundle", bundleId);
    }

    protected void uploadPropertyFiles(Map<String,Map<String,String>> propertyFiles) {
        if (propertyFiles == null) return;
        
        for (Map.Entry<String,Map<String,String>> entry : propertyFiles.entrySet()) {
            String file = entry.getKey();
            Map<String,String> contents = entry.getValue();

            Properties props = new Properties();
            for (Map.Entry<String,String> prop : contents.entrySet()) {
                props.setProperty(prop.getKey(), prop.getValue());
            }
            
            File local = Os.writePropertiesToTempFile(props, "karaf-"+getId(), ".cfg");
            local.setReadable(true);
            try {
                String remote = getDriver().getRunDir() + "/" + file;
                getDriver().copyResource(local, remote);
            } finally {
                local.delete();
            }
        }
    }
}
