/*
 * Copyright 2021 The Android Open Source Project
 *
 * 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 androidx.media3.common.util;

import static androidx.media3.common.util.Assertions.checkNotNull;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;
import android.os.Looper;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyCallback.DisplayInfoListener;
import android.telephony.TelephonyDisplayInfo;
import android.telephony.TelephonyManager;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import java.lang.ref.WeakReference;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Observer for network type changes.
 *
 * <p>{@link #register Registered} listeners are informed at registration and whenever the network
 * type changes.
 *
 * <p>The current network type can also be {@link #getNetworkType queried} without registration.
 */
@UnstableApi
public final class NetworkTypeObserver {

  /** A listener for network type changes. */
  public interface Listener {

    /**
     * Called when the network type changed or when the listener is first registered.
     *
     * <p>This method is always called on the main thread.
     */
    void onNetworkTypeChanged(@C.NetworkType int networkType);
  }

  @Nullable private static NetworkTypeObserver staticInstance;

  private final Handler mainHandler;
  // This class needs to hold weak references as it doesn't require listeners to unregister.
  private final CopyOnWriteArrayList<WeakReference<Listener>> listeners;
  private final Object networkTypeLock;

  @GuardedBy("networkTypeLock")
  private @C.NetworkType int networkType;

  /**
   * Returns a network type observer instance.
   *
   * @param context A {@link Context}.
   */
  public static synchronized NetworkTypeObserver getInstance(Context context) {
    if (staticInstance == null) {
      staticInstance = new NetworkTypeObserver(context);
    }
    return staticInstance;
  }

  /** Resets the network type observer for tests. */
  @VisibleForTesting
  public static synchronized void resetForTests() {
    staticInstance = null;
  }

  private NetworkTypeObserver(Context context) {
    mainHandler = new Handler(Looper.getMainLooper());
    listeners = new CopyOnWriteArrayList<>();
    networkTypeLock = new Object();
    networkType = C.NETWORK_TYPE_UNKNOWN;
    IntentFilter filter = new IntentFilter();
    filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
    context.registerReceiver(/* receiver= */ new Receiver(), filter);
  }

  /**
   * Registers a listener.
   *
   * <p>The current network type will be reported to the listener after registration.
   *
   * @param listener The {@link Listener}.
   */
  public void register(Listener listener) {
    removeClearedReferences();
    listeners.add(new WeakReference<>(listener));
    // Simulate an initial update on the main thread (like the sticky broadcast we'd receive if
    // we were to register a separate broadcast receiver for each listener).
    mainHandler.post(() -> listener.onNetworkTypeChanged(getNetworkType()));
  }

  /** Returns the current network type. */
  public @C.NetworkType int getNetworkType() {
    synchronized (networkTypeLock) {
      return networkType;
    }
  }

  private void removeClearedReferences() {
    for (WeakReference<Listener> listenerReference : listeners) {
      if (listenerReference.get() == null) {
        listeners.remove(listenerReference);
      }
    }
  }

  private void updateNetworkType(@C.NetworkType int networkType) {
    synchronized (networkTypeLock) {
      if (this.networkType == networkType) {
        return;
      }
      this.networkType = networkType;
    }
    for (WeakReference<Listener> listenerReference : listeners) {
      @Nullable Listener listener = listenerReference.get();
      if (listener != null) {
        listener.onNetworkTypeChanged(networkType);
      } else {
        listeners.remove(listenerReference);
      }
    }
  }

