/*
 * Copyright (C) 2025 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 com.google.snippet.telecom;

import static android.Manifest.permission.ANSWER_PHONE_CALLS;
import static android.Manifest.permission.CALL_PHONE;
import static android.Manifest.permission.MODIFY_PHONE_STATE;
import static android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_RX;
import static android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX;
import static android.telecom.Call.Details.PROPERTY_WIFI;
import static android.telecom.TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE;
import static android.telecom.TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.telecom.Call;
import android.telecom.Connection;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import androidx.annotation.RequiresPermission;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.mobly.snippet.Snippet;
import com.google.android.mobly.snippet.rpc.Rpc;
import com.google.android.mobly.snippet.rpc.RpcDefault;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeoutException;

/** Snippet class for TelecomManager APIs. */
public class TelecomManagerSnippet implements Snippet {

  private static class TelecomManagerSnippetException extends Exception {
    TelecomManagerSnippetException(String msg) {
      super(msg);
    }
  }

  private enum CallType {
    CALL_TYPE_UNKNOWN(0),
    CALL_TYPE_LEGACY(1),
    CALL_TYPE_VILTE(2),
    CALL_TYPE_VOLTE(3),
    CALL_TYPE_VOWIFI(4);

    private final int value;

    CallType(int value) {
      this.value = value;
    }

    public int getValue() {
      return this.value;
    }
  }

  private static final Duration END_CALL_TIMEOUT = Duration.ofSeconds(5);
  private static final Duration INCOMING_CALL_TIMEOUT = Duration.ofSeconds(5);
  private static final Duration OUTGOING_CALL_TIMEOUT = Duration.ofSeconds(5);
  private static final Uri TEST_DIAL_NUMBER = Uri.fromParts("tel", "5551212", null);

  private final Context context;
  private final TelecomManager telecomManager;

  private ConnectionImpl selfManagedConnection;

  private ConnectionImpl.Listener connectionListener =
      new ConnectionImpl.Listener() {
        @Override
        void onShowIncomingCallUi(ConnectionImpl connection) {
          super.onShowIncomingCallUi(connection);
          connection.onAnswer(0);
        }

        @Override
        void onHold(ConnectionImpl connection) {
          super.onHold(connection);
        }
      };

  public TelecomManagerSnippet() {
    context = InstrumentationRegistry.getInstrumentation().getContext();
    telecomManager = context.getSystemService(TelecomManager.class);
  }

  @Rpc(description = "Gets the state of the call from the provided address.")
  public int telecomGetCallState(String phoneNumber) throws TelecomManagerSnippetException {
    throwIfNotDefaultDialer();
    Call call = InCallServiceImpl.getCallByPhoneNumber(phoneNumber);
    if (call == null) {
      return Call.STATE_DISCONNECTED;
    }
    return call.getDetails().getState();
  }

  @RequiresPermission(anyOf = {ANSWER_PHONE_CALLS, MODIFY_PHONE_STATE})
  @Rpc(description = "Accepts the incoming call from the provided address.")
  public void telecomAcceptRingingCall(String phoneNumber, @RpcDefault("0") Integer videoState)
      throws TelecomManagerSnippetException, TimeoutException {
    throwIfNotDefaultDialer();
    Call call = InCallServiceImpl.getCallByPhoneNumber(phoneNumber);
    if (call == null) {
      throw new TelecomManagerSnippetException(
          "No incoming call found for phone number: " + phoneNumber);
    }
    call.answer(videoState);
    WaitUtils.waitForSnippetEvent(
        /* callbackId= */ InCallServiceImpl.CALLBACK_ID,
        /* eventName= */ InCallServiceImpl.CALLBACK_KEY,
        /* timeoutMs= */ INCOMING_CALL_TIMEOUT.toMillis(),
        /* predicate= */ event ->
            event.getData().getInt("state") == Call.STATE_ACTIVE
                && InCallServiceImpl.arePhoneNumbersEqual(
                    event.getData().getString("phoneNumber"), phoneNumber),
        /* errorMessage= */ "Timed out waiting for incoming call to be connected.");
  }

