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

import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.management.openmbean.CompositeData;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.sensor.EnricherSpec;
import org.apache.brooklyn.core.config.render.RendererHints;
import org.apache.brooklyn.feed.http.HttpValueFunctions;
import org.apache.brooklyn.feed.jmx.JmxAttributePollConfig;
import org.apache.brooklyn.feed.jmx.JmxFeed;
import org.apache.brooklyn.policy.enricher.RollingTimeWindowMeanEnricher;
import org.apache.brooklyn.policy.enricher.TimeFractionDeltaEnricher;
import org.apache.brooklyn.util.math.MathFunctions;
import org.apache.brooklyn.util.text.ByteSizeStrings;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;

import com.google.common.base.Function;

public class JavaAppUtils {

    public static boolean isEntityMxBeanStatsEnabled(Entity entity) {
        return groovyTruth(entity.getConfig(UsesJavaMXBeans.MXBEAN_STATS_ENABLED));
    }

    /**
     * @see #connectJavaAppServerPolicies(Entity, Duration)
     * @see #getMxBeanSensorsBuilder(Entity)
     */
    @Nullable
    public static JmxFeed connectMXBeanSensors(Entity entity) {
        if (isEntityMxBeanStatsEnabled(entity)) {
            return getMxBeanSensorsBuilder(entity).build();
        } else {
            return null;
        }
    }

    /** @see #connectJavaAppServerPolicies(Entity, Duration) */
    @Nullable
    public static JmxFeed connectMXBeanSensors(Entity entity, long jmxPollPeriodMs) {
        if (isEntityMxBeanStatsEnabled(entity)) {
            return getMxBeanSensorsBuilder(entity, jmxPollPeriodMs).build();
        } else {
            return null;
        }
    }

    /**
     * @param entity The entity at which to poll
     * @param jmxPollPeriod How often to poll
     * @return A {@link JmxFeed} configured to poll the given entity at the given period,
     *         or null if the entity is not configured for MXBEAN_STATS
     * @see org.apache.brooklyn.entity.java.UsesJavaMXBeans#MXBEAN_STATS_ENABLED
     */
    @Nullable
    public static JmxFeed connectMXBeanSensors(Entity entity, Duration jmxPollPeriod) {
        if (isEntityMxBeanStatsEnabled(entity)) {
            return getMxBeanSensorsBuilder(entity, jmxPollPeriod).build();
        } else {
            return null;
        }
    }
    
    public static void connectJavaAppServerPolicies(Entity entity) {
        connectJavaAppServerPolicies(entity, Duration.TEN_SECONDS);
    }

    public static void connectJavaAppServerPolicies(Entity entity, Duration windowPeriod) {
        entity.enrichers().add(EnricherSpec.create(TimeFractionDeltaEnricher.class)
                .configure("producer", entity)
                .configure("source", UsesJavaMXBeans.PROCESS_CPU_TIME)
                .configure("target", UsesJavaMXBeans.PROCESS_CPU_TIME_FRACTION_LAST)
                .configure("durationPerOrigUnit", Duration.millis(1)));

        entity.enrichers().add(EnricherSpec.create(RollingTimeWindowMeanEnricher.class)
                .configure("producer", entity)
                .configure("source", UsesJavaMXBeans.PROCESS_CPU_TIME_FRACTION_LAST)
                .configure("target", UsesJavaMXBeans.PROCESS_CPU_TIME_FRACTION_IN_WINDOW)
                .configure("timePeriod", windowPeriod));
    }

    /**
     * @param entity The entity at which to poll
     * @return A {@link JmxFeed.Builder} configured to poll entity every ten seconds
     * @see #getMxBeanSensorsBuilder(Entity, Duration)
     */
    @Nonnull
    public static JmxFeed.Builder getMxBeanSensorsBuilder(Entity entity) {
        return getMxBeanSensorsBuilder(entity, Duration.TEN_SECONDS);
    }

    /** @see #getMxBeanSensorsBuilder(Entity, Duration) */
    @Nonnull
    public static JmxFeed.Builder getMxBeanSensorsBuilder(Entity entity, long jmxPollPeriod) {
        return getMxBeanSensorsBuilder(entity, Duration.millis(jmxPollPeriod));
    }

