/*
 *  Copyright 2013 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.webrtc;

import android.content.Context;
import android.os.Process;
import androidx.annotation.Nullable;
import java.util.List;
import org.webrtc.Logging.Severity;
import org.webrtc.PeerConnection;
import org.webrtc.audio.AudioDeviceModule;
import org.webrtc.audio.JavaAudioDeviceModule;

/**
 * Java wrapper for a C++ PeerConnectionFactoryInterface.  Main entry point to
 * the PeerConnection API for clients.
 */
public class PeerConnectionFactory {
  public static final String TRIAL_ENABLED = "Enabled";
  @Deprecated public static final String VIDEO_FRAME_EMIT_TRIAL = "VideoFrameEmit";

  private static final String TAG = "PeerConnectionFactory";
  private static final String VIDEO_CAPTURER_THREAD_NAME = "VideoCapturerThread";

  /** Helper class holding both Java and C++ thread info. */
  private static class ThreadInfo {
    final Thread thread;
    final int tid;

    public static ThreadInfo getCurrent() {
      return new ThreadInfo(Thread.currentThread(), Process.myTid());
    }

    private ThreadInfo(Thread thread, int tid) {
      this.thread = thread;
      this.tid = tid;
    }
  }

  private static volatile boolean internalTracerInitialized;

  // Remove these once deprecated static printStackTrace() is gone.
  @Nullable private static ThreadInfo staticNetworkThread;
  @Nullable private static ThreadInfo staticWorkerThread;
  @Nullable private static ThreadInfo staticSignalingThread;

  private long nativeFactory;
  @Nullable private volatile ThreadInfo networkThread;
  @Nullable private volatile ThreadInfo workerThread;
  @Nullable private volatile ThreadInfo signalingThread;

  public static class InitializationOptions {
    final Context applicationContext;
    final String fieldTrials;
    final boolean enableInternalTracer;
    final NativeLibraryLoader nativeLibraryLoader;
    final String nativeLibraryName;
    @Nullable Loggable loggable;
    @Nullable Severity loggableSeverity;

    private InitializationOptions(Context applicationContext, String fieldTrials,
        boolean enableInternalTracer, NativeLibraryLoader nativeLibraryLoader,
        String nativeLibraryName, @Nullable Loggable loggable,
        @Nullable Severity loggableSeverity) {
      this.applicationContext = applicationContext;
      this.fieldTrials = fieldTrials;
      this.enableInternalTracer = enableInternalTracer;
      this.nativeLibraryLoader = nativeLibraryLoader;
      this.nativeLibraryName = nativeLibraryName;
      this.loggable = loggable;
      this.loggableSeverity = loggableSeverity;
    }

    public static Builder builder(Context applicationContext) {
      return new Builder(applicationContext);
    }

    public static class Builder {
      private final Context applicationContext;
      private String fieldTrials = "";
      private boolean enableInternalTracer;
      private NativeLibraryLoader nativeLibraryLoader = new NativeLibrary.DefaultLoader();
      private String nativeLibraryName = "jingle_peerconnection_so";
      @Nullable private Loggable loggable;
      @Nullable private Severity loggableSeverity;

      Builder(Context applicationContext) {
        this.applicationContext = applicationContext;
      }

      public Builder setFieldTrials(String fieldTrials) {
        this.fieldTrials = fieldTrials;
        return this;
      }

      public Builder setEnableInternalTracer(boolean enableInternalTracer) {
        this.enableInternalTracer = enableInternalTracer;
        return this;
      }

      public Builder setNativeLibraryLoader(NativeLibraryLoader nativeLibraryLoader) {
        this.nativeLibraryLoader = nativeLibraryLoader;
        return this;
      }

      public Builder setNativeLibraryName(String nativeLibraryName) {
        this.nativeLibraryName = nativeLibraryName;
        return this;
      }

      public Builder setInjectableLogger(Loggable loggable, Severity severity) {
        this.loggable = loggable;
        this.loggableSeverity = severity;
        return this;
      }