  @RequiresPermission(CALL_PHONE)
  @Rpc(description = "Places a new outgoing call to the provided address.")
  public void telecomPlaceCall(
      String phoneNumber,
      @RpcDefault("false") Boolean startWithSpeakerphone,
      @RpcDefault("0") Integer videoState,
      @RpcDefault("true") Boolean shouldDefaultDialerSet)
      throws TelecomManagerSnippetException, TimeoutException {
    if (shouldDefaultDialerSet) {
      throwIfNotDefaultDialer();
    }
    Uri address = Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
    Bundle extras = new Bundle();
    extras.putBoolean(EXTRA_START_CALL_WITH_SPEAKERPHONE, startWithSpeakerphone);
    extras.putInt(EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
    telecomManager.placeCall(address, extras);
    if (shouldDefaultDialerSet) {
      WaitUtils.waitForSnippetEvent(
          /* callbackId= */ InCallServiceImpl.CALLBACK_ID,
          /* eventName= */ InCallServiceImpl.CALLBACK_KEY,
          /* timeoutMs= */ OUTGOING_CALL_TIMEOUT.toMillis(),
          /* predicate= */ event ->
              InCallServiceImpl.arePhoneNumbersEqual(
                      event.getData().getString("phoneNumber"), phoneNumber)
                  && (event.getData().getInt("state") == Call.STATE_DIALING
                      || event.getData().getInt("state") == Call.STATE_ACTIVE),
          /* errorMessage= */ "Timed out waiting for outgoing call to be connected.");
    }
  }

  @RequiresPermission(ANSWER_PHONE_CALLS)
  @Rpc(description = "Ends the ongoing call from the provided address.")
  public void telecomEndCall(String phoneNumber)
      throws TelecomManagerSnippetException, TimeoutException {
    throwIfNotDefaultDialer();
    Call call = InCallServiceImpl.getCallByPhoneNumber(phoneNumber);
    if (call == null) {
      throw new TelecomManagerSnippetException(
          "No active call found for phone number: " + phoneNumber);
    }
    call.disconnect();
    WaitUtils.waitUntilTrue(
        /* supplier= */ () -> InCallServiceImpl.getCallByPhoneNumber(phoneNumber) == null,
        /* timeoutInMillis= */ END_CALL_TIMEOUT.toMillis(),
        /* errorMessage= */ "Timed out waiting for ongoing call to be disconnected.");
  }

  @RequiresPermission(ANSWER_PHONE_CALLS)
  @Rpc(description = "Ends all ongoing calls.")
  public void telecomEndAllCalls() throws TelecomManagerSnippetException, TimeoutException {
    throwIfNotDefaultDialer();
    for (Call call : InCallServiceImpl.getActiveCalls()) {
      call.disconnect();
    }
    WaitUtils.waitUntilTrue(
        /* supplier= */ () -> InCallServiceImpl.getActiveCalls().isEmpty(),
        /* timeoutInMillis= */ END_CALL_TIMEOUT.toMillis(),
        /* errorMessage= */ "Timed out waiting for all ongoing calls to be disconnected.");
  }

  @Rpc(description = "Returns the type of the call with the provided address.")
  public int telecomGetCallType(String phoneNumber) throws TelecomManagerSnippetException {
    throwIfNotDefaultDialer();
    Call call = InCallServiceImpl.getCallByPhoneNumber(phoneNumber);
    if (call == null) {
      return CallType.CALL_TYPE_UNKNOWN.getValue();
    }

    Call.Details callDetails = call.getDetails();
    if (callDetails == null) {
      return CallType.CALL_TYPE_UNKNOWN.getValue();
    }

    if (isVowifiCall(callDetails)) {
      return CallType.CALL_TYPE_VOWIFI.getValue();
    }

    if (isVilteCall(callDetails)) {
      return CallType.CALL_TYPE_VILTE.getValue();
    }

    if (isVolteCall(callDetails)) {
      return CallType.CALL_TYPE_VOLTE.getValue();
    }

    return CallType.CALL_TYPE_LEGACY.getValue();
  }

  @Rpc(description = "Places a self-managed call.")
  public void addNewIncomingCall(@RpcDefault("true") Boolean setActive)
      throws TelecomManagerSnippetException {
    Bundle extras = new Bundle();
    extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, TEST_DIAL_NUMBER);
    extras.putBoolean(ConnectionImpl.EXTRA_PLAY_CS_AUDIO, true);

    telecomManager.addNewIncomingCall(
        PhoneAccountUtils.TEST_SELF_MANAGED_PHONE_ACCOUNT_HANDLE, extras);

    ManagedConnectionService connectionService =
        ManagedConnectionService.waitForAndGetConnectionService();
    if (connectionService == null) {
      PhoneAccountUtils.cleanupConnectionServices();
      throw new TelecomManagerSnippetException("ConnectionService is not initialized");
    }

    ConnectionImpl connection = connectionService.waitForAndGetConnection();
    if (connection == null) {
      PhoneAccountUtils.cleanupConnectionServices();
      throw new TelecomManagerSnippetException("Could not get connection");
    }

    connection.addListener(connectionListener);
    connection.waitForAudioStateChanged();
    if (setActive) {
      connection.setActive();
    }
  }

