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

import javax.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.app.Application;
import android.content.Intent;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.cxxbridge.JSBundleLoader;
import com.facebook.react.devsupport.DevSupportManager;
import com.facebook.react.devsupport.RedBoxHandler;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.uimanager.UIImplementationProvider;
import com.facebook.react.uimanager.ViewManager;

/**
 * This class is managing instances of {@link CatalystInstance}. It exposes a way to configure
 * catalyst instance using {@link ReactPackage} and keeps track of the lifecycle of that
 * instance. It also sets up connection between the instance and developers support functionality
 * of the framework.
 *
 * An instance of this manager is required to start JS application in {@link ReactRootView} (see
 * {@link ReactRootView#startReactApplication} for more info).
 *
 * The lifecycle of the instance of {@link ReactInstanceManager} should be bound to the activity
 * that owns the {@link ReactRootView} that is used to render react application using this
 * instance manager (see {@link ReactRootView#startReactApplication}). It's required to pass
 * owning activity's lifecycle events to the instance manager (see {@link #onHostPause},
 * {@link #onHostDestroy} and {@link #onHostResume}).
 *
 * Ideally, this would be an interface, but because of the API used by earlier versions, it has to
 * have a static method, and so cannot (in Java < 8), be one.
 */
public abstract class ReactInstanceManager {

  /**
   * Listener interface for react instance events.
   */
  public interface ReactInstanceEventListener {
    /**
     * Called when the react context is initialized (all modules registered). Always called on the
     * UI thread.
     */
    void onReactContextInitialized(ReactContext context);
  }

  public abstract DevSupportManager getDevSupportManager();

  public abstract MemoryPressureRouter getMemoryPressureRouter();

  /**
   * Trigger react context initialization asynchronously in a background async task. This enables
   * applications to pre-load the application JS, and execute global code before
   * {@link ReactRootView} is available and measured. This should only be called the first time the
   * application is set up, which is enforced to keep developers from accidentally creating their
   * application multiple times without realizing it.
   *
   * Called from UI thread.
   */
  public abstract void createReactContextInBackground();

  /**
   * @return whether createReactContextInBackground has been called. Will return false after
   * onDestroy until a new initial context has been created.
   */
  public abstract boolean hasStartedCreatingInitialContext();

  /**
   * This method will give JS the opportunity to consume the back button event. If JS does not
   * consume the event, mDefaultBackButtonImpl will be invoked at the end of the round trip to JS.
   */
  public abstract void onBackPressed();

  /**
   * This method will give JS the opportunity to receive intents via Linking.
   */
  public abstract void onNewIntent(Intent intent);

  /**
   * Call this from {@link Activity#onPause()}. This notifies any listening modules so they can do
   * any necessary cleanup.
   *
   * @deprecated Use {@link #onHostPause(Activity)} instead.
   */
  @Deprecated
  public abstract void onHostPause();

  /**
   * Call this from {@link Activity#onPause()}. This notifies any listening modules so they can do
   * any necessary cleanup. The passed Activity is the current Activity being paused. This will
   * always be the foreground activity that would be returned by
   * {@link ReactContext#getCurrentActivity()}.
   *
   * @param activity the activity being paused
   */
  public abstract void onHostPause(Activity activity);

  /**
   * Use this method when the activity resumes to enable invoking the back button directly from JS.
   *
   * This method retains an instance to provided mDefaultBackButtonImpl. Thus it's
   * important to pass from the activity instance that owns this particular instance of {@link
   * ReactInstanceManager}, so that once this instance receive {@link #onHostDestroy} event it will
   * clear the reference to that defaultBackButtonImpl.
   *
   * @param defaultBackButtonImpl a {@link DefaultHardwareBackBtnHandler} from an Activity that owns
   * this instance of {@link ReactInstanceManager}.
   */
  public abstract void onHostResume(
    Activity activity,
    DefaultHardwareBackBtnHandler defaultBackButtonImpl);

  /**
   * Call this from {@link Activity#onDestroy()}. This notifies any listening modules so they can do
   * any necessary cleanup.
   *
   * @deprecated use {@link #onHostDestroy(Activity)} instead
   */
  @Deprecated
  public abstract void onHostDestroy();

  /**
   * Call this from {@link Activity#onDestroy()}. This notifies any listening modules so they can do
   * any necessary cleanup. If the activity being destroyed is not the current activity, no modules
   * are notified.
   *
   * @param activity the activity being destroyed
   */
  public abstract void onHostDestroy(Activity activity);

  public abstract void onActivityResult(
    Activity activity,
    int requestCode,
    int resultCode,
    Intent data);
  public abstract void showDevOptionsDialog();

  /**
   * Get the URL where the last bundle was loaded from.
   */
  public abstract String getSourceUrl();

