/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.devsupport;

import javax.annotation.Nullable;

import java.io.File;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;

// This module is being called only by Java via the static method "captureHeap" that
// requires it to alreay be initialized, thus we eagerly initialize this module
@ReactModule(name = "JSCHeapCapture", needsEagerInit = true)
public class JSCHeapCapture extends ReactContextBaseJavaModule {
  public interface HeapCapture extends JavaScriptModule {
    void captureHeap(String path);
  }

  public static class CaptureException extends Exception {
    CaptureException(String message) {
      super(message);
    }
    CaptureException(String message, Throwable cause) {
      super(message, cause);
    }
  }

  public interface CaptureCallback {
    void onComplete(List<File> captures, List<CaptureException> failures);
  }

  private interface PerCaptureCallback {
    void onSuccess(File capture);
    void onFailure(CaptureException cause);
  }

  private @Nullable HeapCapture mHeapCapture;
  private @Nullable PerCaptureCallback mCaptureInProgress;

  private static final HashSet<JSCHeapCapture> sRegisteredDumpers = new HashSet<>();

  private static synchronized void registerHeapCapture(JSCHeapCapture dumper) {
    if (sRegisteredDumpers.contains(dumper)) {
      throw new RuntimeException(
        "a JSCHeapCapture registered more than once");
    }
    sRegisteredDumpers.add(dumper);
  }

  private static synchronized void unregisterHeapCapture(JSCHeapCapture dumper) {
    sRegisteredDumpers.remove(dumper);
  }

  public static synchronized void captureHeap(String path, final CaptureCallback callback) {
    final LinkedList<File> captureFiles = new LinkedList<>();
    final LinkedList<CaptureException> captureFailures = new LinkedList<>();

    if (sRegisteredDumpers.isEmpty()) {
      captureFailures.add(new CaptureException("No JSC registered"));
      callback.onComplete(captureFiles, captureFailures);
      return;
    }

    int disambiguate = 0;
    File f = new File(path + "/capture" + Integer.toString(disambiguate) + ".json");
    while (f.delete()) {
      disambiguate++;
      f = new File(path + "/capture" + Integer.toString(disambiguate) + ".json");
    }

    final int numRegisteredDumpers = sRegisteredDumpers.size();
    disambiguate = 0;
    for (JSCHeapCapture dumper : sRegisteredDumpers) {
      File file = new File(path + "/capture" + Integer.toString(disambiguate) + ".json");
      dumper.captureHeapHelper(file, new PerCaptureCallback() {
        @Override
        public void onSuccess(File capture) {
          captureFiles.add(capture);
          if (captureFiles.size() + captureFailures.size() == numRegisteredDumpers) {
            callback.onComplete(captureFiles, captureFailures);
          }
        }
        @Override
        public void onFailure(CaptureException cause) {
          captureFailures.add(cause);
          if (captureFiles.size() + captureFailures.size() == numRegisteredDumpers) {
            callback.onComplete(captureFiles, captureFailures);
          }
        }
      });
    }
  }

  public JSCHeapCapture(ReactApplicationContext reactContext) {
    super(reactContext);
    mHeapCapture = null;
    mCaptureInProgress = null;
  }

  private synchronized void captureHeapHelper(File file, PerCaptureCallback callback) {
    if (mHeapCapture == null) {
      callback.onFailure(new CaptureException("HeapCapture.js module not connected"));
      return;
    }
    if (mCaptureInProgress != null) {
      callback.onFailure(new CaptureException("Heap capture already in progress"));
      return;
    }
    mCaptureInProgress = callback;
    mHeapCapture.captureHeap(file.getPath());
  }

  @ReactMethod
  public synchronized void captureComplete(String path, String error) {
    if (mCaptureInProgress != null) {
      if (error == null) {
        mCaptureInProgress.onSuccess(new File(path));
      } else {
        mCaptureInProgress.onFailure(new CaptureException(error));
      }
      mCaptureInProgress = null;
    }
  }

  @Override
  public String getName() {
    return "JSCHeapCapture";
  }

  @Override
  public void initialize() {
    super.initialize();
    mHeapCapture = getReactApplicationContext().getJSModule(HeapCapture.class);
    registerHeapCapture(this);
  }

  @Override
  public void onCatalystInstanceDestroy() {
    super.onCatalystInstanceDestroy();
    unregisterHeapCapture(this);
    mHeapCapture = null;
  }
}
