/*
 * Copyright 2016 the original author or authors.
 *
 * Licensed 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.gradle.launcher.daemon.server.health.gc;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.gradle.api.Transformer;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.specs.Spec;
import org.gradle.util.CollectionUtils;

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class GarbageCollectionMonitor {
    public static final int POLL_INTERVAL_SECONDS = 1;
    private static final int POLL_DELAY_SECONDS = 1;
    private static final int EVENT_WINDOW = 20;
    private static final Logger LOGGER = Logging.getLogger(GarbageCollectionMonitor.class);
    private final Map<String, SlidingWindow<GarbageCollectionEvent>> events;
    private final GarbageCollectorMonitoringStrategy gcStrategy;
    private final ScheduledExecutorService pollingExecutor;

    public GarbageCollectionMonitor(ScheduledExecutorService pollingExecutor) {
        this(determineGcStrategy(), pollingExecutor);
    }

    public GarbageCollectionMonitor(GarbageCollectorMonitoringStrategy gcStrategy, ScheduledExecutorService pollingExecutor) {
        this.pollingExecutor = pollingExecutor;
        this.gcStrategy = gcStrategy;

        if (gcStrategy != GarbageCollectorMonitoringStrategy.UNKNOWN) {
            events = ImmutableMap.<String, SlidingWindow<GarbageCollectionEvent>>of(
                gcStrategy.getTenuredPoolName(), new DefaultSlidingWindow<GarbageCollectionEvent>(EVENT_WINDOW),
                gcStrategy.getPermGenPoolName(), new DefaultSlidingWindow<GarbageCollectionEvent>(EVENT_WINDOW)
            );
            pollForValues(gcStrategy.getGarbageCollectorName(), ImmutableList.copyOf(events.keySet()));
        } else {
            events = ImmutableMap.<String, SlidingWindow<GarbageCollectionEvent>>builder().build();
        }
    }

    private static GarbageCollectorMonitoringStrategy determineGcStrategy() {
        JVMStrategy jvmStrategy = JVMStrategy.current();

        final List<String> garbageCollectors = CollectionUtils.collect(ManagementFactory.getGarbageCollectorMXBeans(), new Transformer<String, GarbageCollectorMXBean>() {
            @Override
            public String transform(GarbageCollectorMXBean garbageCollectorMXBean) {
                return garbageCollectorMXBean.getName();
            }
        });
        GarbageCollectorMonitoringStrategy gcStrategy = CollectionUtils.findFirst(jvmStrategy.getGcStrategies(), new Spec<GarbageCollectorMonitoringStrategy>() {
            @Override
            public boolean isSatisfiedBy(GarbageCollectorMonitoringStrategy strategy) {
                return garbageCollectors.contains(strategy.getGarbageCollectorName());
            }
        });

        if (gcStrategy == null) {
            LOGGER.info("Unable to determine a garbage collection monitoring strategy for " + jvmStrategy.toString());
            return GarbageCollectorMonitoringStrategy.UNKNOWN;
        } else {
            return gcStrategy;
        }
    }

    private void pollForValues(String garbageCollectorName, List<String> memoryPoolNames) {
        pollingExecutor.scheduleAtFixedRate(new GarbageCollectionCheck(events, memoryPoolNames, garbageCollectorName), POLL_DELAY_SECONDS, POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
    }

    public GarbageCollectionStats getTenuredStats() {
        return getGarbageCollectionStatsWithEmptyDefault(gcStrategy.getTenuredPoolName());
    }

    public GarbageCollectionStats getPermGenStats() {
        return getGarbageCollectionStatsWithEmptyDefault(gcStrategy.getPermGenPoolName());
    }

    private GarbageCollectionStats getGarbageCollectionStatsWithEmptyDefault(final String memoryPoolName) {
        SlidingWindow<GarbageCollectionEvent> slidingWindow;
        if ((memoryPoolName == null) || events.get(memoryPoolName) == null) { // events has no entries on UNKNOWN
            slidingWindow = new DefaultSlidingWindow<GarbageCollectionEvent>(EVENT_WINDOW);
        } else {
            slidingWindow = events.get(memoryPoolName);
        }
        return new GarbageCollectionStats(slidingWindow.snapshot());
    }

    public GarbageCollectorMonitoringStrategy getGcStrategy() {
        return gcStrategy;
    }

    public enum JVMStrategy {
        IBM(GarbageCollectorMonitoringStrategy.IBM_ALL),
        ORACLE_HOTSPOT(GarbageCollectorMonitoringStrategy.ORACLE_PARALLEL_CMS,
                        GarbageCollectorMonitoringStrategy.ORACLE_SERIAL,
                        GarbageCollectorMonitoringStrategy.ORACLE_6_CMS,
                        GarbageCollectorMonitoringStrategy.ORACLE_G1),
        UNSUPPORTED();

        final GarbageCollectorMonitoringStrategy[] gcStrategies;

        JVMStrategy(GarbageCollectorMonitoringStrategy... gcStrategies) {
            this.gcStrategies = gcStrategies;
        }

        static JVMStrategy current() {
            String vmname = System.getProperty("java.vm.name");

            if (vmname.equals("IBM J9 VM")) {
                return IBM;
            }

            if (vmname.startsWith("Java HotSpot(TM)")) {
                return ORACLE_HOTSPOT;
            }

            return UNSUPPORTED;
        }

        public GarbageCollectorMonitoringStrategy[] getGcStrategies() {
            return gcStrategies;
        }

        @Override
        public String toString() {
            return "JVMStrategy{" + System.getProperty("java.vendor") + " version " + System.getProperty("java.version") + "}";
        }
    }
}
