/*
 * 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.process.internal.health.memory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.gradle.internal.concurrent.ExecutorFactory;
import org.gradle.internal.concurrent.Stoppable;
import org.gradle.internal.concurrent.StoppableScheduledExecutor;
import org.gradle.internal.event.ListenerManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class DefaultMemoryManager implements MemoryManager, Stoppable {
    private static final Logger LOGGER = LoggerFactory.getLogger(MemoryManager.class);
    public static final int STATUS_INTERVAL_SECONDS = 5;
    private static final double DEFAULT_MIN_FREE_MEMORY_PERCENTAGE = 0.1D; // 10%
    private static final long MIN_THRESHOLD_BYTES = 384 * 1024 * 1024; // 384M

    private final double minFreeMemoryPercentage;
    private final OsMemoryInfo osMemoryInfo;
    private final JvmMemoryInfo jvmMemoryInfo;
    private final ListenerManager listenerManager;
    private final StoppableScheduledExecutor scheduler;
    private final JvmMemoryStatusListener jvmBroadcast;
    private final OsMemoryStatusListener osBroadcast;
    private final boolean osMemoryStatusSupported;
    private final Object holdersLock = new Object();
    private final Object memoryLock = new Object();
    private final List<MemoryHolder> holders = new ArrayList<MemoryHolder>();
    private OsMemoryStatus currentOsMemoryStatus;
    private final OsMemoryStatusListener osMemoryStatusListener;

    public DefaultMemoryManager(OsMemoryInfo osMemoryInfo, JvmMemoryInfo jvmMemoryInfo, ListenerManager listenerManager, ExecutorFactory executorFactory) {
        this(osMemoryInfo, jvmMemoryInfo, listenerManager, executorFactory, DEFAULT_MIN_FREE_MEMORY_PERCENTAGE, true);
    }

    @VisibleForTesting
    DefaultMemoryManager(OsMemoryInfo osMemoryInfo, JvmMemoryInfo jvmMemoryInfo, ListenerManager listenerManager, ExecutorFactory executorFactory, double minFreeMemoryPercentage, boolean autoFree) {
        Preconditions.checkArgument(minFreeMemoryPercentage >= 0, "Free memory percentage must be >= 0");
        Preconditions.checkArgument(minFreeMemoryPercentage <= 1, "Free memory percentage must be <= 1");
        this.minFreeMemoryPercentage = minFreeMemoryPercentage;
        this.osMemoryInfo = osMemoryInfo;
        this.jvmMemoryInfo = jvmMemoryInfo;
        this.listenerManager = listenerManager;
        this.scheduler = executorFactory.createScheduled("Memory manager", 1);
        this.jvmBroadcast = listenerManager.getBroadcaster(JvmMemoryStatusListener.class);
        this.osBroadcast = listenerManager.getBroadcaster(OsMemoryStatusListener.class);
        this.osMemoryStatusSupported = supportsOsMemoryStatus();
        this.osMemoryStatusListener = new OsMemoryListener(autoFree);
        start();
    }

    private boolean supportsOsMemoryStatus() {
        try {
            osMemoryInfo.getOsSnapshot();
            return true;
        } catch (UnsupportedOperationException ex) {
            return false;
        }
    }

    private void start() {
        scheduler.scheduleAtFixedRate(new MemoryCheck(), 0, STATUS_INTERVAL_SECONDS, TimeUnit.SECONDS);
        LOGGER.debug("Memory status broadcaster started");
        if (osMemoryStatusSupported) {
            addListener(osMemoryStatusListener);
        } else {
            LOGGER.info("This JVM does not support getting OS memory, so no OS memory status updates will be broadcast");
        }
    }

    @Override
    public void stop() {
        scheduler.stop();
        listenerManager.removeListener(osMemoryStatusListener);
    }

    @Override
    public void requestFreeMemory(long memoryAmountBytes) {
        synchronized (memoryLock) {
            if (currentOsMemoryStatus != null) {
                long totalPhysicalMemory = currentOsMemoryStatus.getTotalPhysicalMemory();
                long requestedFreeMemory = getMemoryThresholdInBytes(totalPhysicalMemory) + (memoryAmountBytes > 0 ? memoryAmountBytes : 0);
                long freeMemory = currentOsMemoryStatus.getFreePhysicalMemory();
                long newFreeMemory = doRequestFreeMemory(requestedFreeMemory, freeMemory);
                // If we've freed memory, invalidate the current OS memory snapshot
                if (newFreeMemory > freeMemory) {
                    currentOsMemoryStatus = null;
                }
            } else {
                LOGGER.debug("There is no current snapshot of OS memory available - memory cannot be freed until a new memory status update occurs");
            }
        }
    }

    private long doRequestFreeMemory(long requestedFreeMemory, long freeMemory) {
        long toReleaseMemory = requestedFreeMemory;
        if (freeMemory < requestedFreeMemory) {
            LOGGER.debug("{} memory requested, {} free", requestedFreeMemory, freeMemory);
            synchronized (holdersLock) {
                for (MemoryHolder holder : holders) {
                    long released = holder.attemptToRelease(toReleaseMemory);
                    toReleaseMemory -= released;
                    freeMemory += released;
                    if (freeMemory >= requestedFreeMemory) {
                        break;
                    }
                }
            }
            LOGGER.debug("{} memory requested, {} released, {} free", requestedFreeMemory, requestedFreeMemory - toReleaseMemory, freeMemory);
        }
        return freeMemory;
    }

    private long getMemoryThresholdInBytes(long totalPhysicalMemory) {
        return Math.max(MIN_THRESHOLD_BYTES, (long) (totalPhysicalMemory * minFreeMemoryPercentage));
    }

    private class MemoryCheck implements Runnable {
        @Override
        public void run() {
            try {
                if (osMemoryStatusSupported) {
                    OsMemoryStatus os = osMemoryInfo.getOsSnapshot();
                    LOGGER.debug("Emitting OS memory status event {}", os);
                    osBroadcast.onOsMemoryStatus(os);
                }
                JvmMemoryStatus jvm = jvmMemoryInfo.getJvmSnapshot();
                LOGGER.debug("Emitting JVM memory status event {}", jvm);
                jvmBroadcast.onJvmMemoryStatus(jvm);
            } catch (Exception ex) {
                LOGGER.debug("Failed to collect memory status: {}", ex.getMessage(), ex);
            }
        }
    }

    private class OsMemoryListener implements OsMemoryStatusListener {
        private final boolean autoFree;

        private OsMemoryListener(boolean autoFree) {
            this.autoFree = autoFree;
        }

        @Override
        public void onOsMemoryStatus(OsMemoryStatus os) {
            currentOsMemoryStatus = os;
            if (autoFree) {
                requestFreeMemory(0);
            }
        }
    }

    @Override
    public void addMemoryHolder(MemoryHolder holder) {
        holders.add(holder);
    }

    @Override
    public void removeMemoryHolder(MemoryHolder holder) {
        holders.remove(holder);
    }

    @Override
    public void addListener(JvmMemoryStatusListener listener) {
        listenerManager.addListener(listener);
    }

    @Override
    public void addListener(OsMemoryStatusListener listener) {
        listenerManager.addListener(listener);
    }

    @Override
    public void removeListener(JvmMemoryStatusListener listener) {
        listenerManager.removeListener(listener);
    }

    @Override
    public void removeListener(OsMemoryStatusListener listener) {
        listenerManager.removeListener(listener);
    }
}