    /**
     * @param entity The entity at which to poll
     * @param jmxPollPeriod How often to poll
     * @return A {@link JmxFeed.Builder} configured to poll many interesting MXBeans
     *         at the given entity and to repeat according to the given poll period.
     *         <p/>
     *         If an entity does not have MXBean stats enabled (i.e. {@link UsesJavaMXBeans#MXBEAN_STATS_ENABLED} is
     *         configured to false) then returns a builder configured with entity and duration but no polls.
     *         <p/>
     *         Use {@link #connectMXBeanSensors(Entity, Duration)} to create and build in one step.
     */
    @Nonnull
    @SuppressWarnings({"unchecked"})
    public static JmxFeed.Builder getMxBeanSensorsBuilder(Entity entity, Duration jmxPollPeriod) {
        JmxFeed.Builder builder = JmxFeed.builder()
                .entity(entity)
                .period(jmxPollPeriod);
        if (isEntityMxBeanStatsEnabled(entity)) {
            // TODO Could we reuse the result of compositeDataToMemoryUsage?
            builder
                .pollAttribute(new JmxAttributePollConfig<Long>(UsesJavaMXBeans.USED_HEAP_MEMORY)
                        .objectName(ManagementFactory.MEMORY_MXBEAN_NAME)
                        .attributeName("HeapMemoryUsage")
                        .onSuccess((Function) HttpValueFunctions.chain(compositeDataToMemoryUsage(), new Function<MemoryUsage, Long>() {
                            @Override public Long apply(MemoryUsage input) {
                                return (input == null) ? null : input.getUsed();
                            }})))
                .pollAttribute(new JmxAttributePollConfig<Long>(UsesJavaMXBeans.INIT_HEAP_MEMORY)
                        .objectName(ManagementFactory.MEMORY_MXBEAN_NAME)
                        .attributeName("HeapMemoryUsage")
                        .onSuccess((Function) HttpValueFunctions.chain(compositeDataToMemoryUsage(), new Function<MemoryUsage, Long>() {
                            @Override public Long apply(MemoryUsage input) {
                                return (input == null) ? null : input.getInit();
                            }})))
                .pollAttribute(new JmxAttributePollConfig<Long>(UsesJavaMXBeans.COMMITTED_HEAP_MEMORY)
                        .objectName(ManagementFactory.MEMORY_MXBEAN_NAME)
                        .attributeName("HeapMemoryUsage")
                        .onSuccess((Function) HttpValueFunctions.chain(compositeDataToMemoryUsage(), new Function<MemoryUsage, Long>() {
                            @Override public Long apply(MemoryUsage input) {
                                return (input == null) ? null : input.getCommitted();
                            }})))
                .pollAttribute(new JmxAttributePollConfig<Long>(UsesJavaMXBeans.MAX_HEAP_MEMORY)
                        .objectName(ManagementFactory.MEMORY_MXBEAN_NAME)
                        .attributeName("HeapMemoryUsage")
                        .onSuccess((Function) HttpValueFunctions.chain(compositeDataToMemoryUsage(), new Function<MemoryUsage, Long>() {
                            @Override public Long apply(MemoryUsage input) {
                                return (input == null) ? null : input.getMax();
                            }})))
                .pollAttribute(new JmxAttributePollConfig<Long>(UsesJavaMXBeans.NON_HEAP_MEMORY_USAGE)
                        .objectName(ManagementFactory.MEMORY_MXBEAN_NAME)
                        .attributeName("NonHeapMemoryUsage")
                        .onSuccess((Function) HttpValueFunctions.chain(compositeDataToMemoryUsage(), new Function<MemoryUsage, Long>() {
                            @Override public Long apply(MemoryUsage input) {
                                return (input == null) ? null : input.getUsed();
                            }})))

                .pollAttribute(new JmxAttributePollConfig<Integer>(UsesJavaMXBeans.CURRENT_THREAD_COUNT)
                        .objectName(ManagementFactory.THREAD_MXBEAN_NAME)
                        .attributeName("ThreadCount"))
                .pollAttribute(new JmxAttributePollConfig<Integer>(UsesJavaMXBeans.PEAK_THREAD_COUNT)
                        .objectName(ManagementFactory.THREAD_MXBEAN_NAME)
                        .attributeName("PeakThreadCount"))

                .pollAttribute(new JmxAttributePollConfig<Long>(UsesJavaMXBeans.START_TIME)
                        .objectName(ManagementFactory.RUNTIME_MXBEAN_NAME)
                        .period(60, TimeUnit.SECONDS)
                        .attributeName("StartTime"))
                .pollAttribute(new JmxAttributePollConfig<Long>(UsesJavaMXBeans.UP_TIME)
                        .objectName(ManagementFactory.RUNTIME_MXBEAN_NAME)
                        .period(60, TimeUnit.SECONDS)
                        .attributeName("Uptime"))

                .pollAttribute(new JmxAttributePollConfig<Double>(UsesJavaMXBeans.PROCESS_CPU_TIME)
                        .objectName(ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME)
                        .attributeName("ProcessCpuTime")
                        .onSuccess((Function) MathFunctions.times(0.001*0.001)))   // nanos to millis
                .pollAttribute(new JmxAttributePollConfig<Double>(UsesJavaMXBeans.SYSTEM_LOAD_AVERAGE)
                        .objectName(ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME)
                        .attributeName("SystemLoadAverage"))
                .pollAttribute(new JmxAttributePollConfig<Integer>(UsesJavaMXBeans.AVAILABLE_PROCESSORS)
                        .objectName(ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME)
                        .period(60, TimeUnit.SECONDS)
                        .attributeName("AvailableProcessors"))
                .pollAttribute(new JmxAttributePollConfig<Long>(UsesJavaMXBeans.TOTAL_PHYSICAL_MEMORY_SIZE)
                        .objectName(ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME)
                        .period(60, TimeUnit.SECONDS)
                        .attributeName("TotalPhysicalMemorySize"))
                .pollAttribute(new JmxAttributePollConfig<Long>(UsesJavaMXBeans.FREE_PHYSICAL_MEMORY_SIZE)
                        .objectName(ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME)
                        .period(60, TimeUnit.SECONDS)
                        .attributeName("FreePhysicalMemorySize"));
         //FIXME: need a new type of adapter that maps multiple objectNames to a mapping
         // jmxAdapter.objectName(ManagementFactory.GARBAGE_COLLECTOR_MXBEAN_DOMAIN_TYPE + ",*").with {
         //     attribute("SystemLoadAverage").subscribe(UsesJavaMXBeans.GARBAGE_COLLECTION_TIME, { def m -> log.info("XXXXXXX $m") });
         // }
        }
        return builder;
    }

