// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.util;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.Consumer;
import com.intellij.util.SystemProperties;
import com.intellij.util.concurrency.SequentialTaskExecutor;
import org.jetbrains.annotations.NotNull;

import javax.management.Notification;
import javax.management.NotificationEmitter;
import javax.management.NotificationListener;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryNotificationInfo;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryType;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;

public final class LowMemoryWatcherManager implements Disposable {
  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.LowMemoryWatcherManager");

  private static final long MEM_THRESHOLD = 5 /*MB*/ * 1024 * 1024;
  @NotNull private final ExecutorService myExecutorService;

  private Future<?> mySubmitted; // guarded by ourJanitor
  private final Future<?> myMemoryPoolMXBeansFuture;
  private final AtomicBoolean myProcessing = new AtomicBoolean();
  private final Consumer<Boolean> myJanitor = new Consumer<Boolean>() {
    @Override
    public void consume(@NotNull Boolean afterGc) {
      // null mySubmitted before all listeners called to avoid data race when listener added in the middle of the execution and is lost
      // this may however cause listeners to execute more than once (potentially even in parallel)
      synchronized (myJanitor) {
        mySubmitted = null;
      }
      LowMemoryWatcher.onLowMemorySignalReceived(afterGc);
    }
  };

  public LowMemoryWatcherManager(@NotNull ExecutorService backendExecutorService) {
    myExecutorService = SequentialTaskExecutor.createSequentialApplicationPoolExecutor("LowMemoryWatcherManager", backendExecutorService);

    myMemoryPoolMXBeansFuture = initializeMXBeanListenersLater(backendExecutorService);
  }

  @NotNull
  private Future<?> initializeMXBeanListenersLater(@NotNull ExecutorService backendExecutorService) {
    // do it in the other thread to get it out of the way during startup
    return backendExecutorService.submit(() -> {
      try {
        for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
          if (bean.getType() == MemoryType.HEAP && bean.isCollectionUsageThresholdSupported() && bean.isUsageThresholdSupported()) {
            long max = bean.getUsage().getMax();
            long threshold = Math.min((long)(max * getOccupiedMemoryThreshold()), max - MEM_THRESHOLD);
            if (threshold > 0) {
              bean.setUsageThreshold(threshold);
              bean.setCollectionUsageThreshold(threshold);
            }
          }
        }
        ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).addNotificationListener(myLowMemoryListener, null, null);
      }
      catch (Throwable e) {
        // should not happen normally
        LOG.info("Errors initializing LowMemoryWatcher: ", e);
      }
    });
  }

  private final NotificationListener myLowMemoryListener = new NotificationListener() {
    @Override
    public void handleNotification(Notification notification, Object __) {
      boolean memoryThreshold = MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED.equals(notification.getType());
      boolean memoryCollectionThreshold = MemoryNotificationInfo.MEMORY_COLLECTION_THRESHOLD_EXCEEDED.equals(notification.getType());

      if (memoryThreshold || memoryCollectionThreshold) {
        final boolean afterGc = memoryCollectionThreshold;

        // whether LowMemoryWatcher runnables should be executed on the same thread that the low memory events come
        if (SystemProperties.getBooleanProperty("low.memory.watcher.sync", false)) {
          handleEventImmediately(afterGc);
          return;
        }

        synchronized (myJanitor) {
          if (mySubmitted == null) {
            mySubmitted = myExecutorService.submit(() -> myJanitor.consume(afterGc));
          }
        }
      }
    }
  };

  private static float getOccupiedMemoryThreshold() {
    return SystemProperties.getFloatProperty("low.memory.watcher.notification.threshold", 0.95f);
  }

  private void handleEventImmediately(boolean afterGc) {
    if (myProcessing.compareAndSet(false, true)) {
      try {
        myJanitor.consume(afterGc);
      }
      finally {
        myProcessing.set(false);
      }
    }
  }

  @Override
  public void dispose() {
    try {
      myMemoryPoolMXBeansFuture.get();
      ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).removeNotificationListener(myLowMemoryListener);
    }
    catch (Exception e) {
      LOG.error(e);
    }
    synchronized (myJanitor) {
      if (mySubmitted != null) {
        mySubmitted.cancel(false);
        mySubmitted = null;
      }
    }

    LowMemoryWatcher.stopAll();
  }
}

@@ -253,24 +253,66 @@
 tic.Logger;%0A
+import com.intellij.util.ConcurrencyUtil;%0A
 import com.i
@@ -823,17 +823,31 @@
 current.
-*
+ExecutorService
 ;%0Aimport
@@ -872,28 +872,14 @@
 ent.
-atomic.AtomicBoolean
+Future
 ;%0A%0Ap
@@ -1299,74 +1299,8 @@
 re;%0A
-  private final AtomicBoolean myProcessing = new AtomicBoolean();%0A
   pr
@@ -1900,27 +1900,272 @@
     
-myExecutorService =
+// whether LowMemoryWatcher runnables should be executed on the same thread that the low memory events come%0A    myExecutorService = SystemProperties.getBooleanProperty(%22low.memory.watcher.sync%22, false) ?%0A      ConcurrencyUtil.newSameThreadExecutorService() :%0A     
  Seq
@@ -3939,368 +3939,244 @@
     
-final boolean afterGc = memoryCollectionThreshold;%0A%0A        // whether LowMemoryWatcher runnables should be executed on the same thread that the low memory events come%0A        if (SystemProperties.getBooleanProperty(%22low.memory.watcher.sync%22, false)) %7B%0A          handleEventImmediately(afterGc);%0A          return;%0A        %7D%0A%0A        synchronized (myJanitor) %7B%0A
+synchronized (myJanitor) %7B%0A          if (mySubmitted == null) %7B%0A            mySubmitted = myExecutorService.submit(() -%3E myJanitor.consume(memoryCollectionThreshold));%0A            // maybe it's executed too fast or even synchronously%0A  
     
@@ -4192,32 +4192,33 @@
 (mySubmitted
- == null
+.isDone()
 ) %7B%0A        
@@ -4213,32 +4213,34 @@
 ) %7B%0A            
+ 
+ 
 mySubmitted = my
@@ -4241,67 +4241,27 @@
 d = 
-myExecutorService.submit(() -%3E myJanitor.consume(afterGc));
+null;%0A            %7D
 %0A   
@@ -4460,240 +4460,8 @@
  %7D%0A%0A
-  private void handleEventImmediately(boolean afterGc) %7B%0A    if (myProcessing.compareAndSet(false, true)) %7B%0A      try %7B%0A        myJanitor.consume(afterGc);%0A      %7D%0A      finally %7B%0A        myProcessing.set(false);%0A      %7D%0A    %7D%0A  %7D%0A%0A
   @O

simplify code with the help of SameThreadExecutor