  /**
   * The JS Bundle file that this Instance Manager was constructed with.
   */
  public abstract @Nullable String getJSBundleFile();

  /**
   * Attach given {@param rootView} to a catalyst instance manager and start JS application using
   * JS module provided by {@link ReactRootView#getJSModuleName}. If the react context is currently
   * being (re)-created, or if react context has not been created yet, the JS application associated
   * with the provided root view will be started asynchronously, i.e this method won't block.
   * This view will then be tracked by this manager and in case of catalyst instance restart it will
   * be re-attached.
   */
  public abstract void attachMeasuredRootView(ReactRootView rootView);

  /**
   * Detach given {@param rootView} from current catalyst instance. It's safe to call this method
   * multiple times on the same {@param rootView} - in that case view will be detached with the
   * first call.
   */
  public abstract void detachRootView(ReactRootView rootView);

  /**
   * Destroy this React instance and the attached JS context.
   */
  public abstract void destroy();

  /**
   * Uses configured {@link ReactPackage} instances to create all view managers
   */
  public abstract List<ViewManager> createAllViewManagers(
    ReactApplicationContext catalystApplicationContext);

  /**
   * Add a listener to be notified of react instance events.
   */
  public abstract void addReactInstanceEventListener(ReactInstanceEventListener listener);

  /**
   * Remove a listener previously added with {@link #addReactInstanceEventListener}.
   */
  public abstract void removeReactInstanceEventListener(ReactInstanceEventListener listener);

  @VisibleForTesting
  public abstract @Nullable ReactContext getCurrentReactContext();

  public abstract LifecycleState getLifecycleState();

  /**
   * Creates a builder that is capable of creating an instance of {@link ReactInstanceManagerImpl}.
   */
  public static Builder builder() {
    return new Builder();
  }

  /**
   * Builder class for {@link ReactInstanceManagerImpl}
   */
  public static class Builder {

    protected final List<ReactPackage> mPackages = new ArrayList<>();

    protected @Nullable String mJSBundleAssetUrl;
    protected @Nullable JSBundleLoader mJSBundleLoader;
    protected @Nullable String mJSMainModuleName;
    protected @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener;
    protected @Nullable Application mApplication;
    protected boolean mUseDeveloperSupport;
    protected @Nullable LifecycleState mInitialLifecycleState;
    protected @Nullable UIImplementationProvider mUIImplementationProvider;
    protected @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler;
    protected JSCConfig mJSCConfig = JSCConfig.EMPTY;
    protected @Nullable Activity mCurrentActivity;
    protected @Nullable DefaultHardwareBackBtnHandler mDefaultHardwareBackBtnHandler;
    protected @Nullable RedBoxHandler mRedBoxHandler;
    protected boolean mLazyNativeModulesEnabled;
    protected boolean mLazyViewManagersEnabled;

    protected Builder() {
    }

    /**
     * Sets a provider of {@link UIImplementation}.
     * Uses default provider if null is passed.
     */
    public Builder setUIImplementationProvider(
        @Nullable UIImplementationProvider uiImplementationProvider) {
      mUIImplementationProvider = uiImplementationProvider;
      return this;
    }

    /**
     * Name of the JS bundle file to be loaded from application's raw assets.
     * Example: {@code "index.android.js"}
     */
    public Builder setBundleAssetName(String bundleAssetName) {
      mJSBundleAssetUrl = (bundleAssetName == null ? null : "assets://" + bundleAssetName);
      mJSBundleLoader = null;
      return this;
    }

    /**
     * Path to the JS bundle file to be loaded from the file system.
     *
     * Example: {@code "assets://index.android.js" or "/sdcard/main.jsbundle"}
     */
    public Builder setJSBundleFile(String jsBundleFile) {
      if (jsBundleFile.startsWith("assets://")) {
        mJSBundleAssetUrl = jsBundleFile;
        mJSBundleLoader = null;
        return this;
      }
      return setJSBundleLoader(JSBundleLoader.createFileLoader(jsBundleFile));
    }

    /**
     * Bundle loader to use when setting up JS environment. This supersedes
     * prior invcations of {@link setJSBundleFile} and {@link setBundleAssetName}.
     *
     * Example: {@code JSBundleLoader.createFileLoader(application, bundleFile)}
     */
    public Builder setJSBundleLoader(JSBundleLoader jsBundleLoader) {
      mJSBundleLoader = jsBundleLoader;
      mJSBundleAssetUrl = null;
      return this;
    }

    /**
     * Path to your app's main module on the packager server. This is used when
     * reloading JS during development. All paths are relative to the root folder
     * the packager is serving files from.
     * Examples:
     * {@code "index.android"} or
     * {@code "subdirectory/index.android"}
     */
    public Builder setJSMainModuleName(String jsMainModuleName) {
      mJSMainModuleName = jsMainModuleName;
      return this;
    }