    public static Function<CompositeData, MemoryUsage> compositeDataToMemoryUsage() {
        return new Function<CompositeData, MemoryUsage>() {
            @Override public MemoryUsage apply(CompositeData input) {
                return (input == null) ? null : MemoryUsage.from(input);
            }
        };
    }

    private static final AtomicBoolean initialized = new AtomicBoolean(false);

    /** Setup renderer hints for the MXBean attributes. */
    public static void init() {
        if (initialized.getAndSet(true)) return;

        RendererHints.register(UsesJavaMXBeans.USED_HEAP_MEMORY, RendererHints.displayValue(ByteSizeStrings.metric()));
        RendererHints.register(UsesJavaMXBeans.INIT_HEAP_MEMORY, RendererHints.displayValue(ByteSizeStrings.metric()));
        RendererHints.register(UsesJavaMXBeans.MAX_HEAP_MEMORY, RendererHints.displayValue(ByteSizeStrings.metric()));
        RendererHints.register(UsesJavaMXBeans.COMMITTED_HEAP_MEMORY, RendererHints.displayValue(ByteSizeStrings.metric()));
        RendererHints.register(UsesJavaMXBeans.NON_HEAP_MEMORY_USAGE, RendererHints.displayValue(ByteSizeStrings.metric()));
        RendererHints.register(UsesJavaMXBeans.TOTAL_PHYSICAL_MEMORY_SIZE, RendererHints.displayValue(ByteSizeStrings.metric()));
        RendererHints.register(UsesJavaMXBeans.FREE_PHYSICAL_MEMORY_SIZE, RendererHints.displayValue(ByteSizeStrings.metric()));

        RendererHints.register(UsesJavaMXBeans.START_TIME, RendererHints.displayValue(Time.toDateString()));
        RendererHints.register(UsesJavaMXBeans.UP_TIME, RendererHints.displayValue(Duration.millisToStringRounded()));
        RendererHints.register(UsesJavaMXBeans.PROCESS_CPU_TIME, RendererHints.displayValue(Duration.millisToStringRounded()));
        RendererHints.register(UsesJavaMXBeans.PROCESS_CPU_TIME_FRACTION_LAST, RendererHints.displayValue(MathFunctions.percent(4)));
        RendererHints.register(UsesJavaMXBeans.PROCESS_CPU_TIME_FRACTION_IN_WINDOW, RendererHints.displayValue(MathFunctions.percent(4)));
    }

    static {
        init();
    }
}