      public PeerConnectionFactory.InitializationOptions createInitializationOptions() {
        return new PeerConnectionFactory.InitializationOptions(applicationContext, fieldTrials,
            enableInternalTracer, nativeLibraryLoader, nativeLibraryName, loggable,
            loggableSeverity);
      }
    }
  }

  public static class Options {
    // Keep in sync with webrtc/rtc_base/network.h!
    //
    // These bit fields are defined for `networkIgnoreMask` below.
    static final int ADAPTER_TYPE_UNKNOWN = 0;
    static final int ADAPTER_TYPE_ETHERNET = 1 << 0;
    static final int ADAPTER_TYPE_WIFI = 1 << 1;
    static final int ADAPTER_TYPE_CELLULAR = 1 << 2;
    static final int ADAPTER_TYPE_VPN = 1 << 3;
    static final int ADAPTER_TYPE_LOOPBACK = 1 << 4;
    static final int ADAPTER_TYPE_ANY = 1 << 5;

    public int networkIgnoreMask;
    public boolean disableEncryption;
    public boolean disableNetworkMonitor;

    @CalledByNative("Options")
    int getNetworkIgnoreMask() {
      return networkIgnoreMask;
    }

    @CalledByNative("Options")
    boolean getDisableEncryption() {
      return disableEncryption;
    }

    @CalledByNative("Options")
    boolean getDisableNetworkMonitor() {
      return disableNetworkMonitor;
    }
  }

  public static class Builder {
    @Nullable private Options options;
    @Nullable private AudioDeviceModule audioDeviceModule;
    private AudioEncoderFactoryFactory audioEncoderFactoryFactory =
        new BuiltinAudioEncoderFactoryFactory();
    private AudioDecoderFactoryFactory audioDecoderFactoryFactory =
        new BuiltinAudioDecoderFactoryFactory();
    @Nullable private VideoEncoderFactory videoEncoderFactory;
    @Nullable private VideoDecoderFactory videoDecoderFactory;
    @Nullable private AudioProcessingFactory audioProcessingFactory;
    @Nullable private FecControllerFactoryFactoryInterface fecControllerFactoryFactory;
    @Nullable private NetworkControllerFactoryFactory networkControllerFactoryFactory;
    @Nullable private NetworkStatePredictorFactoryFactory networkStatePredictorFactoryFactory;
    @Nullable private NetEqFactoryFactory neteqFactoryFactory;

    private Builder() {}

    public Builder setOptions(Options options) {
      this.options = options;
      return this;
    }

    public Builder setAudioDeviceModule(AudioDeviceModule audioDeviceModule) {
      this.audioDeviceModule = audioDeviceModule;
      return this;
    }

    public Builder setAudioEncoderFactoryFactory(
        AudioEncoderFactoryFactory audioEncoderFactoryFactory) {
      if (audioEncoderFactoryFactory == null) {
        throw new IllegalArgumentException(
            "PeerConnectionFactory.Builder does not accept a null AudioEncoderFactoryFactory.");
      }
      this.audioEncoderFactoryFactory = audioEncoderFactoryFactory;
      return this;
    }

    public Builder setAudioDecoderFactoryFactory(
        AudioDecoderFactoryFactory audioDecoderFactoryFactory) {
      if (audioDecoderFactoryFactory == null) {
        throw new IllegalArgumentException(
            "PeerConnectionFactory.Builder does not accept a null AudioDecoderFactoryFactory.");
      }
      this.audioDecoderFactoryFactory = audioDecoderFactoryFactory;
      return this;
    }

    public Builder setVideoEncoderFactory(VideoEncoderFactory videoEncoderFactory) {
      this.videoEncoderFactory = videoEncoderFactory;
      return this;
    }

    public Builder setVideoDecoderFactory(VideoDecoderFactory videoDecoderFactory) {
      this.videoDecoderFactory = videoDecoderFactory;
      return this;
    }

    public Builder setAudioProcessingFactory(AudioProcessingFactory audioProcessingFactory) {
      if (audioProcessingFactory == null) {
        throw new NullPointerException(
            "PeerConnectionFactory builder does not accept a null AudioProcessingFactory.");
      }
      this.audioProcessingFactory = audioProcessingFactory;
      return this;
    }