    public Builder addPackage(ReactPackage reactPackage) {
      mPackages.add(reactPackage);
      return this;
    }

    public Builder setBridgeIdleDebugListener(
        NotThreadSafeBridgeIdleDebugListener bridgeIdleDebugListener) {
      mBridgeIdleDebugListener = bridgeIdleDebugListener;
      return this;
    }

    /**
     * Required. This must be your {@code Application} instance.
     */
    public Builder setApplication(Application application) {
      mApplication = application;
      return this;
    }

    public Builder setCurrentActivity(Activity activity) {
      mCurrentActivity = activity;
      return this;
    }

    public Builder setDefaultHardwareBackBtnHandler(
        DefaultHardwareBackBtnHandler defaultHardwareBackBtnHandler) {
      mDefaultHardwareBackBtnHandler = defaultHardwareBackBtnHandler;
      return this;
    }

    /**
     * When {@code true}, developer options such as JS reloading and debugging are enabled.
     * Note you still have to call {@link #showDevOptionsDialog} to show the dev menu,
     * e.g. when the device Menu button is pressed.
     */
    public Builder setUseDeveloperSupport(boolean useDeveloperSupport) {
      mUseDeveloperSupport = useDeveloperSupport;
      return this;
    }

    /**
     * Sets the initial lifecycle state of the host. For example, if the host is already resumed at
     * creation time, we wouldn't expect an onResume call until we get an onPause call.
     */
    public Builder setInitialLifecycleState(LifecycleState initialLifecycleState) {
      mInitialLifecycleState = initialLifecycleState;
      return this;
    }

    /**
     * Set the exception handler for all native module calls. If not set, the default
     * {@link DevSupportManager} will be used, which shows a redbox in dev mode and rethrows
     * (crashes the app) in prod mode.
     */
    public Builder setNativeModuleCallExceptionHandler(NativeModuleCallExceptionHandler handler) {
      mNativeModuleCallExceptionHandler = handler;
      return this;
    }

    public Builder setJSCConfig(JSCConfig jscConfig) {
      mJSCConfig = jscConfig;
      return this;
    }

    public Builder setRedBoxHandler(@Nullable RedBoxHandler redBoxHandler) {
      mRedBoxHandler = redBoxHandler;
      return this;
    }

    public Builder setLazyNativeModulesEnabled(boolean lazyNativeModulesEnabled) {
      mLazyNativeModulesEnabled = lazyNativeModulesEnabled;
      return this;
    }

    public Builder setLazyViewManagersEnabled(boolean lazyViewManagersEnabled) {
      mLazyViewManagersEnabled = lazyViewManagersEnabled;
      return this;
    }

    /**
     * Instantiates a new {@link ReactInstanceManagerImpl}.
     * Before calling {@code build}, the following must be called:
     * <ul>
     * <li> {@link #setApplication}
     * <li> {@link #setCurrentActivity} if the activity has already resumed
     * <li> {@link #setDefaultHardwareBackBtnHandler} if the activity has already resumed
     * <li> {@link #setJSBundleFile} or {@link #setJSMainModuleName}
     * </ul>
     */
    public ReactInstanceManager build() {
      Assertions.assertNotNull(
        mApplication,
        "Application property has not been set with this builder");

      Assertions.assertCondition(
        mUseDeveloperSupport || mJSBundleAssetUrl != null || mJSBundleLoader != null,
        "JS Bundle File or Asset URL has to be provided when dev support is disabled");

      Assertions.assertCondition(
        mJSMainModuleName != null || mJSBundleAssetUrl != null || mJSBundleLoader != null,
        "Either MainModuleName or JS Bundle File needs to be provided");

      if (mUIImplementationProvider == null) {
        // create default UIImplementationProvider if the provided one is null.
        mUIImplementationProvider = new UIImplementationProvider();
      }

      return new XReactInstanceManagerImpl(
        mApplication,
        mCurrentActivity,
        mDefaultHardwareBackBtnHandler,
        (mJSBundleLoader == null && mJSBundleAssetUrl != null) ?
          JSBundleLoader.createAssetLoader(mApplication, mJSBundleAssetUrl) : mJSBundleLoader,
        mJSMainModuleName,
        mPackages,
        mUseDeveloperSupport,
        mBridgeIdleDebugListener,
        Assertions.assertNotNull(mInitialLifecycleState, "Initial lifecycle state was not set"),
        mUIImplementationProvider,
        mNativeModuleCallExceptionHandler,
        mJSCConfig,
        mRedBoxHandler,
        mLazyNativeModulesEnabled,
        mLazyViewManagersEnabled);
    }
  }
}