  @Rpc(description = "Confirms the incoming call.")
  public void confirmIncomingCall() throws TelecomManagerSnippetException {
    selfManagedConnection = getOngoingConnection();
    if (selfManagedConnection.getState() != Connection.STATE_ACTIVE) {
      throw new TelecomManagerSnippetException(
          "confirmIncomingCall: fail - ongoing call is not active");
    }
  }

  @Rpc(description = "Unholds the self-managed call.")
  public void unholdSelfManagedCall() throws TelecomManagerSnippetException {
    selfManagedConnection = getOngoingConnection();
    selfManagedConnection.onUnhold();
  }

  @Rpc(description = "Disconnects the self-managed call.")
  public void disconnectSelfManagedCall() throws TelecomManagerSnippetException {
    selfManagedConnection = getOngoingConnection();
    selfManagedConnection.onDisconnect();
  }

  private ConnectionImpl getOngoingConnection() throws TelecomManagerSnippetException {
    if (ManagedConnectionService.getConnectionService() == null) {
      throw new TelecomManagerSnippetException("ConnectionService is not initialized");
    }

    List<ConnectionImpl> ongoingConnections =
        ManagedConnectionService.getConnectionService().getConnections();
    if (ongoingConnections == null || ongoingConnections.size() != 1) {
      throw new TelecomManagerSnippetException("confirmIncomingCall: fail - no ongoing call found");
    }
    return ongoingConnections.get(0);
  }

  private void throwIfNotDefaultDialer() throws TelecomManagerSnippetException {
    if (!telecomManager.getDefaultDialerPackage().equals(context.getPackageName())) {
      throw new TelecomManagerSnippetException(
          "Telecom snippet is not the default dialer. Please set it as the default dialer before"
              + " calling this RPC.");
    }
  }

  private boolean isVowifiCall(Call.Details callDetails) {
    return (callDetails.getCallProperties() & PROPERTY_WIFI) == PROPERTY_WIFI;
  }

  private boolean isVilteCall(Call.Details callDetails) {
    return !VideoProfile.isAudioOnly(callDetails.getVideoState());
  }

  private boolean isVolteCall(Call.Details callDetails) {
    int cap = callDetails.getCallCapabilities();
    boolean hasImsCapabilities =
        ((cap & CAPABILITY_SUPPORTS_VT_LOCAL_TX) == CAPABILITY_SUPPORTS_VT_LOCAL_TX
            || (cap & CAPABILITY_SUPPORTS_VT_LOCAL_RX) == CAPABILITY_SUPPORTS_VT_LOCAL_RX);
    return hasImsCapabilities && !isVowifiCall(callDetails) && !isVilteCall(callDetails);
  }

  @Override
  public void shutdown() {}
}