    public Builder setFecControllerFactoryFactoryInterface(
        FecControllerFactoryFactoryInterface fecControllerFactoryFactory) {
      this.fecControllerFactoryFactory = fecControllerFactoryFactory;
      return this;
    }

    public Builder setNetworkControllerFactoryFactory(
        NetworkControllerFactoryFactory networkControllerFactoryFactory) {
      this.networkControllerFactoryFactory = networkControllerFactoryFactory;
      return this;
    }

    public Builder setNetworkStatePredictorFactoryFactory(
        NetworkStatePredictorFactoryFactory networkStatePredictorFactoryFactory) {
      this.networkStatePredictorFactoryFactory = networkStatePredictorFactoryFactory;
      return this;
    }

    /**
     * Sets a NetEqFactoryFactory for the PeerConnectionFactory. When using a
     * custom NetEqFactoryFactory, the AudioDecoderFactoryFactory will be set
     * to null. The AudioDecoderFactoryFactory should be wrapped in the
     * NetEqFactoryFactory.
     */
    public Builder setNetEqFactoryFactory(NetEqFactoryFactory neteqFactoryFactory) {
      this.neteqFactoryFactory = neteqFactoryFactory;
      return this;
    }

    public PeerConnectionFactory createPeerConnectionFactory() {
      checkInitializeHasBeenCalled();
      if (audioDeviceModule == null) {
        audioDeviceModule = JavaAudioDeviceModule.builder(ContextUtils.getApplicationContext())
                                .createAudioDeviceModule();
      }
      return nativeCreatePeerConnectionFactory(ContextUtils.getApplicationContext(), options,
          audioDeviceModule.getNativeAudioDeviceModulePointer(),
          audioEncoderFactoryFactory.createNativeAudioEncoderFactory(),
          audioDecoderFactoryFactory.createNativeAudioDecoderFactory(), videoEncoderFactory,
          videoDecoderFactory,
          audioProcessingFactory == null ? 0 : audioProcessingFactory.createNative(),
          fecControllerFactoryFactory == null ? 0 : fecControllerFactoryFactory.createNative(),
          networkControllerFactoryFactory == null
              ? 0
              : networkControllerFactoryFactory.createNativeNetworkControllerFactory(),
          networkStatePredictorFactoryFactory == null
              ? 0
              : networkStatePredictorFactoryFactory.createNativeNetworkStatePredictorFactory(),
          neteqFactoryFactory == null ? 0 : neteqFactoryFactory.createNativeNetEqFactory());
    }
  }

  public static Builder builder() {
    return new Builder();
  }

  /**
   * Loads and initializes WebRTC. This must be called at least once before creating a
   * PeerConnectionFactory. Replaces all the old initialization methods. Must not be called while
   * a PeerConnectionFactory is alive.
   */
  public static void initialize(InitializationOptions options) {
    ContextUtils.initialize(options.applicationContext);
    NativeLibrary.initialize(options.nativeLibraryLoader, options.nativeLibraryName);
    nativeInitializeAndroidGlobals();
    nativeInitializeFieldTrials(options.fieldTrials);
    if (options.enableInternalTracer && !internalTracerInitialized) {
      initializeInternalTracer();
    }
    if (options.loggable != null) {
      Logging.injectLoggable(options.loggable, options.loggableSeverity);
      nativeInjectLoggable(new JNILogging(options.loggable), options.loggableSeverity.ordinal());
    } else {
      Logging.d(TAG,
          "PeerConnectionFactory was initialized without an injected Loggable. "
              + "Any existing Loggable will be deleted.");
      Logging.deleteInjectedLoggable();
      nativeDeleteLoggable();
    }
  }

  private static void checkInitializeHasBeenCalled() {
    if (!NativeLibrary.isLoaded() || ContextUtils.getApplicationContext() == null) {
      throw new IllegalStateException(
          "PeerConnectionFactory.initialize was not called before creating a "
          + "PeerConnectionFactory.");
    }
  }

  private static void initializeInternalTracer() {
    internalTracerInitialized = true;
    nativeInitializeInternalTracer();
  }

