/*
 * Copyright (C) 2017 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.exoplayer.scheduler;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;

import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.BatteryManager;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PowerManager;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** Defines a set of device state requirements. */
@UnstableApi
public final class Requirements implements Parcelable {

  /**
   * Requirement flags. Possible flag values are {@link #NETWORK}, {@link #NETWORK_UNMETERED},
   * {@link #DEVICE_IDLE}, {@link #DEVICE_CHARGING} and {@link #DEVICE_STORAGE_NOT_LOW}.
   */
  // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
  // with Kotlin usages from before TYPE_USE was added.
  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
  @IntDef(
      flag = true,
      value = {NETWORK, NETWORK_UNMETERED, DEVICE_IDLE, DEVICE_CHARGING, DEVICE_STORAGE_NOT_LOW})
  public @interface RequirementFlags {}

  /** Requirement that the device has network connectivity. */
  public static final int NETWORK = 1;
  /** Requirement that the device has a network connection that is unmetered. */
  public static final int NETWORK_UNMETERED = 1 << 1;
  /** Requirement that the device is idle. */
  public static final int DEVICE_IDLE = 1 << 2;
  /** Requirement that the device is charging. */
  public static final int DEVICE_CHARGING = 1 << 3;
  /**
   * Requirement that the device's <em>internal</em> storage is not low. Note that this requirement
   * is not affected by the status of external storage.
   */
  public static final int DEVICE_STORAGE_NOT_LOW = 1 << 4;

  private final @RequirementFlags int requirements;

  /**
   * @param requirements A combination of requirement flags.
   */
  public Requirements(@RequirementFlags int requirements) {
    if ((requirements & NETWORK_UNMETERED) != 0) {
      // Make sure network requirement flags are consistent.
      requirements |= NETWORK;
    }
    this.requirements = requirements;
  }

  /** Returns the requirements. */
  public @RequirementFlags int getRequirements() {
    return requirements;
  }

  /**
   * Filters the requirements, returning the subset that are enabled by the provided filter.
   *
   * @param requirementsFilter The enabled {@link RequirementFlags}.
   * @return The filtered requirements. If the filter does not cause a change in the requirements
   *     then this instance will be returned.
   */
  public Requirements filterRequirements(int requirementsFilter) {
    int filteredRequirements = requirements & requirementsFilter;
    return filteredRequirements == requirements ? this : new Requirements(filteredRequirements);
  }

  /** Returns whether network connectivity is required. */
  public boolean isNetworkRequired() {
    return (requirements & NETWORK) != 0;
  }

  /** Returns whether un-metered network connectivity is required. */
  public boolean isUnmeteredNetworkRequired() {
    return (requirements & NETWORK_UNMETERED) != 0;
  }

  /** Returns whether the device is required to be charging. */
  public boolean isChargingRequired() {
    return (requirements & DEVICE_CHARGING) != 0;
  }

  /** Returns whether the device is required to be idle. */
  public boolean isIdleRequired() {
    return (requirements & DEVICE_IDLE) != 0;
  }

  /** Returns whether the device is required to not be low on <em>internal</em> storage. */
  public boolean isStorageNotLowRequired() {
    return (requirements & DEVICE_STORAGE_NOT_LOW) != 0;
  }

  /**
   * Returns whether the requirements are met.
   *
   * @param context Any context.
   * @return Whether the requirements are met.
   */
  public boolean checkRequirements(Context context) {
    return getNotMetRequirements(context) == 0;
  }

  /**
   * Returns requirements that are not met, or 0.
   *
   * @param context Any context.
   * @return The requirements that are not met, or 0.
   */
  public @RequirementFlags int getNotMetRequirements(Context context) {
    @RequirementFlags int notMetRequirements = getNotMetNetworkRequirements(context);
    if (isChargingRequired() && !isDeviceCharging(context)) {
      notMetRequirements |= DEVICE_CHARGING;
    }
    if (isIdleRequired() && !isDeviceIdle(context)) {
      notMetRequirements |= DEVICE_IDLE;
    }
    if (isStorageNotLowRequired() && !isStorageNotLow(context)) {
      notMetRequirements |= DEVICE_STORAGE_NOT_LOW;
    }
    return notMetRequirements;
  }

  private @RequirementFlags int getNotMetNetworkRequirements(Context context) {
    if (!isNetworkRequired()) {
      return 0;
    }

    ConnectivityManager connectivityManager =
        (ConnectivityManager)
            Assertions.checkNotNull(context.getSystemService(Context.CONNECTIVITY_SERVICE));
    @Nullable NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
    if (networkInfo == null
        || !networkInfo.isConnected()
        || !isInternetConnectivityValidated(connectivityManager)) {
      return requirements & (NETWORK | NETWORK_UNMETERED);
    }

    if (isUnmeteredNetworkRequired() && connectivityManager.isActiveNetworkMetered()) {
      return NETWORK_UNMETERED;
    }

    return 0;
  }

  private boolean isDeviceCharging(Context context) {
    @Nullable
    Intent batteryStatus =
        context.registerReceiver(
            /* receiver= */ null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
    if (batteryStatus == null) {
      return false;
    }
    int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
    return status == BatteryManager.BATTERY_STATUS_CHARGING
        || status == BatteryManager.BATTERY_STATUS_FULL;
  }

  private boolean isDeviceIdle(Context context) {
    PowerManager powerManager =
        (PowerManager) Assertions.checkNotNull(context.getSystemService(Context.POWER_SERVICE));
    return Util.SDK_INT >= 23
        ? powerManager.isDeviceIdleMode()
        : Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn();
  }

  private boolean isStorageNotLow(Context context) {
    return context.registerReceiver(
            /* receiver= */ null, new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW))
        == null;
  }

  private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) {
    // It's possible to check NetworkCapabilities.NET_CAPABILITY_VALIDATED from API level 23, but
    // RequirementsWatcher only fires an event to re-check the requirements when NetworkCapabilities
    // change from API level 24. We assume that network capability is validated for API level 23 to
    // keep in sync.
    if (Util.SDK_INT < 24) {
      return true;
    }

    @Nullable Network activeNetwork = connectivityManager.getActiveNetwork();
    if (activeNetwork == null) {
      return false;
    }

    try {
      @Nullable
      NetworkCapabilities networkCapabilities =
          connectivityManager.getNetworkCapabilities(activeNetwork);
      return networkCapabilities != null
          && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
    } catch (SecurityException e) {
      // Workaround for https://issuetracker.google.com/issues/175055271.
      return true;
    }
  }

  @Override
  public boolean equals(@Nullable Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    return requirements == ((Requirements) o).requirements;
  }

  @Override
  public int hashCode() {
    return requirements;
  }

  // Parcelable implementation.

  @Override
  public int describeContents() {
    return 0;
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeInt(requirements);
  }

  public static final Parcelable.Creator<Requirements> CREATOR =
      new Creator<Requirements>() {

        @Override
        public Requirements createFromParcel(Parcel in) {
          return new Requirements(in.readInt());
        }

        @Override
        public Requirements[] newArray(int size) {
          return new Requirements[size];
        }
      };
}