  private static @C.NetworkType int getNetworkTypeFromConnectivityManager(Context context) {
    NetworkInfo networkInfo;
    @Nullable
    ConnectivityManager connectivityManager =
        (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    if (connectivityManager == null) {
      return C.NETWORK_TYPE_UNKNOWN;
    }
    try {
      networkInfo = connectivityManager.getActiveNetworkInfo();
    } catch (SecurityException e) {
      // Expected if permission was revoked.
      return C.NETWORK_TYPE_UNKNOWN;
    }
    if (networkInfo == null || !networkInfo.isConnected()) {
      return C.NETWORK_TYPE_OFFLINE;
    }
    switch (networkInfo.getType()) {
      case ConnectivityManager.TYPE_WIFI:
        return C.NETWORK_TYPE_WIFI;
      case ConnectivityManager.TYPE_WIMAX:
        return C.NETWORK_TYPE_4G;
      case ConnectivityManager.TYPE_MOBILE:
      case ConnectivityManager.TYPE_MOBILE_DUN:
      case ConnectivityManager.TYPE_MOBILE_HIPRI:
        return getMobileNetworkType(networkInfo);
      case ConnectivityManager.TYPE_ETHERNET:
        return C.NETWORK_TYPE_ETHERNET;
      default:
        return C.NETWORK_TYPE_OTHER;
    }
  }

  private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) {
    switch (networkInfo.getSubtype()) {
      case TelephonyManager.NETWORK_TYPE_EDGE:
      case TelephonyManager.NETWORK_TYPE_GPRS:
        return C.NETWORK_TYPE_2G;
      case TelephonyManager.NETWORK_TYPE_1xRTT:
      case TelephonyManager.NETWORK_TYPE_CDMA:
      case TelephonyManager.NETWORK_TYPE_EVDO_0:
      case TelephonyManager.NETWORK_TYPE_EVDO_A:
      case TelephonyManager.NETWORK_TYPE_EVDO_B:
      case TelephonyManager.NETWORK_TYPE_HSDPA:
      case TelephonyManager.NETWORK_TYPE_HSPA:
      case TelephonyManager.NETWORK_TYPE_HSUPA:
      case TelephonyManager.NETWORK_TYPE_IDEN:
      case TelephonyManager.NETWORK_TYPE_UMTS:
      case TelephonyManager.NETWORK_TYPE_EHRPD:
      case TelephonyManager.NETWORK_TYPE_HSPAP:
      case TelephonyManager.NETWORK_TYPE_TD_SCDMA:
        return C.NETWORK_TYPE_3G;
      case TelephonyManager.NETWORK_TYPE_LTE:
        return C.NETWORK_TYPE_4G;
      case TelephonyManager.NETWORK_TYPE_NR:
        return Util.SDK_INT >= 29 ? C.NETWORK_TYPE_5G_SA : C.NETWORK_TYPE_UNKNOWN;
      case TelephonyManager.NETWORK_TYPE_IWLAN:
        return C.NETWORK_TYPE_WIFI;
      case TelephonyManager.NETWORK_TYPE_GSM:
      case TelephonyManager.NETWORK_TYPE_UNKNOWN:
      default: // Future mobile network types.
        return C.NETWORK_TYPE_CELLULAR_UNKNOWN;
    }
  }

  private final class Receiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
      @C.NetworkType int networkType = getNetworkTypeFromConnectivityManager(context);
      if (Util.SDK_INT >= 31 && networkType == C.NETWORK_TYPE_4G) {
        // Delay update of the network type to check whether this is actually 5G-NSA.
        Api31.disambiguate4gAnd5gNsa(context, /* instance= */ NetworkTypeObserver.this);
      } else {
        updateNetworkType(networkType);
      }
    }
  }

  @RequiresApi(31)
  private static final class Api31 {

    public static void disambiguate4gAnd5gNsa(Context context, NetworkTypeObserver instance) {
      try {
        TelephonyManager telephonyManager =
            checkNotNull((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
        DisplayInfoCallback callback = new DisplayInfoCallback(instance);
        telephonyManager.registerTelephonyCallback(context.getMainExecutor(), callback);
        // We are only interested in the initial response with the current state, so unregister
        // the listener immediately.
        telephonyManager.unregisterTelephonyCallback(callback);
      } catch (RuntimeException e) {
        // Ignore problems with listener registration and keep reporting as 4G.
        instance.updateNetworkType(C.NETWORK_TYPE_4G);
      }
    }

    private static final class DisplayInfoCallback extends TelephonyCallback
        implements DisplayInfoListener {

      private final NetworkTypeObserver instance;

      public DisplayInfoCallback(NetworkTypeObserver instance) {
        this.instance = instance;
      }

      @Override
      public void onDisplayInfoChanged(TelephonyDisplayInfo telephonyDisplayInfo) {
        int overrideNetworkType = telephonyDisplayInfo.getOverrideNetworkType();
        boolean is5gNsa =
            overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA
                || overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE
                || overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED;
        instance.updateNetworkType(is5gNsa ? C.NETWORK_TYPE_5G_NSA : C.NETWORK_TYPE_4G);
      }
    }
  }
}