  public static void shutdownInternalTracer() {
    internalTracerInitialized = false;
    nativeShutdownInternalTracer();
  }

  // Field trial initialization. Must be called before PeerConnectionFactory
  // is created.
  // Deprecated, use PeerConnectionFactory.initialize instead.
  @Deprecated
  public static void initializeFieldTrials(String fieldTrialsInitString) {
    nativeInitializeFieldTrials(fieldTrialsInitString);
  }

  // Wrapper of webrtc::field_trial::FindFullName. Develop the feature with default behaviour off.
  // Example usage:
  // if (PeerConnectionFactory.fieldTrialsFindFullName("WebRTCExperiment").equals("Enabled")) {
  //   method1();
  // } else {
  //   method2();
  // }
  public static String fieldTrialsFindFullName(String name) {
    return NativeLibrary.isLoaded() ? nativeFindFieldTrialsFullName(name) : "";
  }
  // Start/stop internal capturing of internal tracing.
  public static boolean startInternalTracingCapture(String tracingFilename) {
    return nativeStartInternalTracingCapture(tracingFilename);
  }

  public static void stopInternalTracingCapture() {
    nativeStopInternalTracingCapture();
  }

  @CalledByNative
  PeerConnectionFactory(long nativeFactory) {
    checkInitializeHasBeenCalled();
    if (nativeFactory == 0) {
      throw new RuntimeException("Failed to initialize PeerConnectionFactory!");
    }
    this.nativeFactory = nativeFactory;
  }

  /**
   * Internal helper function to pass the parameters down into the native JNI bridge.
   */
  @Nullable
  PeerConnection createPeerConnectionInternal(PeerConnection.RTCConfiguration rtcConfig,
      MediaConstraints constraints, PeerConnection.Observer observer,
      SSLCertificateVerifier sslCertificateVerifier) {
    checkPeerConnectionFactoryExists();
    long nativeObserver = PeerConnection.createNativePeerConnectionObserver(observer);
    if (nativeObserver == 0) {
      return null;
    }
    long nativePeerConnection = nativeCreatePeerConnection(
        nativeFactory, rtcConfig, constraints, nativeObserver, sslCertificateVerifier);
    if (nativePeerConnection == 0) {
      return null;
    }
    return new PeerConnection(nativePeerConnection);
  }

  /**
   * Deprecated. PeerConnection constraints are deprecated. Supply values in rtcConfig struct
   * instead and use the method without constraints in the signature.
   */
  @Nullable
  @Deprecated
  public PeerConnection createPeerConnection(PeerConnection.RTCConfiguration rtcConfig,
      MediaConstraints constraints, PeerConnection.Observer observer) {
    return createPeerConnectionInternal(
        rtcConfig, constraints, observer, /* sslCertificateVerifier= */ null);
  }

  /**
   * Deprecated. PeerConnection constraints are deprecated. Supply values in rtcConfig struct
   * instead and use the method without constraints in the signature.
   */
  @Nullable
  @Deprecated
  public PeerConnection createPeerConnection(List<PeerConnection.IceServer> iceServers,
      MediaConstraints constraints, PeerConnection.Observer observer) {
    PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
    rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
    return createPeerConnection(rtcConfig, constraints, observer);
  }

  @Nullable
  public PeerConnection createPeerConnection(
      List<PeerConnection.IceServer> iceServers, PeerConnection.Observer observer) {
    PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
    rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
    return createPeerConnection(rtcConfig, observer);
  }

  @Nullable
  public PeerConnection createPeerConnection(
      PeerConnection.RTCConfiguration rtcConfig, PeerConnection.Observer observer) {
    return createPeerConnection(rtcConfig, null /* constraints */, observer);
  }

  @Nullable
  public PeerConnection createPeerConnection(
      PeerConnection.RTCConfiguration rtcConfig, PeerConnectionDependencies dependencies) {
    return createPeerConnectionInternal(rtcConfig, null /* constraints */,
        dependencies.getObserver(), dependencies.getSSLCertificateVerifier());
  }

  public MediaStream createLocalMediaStream(String label) {
    checkPeerConnectionFactoryExists();
    return new MediaStream(nativeCreateLocalMediaStream(nativeFactory, label));
  }

  /**
   * Create video source with given parameters. If alignTimestamps is false, the caller is
   * responsible for aligning the frame timestamps to rtc::TimeNanos(). This can be used to achieve
   * higher accuracy if there is a big delay between frame creation and frames being delivered to
   * the returned video source. If alignTimestamps is true, timestamps will be aligned to
   * rtc::TimeNanos() when they arrive to the returned video source.
   */
  public VideoSource createVideoSource(boolean isScreencast, boolean alignTimestamps) {
    checkPeerConnectionFactoryExists();
    return new VideoSource(nativeCreateVideoSource(nativeFactory, isScreencast, alignTimestamps));
  }

  /**
   * Same as above with alignTimestamps set to true.
   *
   * @see #createVideoSource(boolean, boolean)
   */
  public VideoSource createVideoSource(boolean isScreencast) {
    return createVideoSource(isScreencast, /* alignTimestamps= */ true);
  }

  public VideoTrack createVideoTrack(String id, VideoSource source) {
    checkPeerConnectionFactoryExists();
    return new VideoTrack(
        nativeCreateVideoTrack(nativeFactory, id, source.getNativeVideoTrackSource()));
  }

  public AudioSource createAudioSource(MediaConstraints constraints) {
    checkPeerConnectionFactoryExists();
    return new AudioSource(nativeCreateAudioSource(nativeFactory, constraints));
  }

  public AudioTrack createAudioTrack(String id, AudioSource source) {
    checkPeerConnectionFactoryExists();
    return new AudioTrack(nativeCreateAudioTrack(nativeFactory, id, source.getNativeAudioSource()));
  }

  // Starts recording an AEC dump. Ownership of the file is transfered to the
  // native code. If an AEC dump is already in progress, it will be stopped and
  // a new one will start using the provided file.
  public boolean startAecDump(int file_descriptor, int filesize_limit_bytes) {
    checkPeerConnectionFactoryExists();
    return nativeStartAecDump(nativeFactory, file_descriptor, filesize_limit_bytes);
  }

  // Stops recording an AEC dump. If no AEC dump is currently being recorded,
  // this call will have no effect.
  public void stopAecDump() {
    checkPeerConnectionFactoryExists();
    nativeStopAecDump(nativeFactory);
  }

  public void dispose() {
    checkPeerConnectionFactoryExists();
    nativeFreeFactory(nativeFactory);
    networkThread = null;
    workerThread = null;
    signalingThread = null;
    nativeFactory = 0;
  }

  /** Returns a pointer to the native webrtc::PeerConnectionFactoryInterface. */
  public long getNativePeerConnectionFactory() {
    checkPeerConnectionFactoryExists();
    return nativeGetNativePeerConnectionFactory(nativeFactory);
  }

  /** Returns a pointer to the native OwnedFactoryAndThreads object */
  public long getNativeOwnedFactoryAndThreads() {
    checkPeerConnectionFactoryExists();
    return nativeFactory;
  }

  private void checkPeerConnectionFactoryExists() {
    if (nativeFactory == 0) {
      throw new IllegalStateException("PeerConnectionFactory has been disposed.");
    }
  }

  private static void printStackTrace(
      @Nullable ThreadInfo threadInfo, boolean printNativeStackTrace) {
    if (threadInfo == null) {
      // Thread callbacks have not been completed yet, ignore call.
      return;
    }
    final String threadName = threadInfo.thread.getName();
    StackTraceElement[] stackTraces = threadInfo.thread.getStackTrace();
    if (stackTraces.length > 0) {
      Logging.w(TAG, threadName + " stacktrace:");
      for (StackTraceElement stackTrace : stackTraces) {
        Logging.w(TAG, stackTrace.toString());
      }
    }
    if (printNativeStackTrace) {
      // Imitate output from debuggerd/tombstone so that stack trace can easily be symbolized with
      // ndk-stack.
      Logging.w(TAG, "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***");
      Logging.w(TAG,
          "pid: " + Process.myPid() + ", tid: " + threadInfo.tid + ", name: " + threadName
              + "  >>> WebRTC <<<");
      nativePrintStackTrace(threadInfo.tid);
    }
  }

  /** Deprecated, use non-static version instead. */
  @Deprecated
  public static void printStackTraces() {
    printStackTrace(staticNetworkThread, /* printNativeStackTrace= */ false);
    printStackTrace(staticWorkerThread, /* printNativeStackTrace= */ false);
    printStackTrace(staticSignalingThread, /* printNativeStackTrace= */ false);
  }

  /**
   * Print the Java stack traces for the critical threads used by PeerConnectionFactory, namely;
   * signaling thread, worker thread, and network thread. If printNativeStackTraces is true, also
   * attempt to print the C++ stack traces for these threads.
   */
  public void printInternalStackTraces(boolean printNativeStackTraces) {
    printStackTrace(signalingThread, printNativeStackTraces);
    printStackTrace(workerThread, printNativeStackTraces);
    printStackTrace(networkThread, printNativeStackTraces);
  }

  @CalledByNative
  private void onNetworkThreadReady() {
    networkThread = ThreadInfo.getCurrent();
    staticNetworkThread = networkThread;
    Logging.d(TAG, "onNetworkThreadReady");
  }

  @CalledByNative
  private void onWorkerThreadReady() {
    workerThread = ThreadInfo.getCurrent();
    staticWorkerThread = workerThread;
    Logging.d(TAG, "onWorkerThreadReady");
  }

  @CalledByNative
  private void onSignalingThreadReady() {
    signalingThread = ThreadInfo.getCurrent();
    staticSignalingThread = signalingThread;
    Logging.d(TAG, "onSignalingThreadReady");
  }

  // Must be called at least once before creating a PeerConnectionFactory
  // (for example, at application startup time).
  private static native void nativeInitializeAndroidGlobals();
  private static native void nativeInitializeFieldTrials(String fieldTrialsInitString);
  private static native String nativeFindFieldTrialsFullName(String name);
  private static native void nativeInitializeInternalTracer();
  // Internal tracing shutdown, called to prevent resource leaks. Must be called after
  // PeerConnectionFactory is gone to prevent races with code performing tracing.
  private static native void nativeShutdownInternalTracer();
  private static native boolean nativeStartInternalTracingCapture(String tracingFilename);
  private static native void nativeStopInternalTracingCapture();

  private static native PeerConnectionFactory nativeCreatePeerConnectionFactory(Context context,
      Options options, long nativeAudioDeviceModule, long audioEncoderFactory,
      long audioDecoderFactory, VideoEncoderFactory encoderFactory,
      VideoDecoderFactory decoderFactory, long nativeAudioProcessor,
      long nativeFecControllerFactory, long nativeNetworkControllerFactory,
      long nativeNetworkStatePredictorFactory, long neteqFactory);

  private static native long nativeCreatePeerConnection(long factory,
      PeerConnection.RTCConfiguration rtcConfig, MediaConstraints constraints, long nativeObserver,
      SSLCertificateVerifier sslCertificateVerifier);
  private static native long nativeCreateLocalMediaStream(long factory, String label);
  private static native long nativeCreateVideoSource(
      long factory, boolean is_screencast, boolean alignTimestamps);
  private static native long nativeCreateVideoTrack(
      long factory, String id, long nativeVideoSource);
  private static native long nativeCreateAudioSource(long factory, MediaConstraints constraints);
  private static native long nativeCreateAudioTrack(long factory, String id, long nativeSource);
  private static native boolean nativeStartAecDump(
      long factory, int file_descriptor, int filesize_limit_bytes);
  private static native void nativeStopAecDump(long factory);
  private static native void nativeFreeFactory(long factory);
  private static native long nativeGetNativePeerConnectionFactory(long factory);
  private static native void nativeInjectLoggable(JNILogging jniLogging, int severity);
  private static native void nativeDeleteLoggable();
  private static native void nativePrintStackTrace(int tid);
}
