package com.cloudflare.realtimekit.incallmanager;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.BroadcastReceiver;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.PowerManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.Nullable;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.Window;
import android.view.WindowManager;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import com.cloudflare.realtimekit.RTKLogger;
import com.cloudflare.realtimekit.incallmanager.AppRTCBluetoothManager;
import com.cloudflare.realtimekit.incallmanager.InCallManagerModule.AudioDevice;

public class InCallManagerModule extends ReactContextBaseJavaModule implements LifecycleEventListener, AudioManager.OnAudioFocusChangeListener {
    private static final String REACT_NATIVE_MODULE_NAME = "InCallManager";
    private static final String TAG = REACT_NATIVE_MODULE_NAME;
    private String mPackageName = "com.cloudflare.realtimekit.incallmanager";

    // --- Screen Manager
    private PowerManager mPowerManager;
    private WindowManager.LayoutParams lastLayoutParams;
    private WindowManager mWindowManager;

    // --- AudioRouteManager
    private AudioManager audioManager;
    private boolean audioManagerActivated = false;
    private boolean isAudioFocused = false;
    //private final Object mAudioFocusLock = new Object();
    private boolean isOrigAudioSetupStored = false;
    private boolean origIsSpeakerPhoneOn = false;
    private boolean origIsMicrophoneMute = false;
    private int origAudioMode = AudioManager.MODE_INVALID;
    private boolean defaultSpeakerOn = true; // Default to video call mode (speaker on)
    private int defaultAudioMode = AudioManager.MODE_IN_COMMUNICATION;
    private int forceSpeakerOn = 0;
    private boolean automatic = true;
    private boolean isProximityRegistered = false;
    private boolean proximityIsNear = false;
    private static final String ACTION_HEADSET_PLUG = (android.os.Build.VERSION.SDK_INT >= 21) ? AudioManager.ACTION_HEADSET_PLUG : Intent.ACTION_HEADSET_PLUG;
    private BroadcastReceiver wiredHeadsetReceiver;
    private BroadcastReceiver noisyAudioReceiver;
    private BroadcastReceiver mediaButtonReceiver;
    private AudioAttributes mAudioAttributes;
    private AudioFocusRequest mAudioFocusRequest;

    /**
     * AudioDevice is the names of possible audio devices that we currently
     * support.
     */
    public enum AudioDevice { SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE }

    /** AudioManager state. */
    public enum AudioManagerState {
        UNINITIALIZED,
        PREINITIALIZED,
        RUNNING,
    }

    private int savedAudioMode = AudioManager.MODE_INVALID;
    private boolean savedIsSpeakerPhoneOn = false;
    private boolean savedIsMicrophoneMute = false;
    private boolean hasWiredHeadset = false;

    // Default audio device; speaker phone for video calls or earpiece for audio
    // only calls.
    private AudioDevice defaultAudioDevice = AudioDevice.NONE;

    // Contains the currently selected audio device.
    // This device is changed automatically using a certain scheme where e.g.
    // a wired headset "wins" over speaker phone. It is also possible for a
    // user to explicitly select a device (and overrid any predefined scheme).
    // See |userSelectedAudioDevice| for details.
    private AudioDevice selectedAudioDevice;

    // Contains the user-selected audio device which overrides the predefined
    // selection scheme.
    // TODO(henrika): always set to AudioDevice.NONE today. Add support for
    // explicit selection based on choice by userSelectedAudioDevice.
    private AudioDevice userSelectedAudioDevice;

    // Contains speakerphone setting: auto, true or false
    private final String useSpeakerphone = "SPEAKERPHONE_AUTO";

    // Handles all tasks related to Bluetooth headset devices.
    private AppRTCBluetoothManager bluetoothManager = null;

    private final InCallProximityManager proximityManager;

    private final InCallWakeLockUtils wakeLockUtils;

    // Contains a list of available audio devices. A Set collection is used to
    // avoid duplicate elements.
    private Set<AudioDevice> audioDevices = new HashSet<>();

    // Guard flag to prevent re-entrant calls to updateAudioDeviceState
    private boolean isUpdatingAudioDeviceState = false;

    @Override
    public String getName() {
        return REACT_NATIVE_MODULE_NAME;
    }

    public InCallManagerModule(ReactApplicationContext reactContext) {
        super(reactContext);
        mPackageName = reactContext.getPackageName();
        reactContext.addLifecycleEventListener(this);
        mWindowManager = (WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE);
        mPowerManager = (PowerManager) reactContext.getSystemService(Context.POWER_SERVICE);
        audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE));
        wakeLockUtils = new InCallWakeLockUtils(reactContext);
        proximityManager = InCallProximityManager.create(reactContext, this);

        UiThreadUtil.runOnUiThread(() -> {
            bluetoothManager = AppRTCBluetoothManager.create(reactContext, this);
        });

        RTKLogger.d("InCallManager", "InCallManager initialized");
    }

    private void manualTurnScreenOff() {
        RTKLogger.d("InCallManager", "manualTurnScreenOff()");
        UiThreadUtil.runOnUiThread(new Runnable() {
            public void run() {
                Activity mCurrentActivity = getCurrentActivity();
                if (mCurrentActivity == null) {
                    RTKLogger.d("InCallManager", "ReactContext doesn't have any Activity attached.");
                    return;
                }
                Window window = mCurrentActivity.getWindow();
                WindowManager.LayoutParams params = window.getAttributes();
                lastLayoutParams = params; // --- store last param
                params.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF; // --- Dim as dark as possible. see BRIGHTNESS_OVERRIDE_OFF
                window.setAttributes(params);
                window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
            }
        });
    }

    private void manualTurnScreenOn() {
        RTKLogger.d("InCallManager", "manualTurnScreenOn()");
        UiThreadUtil.runOnUiThread(new Runnable() {
            public void run() {
                Activity mCurrentActivity = getCurrentActivity();
                if (mCurrentActivity == null) {
                    RTKLogger.d("InCallManager", "ReactContext doesn't have any Activity attached.");
                    return;
                }
                Window window = mCurrentActivity.getWindow();
                if (lastLayoutParams != null) {
                    window.setAttributes(lastLayoutParams);
                } else {
                    WindowManager.LayoutParams params = window.getAttributes();
                    params.screenBrightness = -1; // --- Dim to preferable one
                    window.setAttributes(params);
                }
                window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
            }
        });
    }

    private void storeOriginalAudioSetup() {
        RTKLogger.d("InCallManager", "storeOriginalAudioSetup()");
        if (!isOrigAudioSetupStored) {
            origAudioMode = audioManager.getMode();
            origIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
            origIsMicrophoneMute = audioManager.isMicrophoneMute();
            isOrigAudioSetupStored = true;
        }
    }

    private void restoreOriginalAudioSetup() {
        RTKLogger.d("InCallManager", "restoreOriginalAudioSetup()");
        if (isOrigAudioSetupStored) {
            setSpeakerphoneOn(origIsSpeakerPhoneOn);
            setMicrophoneMute(origIsMicrophoneMute);
            audioManager.setMode(origAudioMode);
            if (getCurrentActivity() != null) {
                getCurrentActivity().setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE);
            }
            isOrigAudioSetupStored = false;
        }
    }

    private void startWiredHeadsetEvent() {
        if (wiredHeadsetReceiver == null) {
            RTKLogger.d("InCallManager", "startWiredHeadsetEvent()");
            IntentFilter filter = new IntentFilter(ACTION_HEADSET_PLUG);
            wiredHeadsetReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (ACTION_HEADSET_PLUG.equals(intent.getAction())) {
                        hasWiredHeadset = intent.getIntExtra("state", 0) == 1;
                        updateAudioRoute();
                        String deviceName = intent.getStringExtra("name");
                        if (deviceName == null) {
                            deviceName = "";
                        }
                        WritableMap data = Arguments.createMap();
                        data.putBoolean("isPlugged", (intent.getIntExtra("state", 0) == 1) ? true : false);
                        data.putBoolean("hasMic", (intent.getIntExtra("microphone", 0) == 1) ? true : false);
                        data.putString("deviceName", deviceName);
                        sendEvent("onAudioDeviceChanged", null);
                    } else {
                        hasWiredHeadset = false;
                    }
                }
            };
            ReactContext reactContext = getReactApplicationContext();
            if (reactContext != null) {
                this.registerReceiver(wiredHeadsetReceiver, filter);
            } else {
                RTKLogger.w("InCallManager", "startWiredHeadsetEvent() reactContext is null");
            }
        }
    }

    private void stopWiredHeadsetEvent() {
        if (wiredHeadsetReceiver != null) {
            RTKLogger.d("InCallManager", "stopWiredHeadsetEvent()");
            this.unregisterReceiver(this.wiredHeadsetReceiver);
            wiredHeadsetReceiver = null;
        }
    }

    private void startNoisyAudioEvent() {
        if (noisyAudioReceiver == null) {
            RTKLogger.d("InCallManager", "startNoisyAudioEvent()");
            IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
            noisyAudioReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
                        updateAudioRoute();
                        sendEvent("NoisyAudio", null);
                    }
                }
            };
            ReactContext reactContext = getReactApplicationContext();
            if (reactContext != null) {
                this.registerReceiver(noisyAudioReceiver, filter);
            } else {
                RTKLogger.w("InCallManager", "startNoisyAudioEvent() reactContext is null");
            }
        }
    }

    private void stopNoisyAudioEvent() {
        if (noisyAudioReceiver != null) {
            RTKLogger.d("InCallManager", "stopNoisyAudioEvent()");
            this.unregisterReceiver(this.noisyAudioReceiver);
            noisyAudioReceiver = null;
        }
    }

    public void onProximitySensorChangedState(boolean isNear) {
        RTKLogger.d("InCallManager", "onProximitySensorChangedState() isNear=" + isNear + ", selectedAudioDevice=" + getSelectedAudioDevice());
        
        if (automatic && getSelectedAudioDevice() == AudioDevice.EARPIECE) {
            if (isNear) {
                turnScreenOff();
            } else {
                turnScreenOn();
            }
            updateAudioRoute();
        }
        WritableMap data = Arguments.createMap();
        data.putBoolean("isNear", isNear);
        sendEvent("Proximity", data);
    }

    @ReactMethod
    public void startProximitySensor() {
        if (!proximityManager.isProximitySupported()) {
            RTKLogger.d("InCallManager", "Proximity Sensor is not supported.");
            return;
        }
        if (isProximityRegistered) {
            RTKLogger.d("InCallManager", "Proximity Sensor is already registered.");
            return;
        }
        // --- SENSOR_DELAY_FASTEST(0 milisecs), SENSOR_DELAY_GAME(20 milisecs), SENSOR_DELAY_UI(60 milisecs), SENSOR_DELAY_NORMAL(200 milisecs)
        if (!proximityManager.start()) {
            RTKLogger.d("InCallManager", "proximityManager.start() failed. return false");
            return;
        }
        RTKLogger.d("InCallManager", "startProximitySensor()");
        isProximityRegistered = true;
    }

    @ReactMethod
    public void stopProximitySensor() {
        if (!proximityManager.isProximitySupported()) {
            RTKLogger.d("InCallManager", "Proximity Sensor is not supported.");
            return;
        }
        if (!isProximityRegistered) {
            RTKLogger.d("InCallManager", "Proximity Sensor is not registered.");
            return;
        }
        RTKLogger.d("InCallManager", "stopProximitySensor()");
        proximityManager.stop();
        isProximityRegistered = false;
    }

    // --- see: https://developer.android.com/reference/android/media/AudioManager
    @Override
    public void onAudioFocusChange(int focusChange) {
        String focusChangeStr;
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                focusChangeStr = "AUDIOFOCUS_GAIN";
                break;
            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
                focusChangeStr = "AUDIOFOCUS_GAIN_TRANSIENT";
                break;
            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
                focusChangeStr = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
                break;
            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
                focusChangeStr = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
                break;
            case AudioManager.AUDIOFOCUS_LOSS:
                focusChangeStr = "AUDIOFOCUS_LOSS";
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                focusChangeStr = "AUDIOFOCUS_LOSS_TRANSIENT";
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                focusChangeStr = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
                break;
            case AudioManager.AUDIOFOCUS_NONE:
                focusChangeStr = "AUDIOFOCUS_NONE";
                break;
            default:
                focusChangeStr = "AUDIOFOCUS_UNKNOW";
                break;
        }

        RTKLogger.d("InCallManager", "onAudioFocusChange(): " + focusChange + " - " + focusChangeStr);

        WritableMap data = Arguments.createMap();
        data.putString("eventText", focusChangeStr);
        data.putInt("eventCode", focusChange);
        sendEvent("onAudioFocusChange", data);
    }

    private void sendEvent(final String eventName, @Nullable WritableMap params) {
        try {
            ReactContext reactContext = getReactApplicationContext();
            if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
                RTKLogger.d("InCallManager", "Sending event: " + eventName);
                reactContext
                        .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                        .emit(eventName, params);
            } else {
                RTKLogger.e("InCallManager", "sendEvent(): reactContext is null or not having CatalystInstance yet.");
            }
        } catch (RuntimeException e) {
            RTKLogger.e("InCallManager", "sendEvent(): java.lang.RuntimeException: Trying to invoke JS before CatalystInstance has been set!");
        }
    }

    /**
     * Starts the audio manager. Default mode is video call (speaker on).
     * Use setVoiceCallMode() to switch to voice call mode (earpiece with proximity sensor).
     */
    @ReactMethod
    public void start() {
        if (!audioManagerActivated) {
            audioManagerActivated = true;

            RTKLogger.d("InCallManager", "start audioRouteManager");
            wakeLockUtils.acquirePartialWakeLock();
            storeOriginalAudioSetup();
            requestAudioFocus();
            startEvents();
            UiThreadUtil.runOnUiThread(() -> {
                bluetoothManager.start();
            });
            audioManager.setMode(defaultAudioMode);
            setSpeakerphoneOn(defaultSpeakerOn);
            setMicrophoneMute(false);
            forceSpeakerOn = 0;
            hasWiredHeadset = hasWiredHeadset();
            defaultAudioDevice = (defaultSpeakerOn) ? AudioDevice.SPEAKER_PHONE : (hasEarpiece()) ? AudioDevice.EARPIECE : AudioDevice.SPEAKER_PHONE;
            userSelectedAudioDevice = AudioDevice.NONE;
            selectedAudioDevice = AudioDevice.NONE;
            audioDevices.clear();
            updateAudioRoute();
        }
    }

    /**
     * Stops the audio manager and releases all resources.
     */
    @ReactMethod
    public void stop() {
        if (audioManagerActivated) {
            RTKLogger.d("InCallManager", "stop() InCallManager");
            stopEvents();
            setSpeakerphoneOn(false);
            setMicrophoneMute(false);
            forceSpeakerOn = 0;
            UiThreadUtil.runOnUiThread(() -> {
                bluetoothManager.stop();
            });
            restoreOriginalAudioSetup();
            abandonAudioFocus();
            audioManagerActivated = false;
            wakeLockUtils.releasePartialWakeLock();
        }
    }

    /**
     * Switches to voice call mode (earpiece with proximity sensor enabled).
     * Call this after start() to enable proximity sensor for voice calls.
     */
    @ReactMethod
    public void setVoiceCallMode() {
        defaultSpeakerOn = false;
        defaultAudioDevice = hasEarpiece() ? AudioDevice.EARPIECE : AudioDevice.SPEAKER_PHONE;
        userSelectedAudioDevice = AudioDevice.NONE;
        updateAudioRoute();
    }

    /**
     * Get all available audio output devices.
     * Returns array of device names: ["SPEAKER_PHONE", "BLUETOOTH", "WIRED_HEADSET", "EARPIECE"]
     */
    @ReactMethod
    public void getAudioDevices(Promise promise) {
        try {
            WritableMap result = Arguments.createMap();
            String devicesJson = "[";
            for (AudioDevice device : audioDevices) {
                devicesJson += "\"" + device.name() + "\",";
            }
            // Strip the last comma
            if (devicesJson.length() > 1) {
                devicesJson = devicesJson.substring(0, devicesJson.length() - 1);
            }
            devicesJson += "]";
            
            result.putString("availableDevices", devicesJson);
            promise.resolve(result);
        } catch (Exception e) {
            promise.reject("ERROR", "Failed to get audio devices", e);
        }
    }

    /**
     * Get the currently active audio output device.
     * Returns device name: "SPEAKER_PHONE", "BLUETOOTH", "WIRED_HEADSET", "EARPIECE", or "NONE"
     */
    @ReactMethod
    public void getCurrentAudioDevice(Promise promise) {
        try {
            WritableMap result = Arguments.createMap();
            
            // Check if audio manager is initialized
            if (selectedAudioDevice == null) {
                result.putString("currentDevice", "NONE");
            } else {
                result.putString("currentDevice", selectedAudioDevice.name());
            }
            
            promise.resolve(result);
        } catch (Exception e) {
            RTKLogger.e("InCallManager", "Failed to get current audio device: " + e.getMessage());
            promise.reject("ERROR", "Failed to get current audio device", e);
        }
    }

    private void startEvents() {
        startWiredHeadsetEvent();
        startNoisyAudioEvent();
        startProximitySensor(); // --- proximity event always enable, but only turn screen off when audio is routing to earpiece.
        setKeepScreenOn(true);
    }

    private void stopEvents() {
        stopWiredHeadsetEvent();
        stopNoisyAudioEvent();
        stopProximitySensor();
        setKeepScreenOn(false);
        turnScreenOn();
    }

    @ReactMethod
    public void requestAudioFocusJS(Promise promise) {
        promise.resolve(requestAudioFocus());
    }

    private String requestAudioFocus() {
        String requestAudioFocusResStr = (android.os.Build.VERSION.SDK_INT >= 26)
                ? requestAudioFocusV26()
                : requestAudioFocusOld();
        RTKLogger.d("InCallManager", "requestAudioFocus(): res = " + requestAudioFocusResStr);
        return requestAudioFocusResStr;
    }

    private String requestAudioFocusV26() {
        if (isAudioFocused) {
            return "";
        }

        if (mAudioAttributes == null) {
            mAudioAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                    .build();
        }

        if (mAudioFocusRequest == null) {
            mAudioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
                    .setAudioAttributes(mAudioAttributes)
                    .setAcceptsDelayedFocusGain(false)
                    .setWillPauseWhenDucked(false)
                    .setOnAudioFocusChangeListener(this)
                    .build();
        }

        int requestAudioFocusRes = audioManager.requestAudioFocus(mAudioFocusRequest);

        String requestAudioFocusResStr;
        switch (requestAudioFocusRes) {
            case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
                requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_FAILED";
                break;
            case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
                isAudioFocused = true;
                requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_GRANTED";
                break;
            case AudioManager.AUDIOFOCUS_REQUEST_DELAYED:
                requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_DELAYED";
                break;
            default:
                requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_UNKNOWN";
                break;
        }

        return requestAudioFocusResStr;
    }

    private String requestAudioFocusOld() {
        if (isAudioFocused) {
            return "";
        }

        int requestAudioFocusRes = audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);

        String requestAudioFocusResStr;
        switch (requestAudioFocusRes) {
            case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
                requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_FAILED";
                break;
            case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
                isAudioFocused = true;
                requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_GRANTED";
                break;
            default:
                requestAudioFocusResStr = "AUDIOFOCUS_REQUEST_UNKNOWN";
                break;
        }

        return requestAudioFocusResStr;
    }

    @ReactMethod
    public void abandonAudioFocusJS(Promise promise) {
        promise.resolve(abandonAudioFocus());
    }

    private String abandonAudioFocus() {
        String abandonAudioFocusResStr = (android.os.Build.VERSION.SDK_INT >= 26)
                ? abandonAudioFocusV26()
                : abandonAudioFocusOld();
        RTKLogger.d("InCallManager", "abandonAudioFocus(): res = " + abandonAudioFocusResStr);
        return abandonAudioFocusResStr;
    }

    private String abandonAudioFocusV26() {
        if (!isAudioFocused || mAudioFocusRequest == null) {
            return "";
        }

        int abandonAudioFocusRes = audioManager.abandonAudioFocusRequest(mAudioFocusRequest);
        String abandonAudioFocusResStr;
        switch (abandonAudioFocusRes) {
            case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
                abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_FAILED";
                break;
            case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
                isAudioFocused = false;
                abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_GRANTED";
                break;
            default:
                abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_UNKNOWN";
                break;
        }

        return abandonAudioFocusResStr;
    }

    private String abandonAudioFocusOld() {
        if (!isAudioFocused) {
            return "";
        }

        int abandonAudioFocusRes = audioManager.abandonAudioFocus(this);

        String abandonAudioFocusResStr;
        switch (abandonAudioFocusRes) {
            case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
                abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_FAILED";
                break;
            case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
                isAudioFocused = false;
                abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_GRANTED";
                break;
            default:
                abandonAudioFocusResStr = "AUDIOFOCUS_REQUEST_UNKNOWN";
                break;
        }

        return abandonAudioFocusResStr;
    }

    @ReactMethod
    public void pokeScreen(int timeout) {
        RTKLogger.d("InCallManager", "pokeScreen()");
        wakeLockUtils.acquirePokeFullWakeLockReleaseAfter(timeout); // --- default 3000 ms
    }

    private void debugScreenPowerState() {
        String isDeviceIdleMode = "unknow"; // --- API 23
        String isIgnoringBatteryOptimizations = "unknow"; // --- API 23
        String isPowerSaveMode = "unknow"; // --- API 21
        String isInteractive = "unknow"; // --- API 20 ( before since API 7 is: isScreenOn())
        String screenState = "unknow"; // --- API 20

        if (android.os.Build.VERSION.SDK_INT >= 23) {
            isDeviceIdleMode = String.format("%s", mPowerManager.isDeviceIdleMode());
            isIgnoringBatteryOptimizations = String.format("%s", mPowerManager.isIgnoringBatteryOptimizations(mPackageName));
        }
        if (android.os.Build.VERSION.SDK_INT >= 21) {
            isPowerSaveMode = String.format("%s", mPowerManager.isPowerSaveMode());
        }
        if (android.os.Build.VERSION.SDK_INT >= 20) {
            isInteractive = String.format("%s", mPowerManager.isInteractive());
            Display display = mWindowManager.getDefaultDisplay();
            switch (display.getState()) {
                case Display.STATE_OFF:
                    screenState = "STATE_OFF";
                    break;
                case Display.STATE_ON:
                    screenState = "STATE_ON";
                    break;
                case Display.STATE_DOZE:
                    screenState = "STATE_DOZE";
                    break;
                case Display.STATE_DOZE_SUSPEND:
                    screenState = "STATE_DOZE_SUSPEND";
                    break;
                default:
                    break;
            }
        } else {
            isInteractive = String.format("%s", mPowerManager.isScreenOn());
        }
        RTKLogger.d("InCallManager", String.format("debugScreenPowerState(): screenState='%s', isInteractive='%s', isPowerSaveMode='%s', isDeviceIdleMode='%s', isIgnoringBatteryOptimizations='%s'", screenState, isInteractive, isPowerSaveMode, isDeviceIdleMode, isIgnoringBatteryOptimizations));
    }

    @ReactMethod
    public void turnScreenOn() {
        if (proximityManager.isProximityWakeLockSupported()) {
            RTKLogger.d("InCallManager", "turnScreenOn(): use proximity lock.");
            proximityManager.releaseProximityWakeLock(true);
        } else {
            RTKLogger.d("InCallManager", "turnScreenOn(): proximity lock is not supported. try manually.");
            manualTurnScreenOn();
        }
    }

    @ReactMethod
    public void turnScreenOff() {
        if (proximityManager.isProximityWakeLockSupported()) {
            RTKLogger.d("InCallManager", "turnScreenOff(): use proximity lock.");
            proximityManager.acquireProximityWakeLock();
        } else {
            RTKLogger.d("InCallManager", "turnScreenOff(): proximity lock is not supported. try manually.");
            manualTurnScreenOff();
        }
    }

    @ReactMethod
    public void setKeepScreenOn(final boolean enable) {
        RTKLogger.d("InCallManager", "setKeepScreenOn() " + enable);
        UiThreadUtil.runOnUiThread(new Runnable() {
            public void run() {
                Activity mCurrentActivity = getCurrentActivity();
                if (mCurrentActivity == null) {
                    RTKLogger.d("InCallManager", "ReactContext doesn't have any Activity attached.");
                    return;
                }
                Window window = mCurrentActivity.getWindow();
                if (enable) {
                    window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
                } else {
                    window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
                }
            }
        });
    }

    @ReactMethod
    public void setSpeakerphoneOn(final boolean enable) {
        if (enable != audioManager.isSpeakerphoneOn())  {
            RTKLogger.d("InCallManager", "setSpeakerphoneOn(): " + enable);
            audioManager.setSpeakerphoneOn(enable);
        }
    }

    // --- TODO (zxcpoiu): These two api name is really confusing. should be changed.
    /**
     * flag: Int
     * 0: use default action
     * 1: force speaker on
     * -1: force speaker off
     */
    @ReactMethod
    public void setForceSpeakerphoneOn(final int flag) {
        if (flag < -1 || flag > 1) {
            return;
        }
        RTKLogger.d("InCallManager", "setForceSpeakerphoneOn() flag: " + flag);
        forceSpeakerOn = flag;

        // --- will call updateAudioDeviceState()
        // --- Note: in some devices, it may not contains specified route thus will not be effected.
        if (flag == 1) {
            selectAudioDevice(AudioDevice.SPEAKER_PHONE);
        } else if (flag == -1) {
            selectAudioDevice(AudioDevice.EARPIECE); // --- use the most common earpiece to force `speaker off`
        } else {
            selectAudioDevice(AudioDevice.NONE); // --- NONE will follow default route, the default route of `video` call is speaker.
        }
    }

    // --- TODO (zxcpoiu): Implement api to let user choose audio devices

    @ReactMethod
    public void setMicrophoneMute(final boolean enable) {
        if (enable != audioManager.isMicrophoneMute())  {
            RTKLogger.d("InCallManager", "setMicrophoneMute(): " + enable);
            audioManager.setMicrophoneMute(enable);
        }
    }

    @ReactMethod
    public void chooseAudioRoute(String audioRoute, Promise promise) {
        RTKLogger.d("InCallManager", "chooseAudioRoute(): user choose audioDevice = " + audioRoute);
        
        if (audioRoute.equals(AudioDevice.EARPIECE.name()) || audioRoute.equals("earpiece")) {
            selectAudioDevice(AudioDevice.EARPIECE);
        } else if (audioRoute.equals(AudioDevice.SPEAKER_PHONE.name()) || audioRoute.equals("speaker")) {
            selectAudioDevice(AudioDevice.SPEAKER_PHONE);
        } else if (audioRoute.equals(AudioDevice.WIRED_HEADSET.name()) || audioRoute.equals("wired")) {
            selectAudioDevice(AudioDevice.WIRED_HEADSET);
        } else if (audioRoute.equals(AudioDevice.BLUETOOTH.name()) || audioRoute.equals("bluetooth")) {
            selectAudioDevice(AudioDevice.BLUETOOTH);
        }
        promise.resolve(getAudioDeviceStatusMap());
    }

    private void pause() {
        if (audioManagerActivated) {
            RTKLogger.d("InCallManager", "pause audioRouteManager");
            stopEvents();
        }
    }

    private void resume() {
        if (audioManagerActivated) {
            RTKLogger.d("InCallManager", "resume audioRouteManager");
            startEvents();
        }
    }

    @Override
    public void onHostResume() {
        RTKLogger.d("InCallManager", "onResume()");
        //resume();
    }

    @Override
    public void onHostPause() {
        RTKLogger.d("InCallManager", "onPause()");
        //pause();
    }

    @Override
    public void onHostDestroy() {
        RTKLogger.d("InCallManager", "onDestroy()");
        stop();
    }

    private void updateAudioRoute() {
        if (!automatic) {
            return;
        }
        updateAudioDeviceState();
    }

// ===== NOTE: below functions is based on appRTC DEMO M64 ===== //
  /** Changes selection of the currently active audio device. */
    private void setAudioDeviceInternal(AudioDevice device) {
        RTKLogger.d("InCallManager", "setAudioDeviceInternal(device=" + device + ")");
        if (!audioDevices.contains(device)) {
            RTKLogger.e("InCallManager", "specified audio device does not exist");
            return;
        }
        switch (device) {
            case SPEAKER_PHONE:
                setSpeakerphoneOn(true);
                audioManager.setWiredHeadsetOn(false);
                stopProximitySensor();
                break;
            case EARPIECE:
                RTKLogger.d("InCallManager", "Routing to EARPIECE");
                audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
                setSpeakerphoneOn(false);
                audioManager.setWiredHeadsetOn(false);
                // Ensure audio is routed to earpiece by stopping any Bluetooth SCO
                if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
                        || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING) {
                    RTKLogger.d("InCallManager", "Stopping Bluetooth SCO for earpiece");
                    bluetoothManager.stopScoAudio();
                }
                startProximitySensor();
                RTKLogger.d("InCallManager", "Earpiece routing complete. Mode: " + audioManager.getMode() 
                    + ", Speaker: " + audioManager.isSpeakerphoneOn());
                break;
            case WIRED_HEADSET:
                audioManager.setWiredHeadsetOn(true);
                setSpeakerphoneOn(false);
                stopProximitySensor();
                break;
            case BLUETOOTH:
                setSpeakerphoneOn(false);
                audioManager.setWiredHeadsetOn(false);
                stopProximitySensor();
                break;
            default:
                RTKLogger.e("InCallManager", "Invalid audio device selection");
                break;
        }
        selectedAudioDevice = device;
    }



    /**
     * Changes default audio device.
     * TODO(henrika): add usage of this method in the AppRTCMobile client.
     */
    public void setDefaultAudioDevice(AudioDevice defaultDevice) {
        switch (defaultDevice) {
            case SPEAKER_PHONE:
                defaultAudioDevice = defaultDevice;
                break;
            case EARPIECE:
                if (hasEarpiece()) {
                    defaultAudioDevice = defaultDevice;
                } else {
                    defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
                }
                break;
            default:
                RTKLogger.e("InCallManager", "Invalid default audio device selection");
                break;
        }
        RTKLogger.d("InCallManager", "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
        updateAudioDeviceState();
    }

    /** Changes selection of the currently active audio device. */
    public void selectAudioDevice(AudioDevice device) {
        RTKLogger.d("InCallManager", "Selecting " + device.name());
        userSelectedAudioDevice = device;
        updateAudioDeviceState();
        if (device != AudioDevice.NONE && !audioDevices.contains(device)) {
            RTKLogger.e("InCallManager", "selectAudioDevice() Can not select " + device + " from available " + audioDevices);
            return;
        }
    }

    /** Returns current set of available/selectable audio devices. */
    public Set<AudioDevice> getAudioDevices() {
        return Collections.unmodifiableSet(new HashSet<>(audioDevices));
    }

    /** Returns the currently selected audio device. */
    public AudioDevice getSelectedAudioDevice() {
        return selectedAudioDevice;
    }

    /** Helper method for receiver registration. */
    private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        ReactContext reactContext = getReactApplicationContext();
        if (reactContext != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                reactContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
            } else {
                reactContext.registerReceiver(receiver, filter);
            }
        }
    }

    /** Helper method for unregistration of an existing receiver. */
    private void unregisterReceiver(final BroadcastReceiver receiver) {
        final ReactContext reactContext = this.getReactApplicationContext();
        if (reactContext != null) {
            try {
                reactContext.unregisterReceiver(receiver);
            } catch (final Exception e) {
                RTKLogger.d("InCallManager", "unregisterReceiver() failed");
            }
        } else {
            RTKLogger.d("InCallManager", "unregisterReceiver() reactContext is null");
        }
    }

    /** Gets the current earpiece state. */
    private boolean hasEarpiece() {
        return getReactApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
    }

    /**
     * Checks whether a wired headset is connected or not.
     * This is not a valid indication that audio playback is actually over
     * the wired headset as audio routing depends on other conditions. We
     * only use it as an early indicator (during initialization) of an attached
     * wired headset.
     */
    @Deprecated
    private boolean hasWiredHeadset() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return audioManager.isWiredHeadsetOn();
        } else {
            final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
            for (AudioDeviceInfo device : devices) {
                final int type = device.getType();
                if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
                    RTKLogger.d("InCallManager", "hasWiredHeadset: found wired headset");
                    return true;
                } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
                    RTKLogger.d("InCallManager", "hasWiredHeadset: found USB audio device");
                    return true;
                } else if (type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) {
                    RTKLogger.d("InCallManager", "hasWiredHeadset: found wired headphones");
                    return true;
                }
            }
            return false;
        }
    }

    @ReactMethod
    public void getIsWiredHeadsetPluggedIn(Promise promise) {
        promise.resolve(this.hasWiredHeadset());
    }

    @ReactMethod
    public void isBluetoothHeadsetConnected(Promise promise) {
        UiThreadUtil.runOnUiThread(() -> {
            try {
                if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
                        || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
                        || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
                    promise.resolve(true);
                    RTKLogger.i("InCallManager", "isBluetoothHeadsetConnected: true");
                } else {
                    promise.resolve(false);
                    RTKLogger.i("InCallManager", "isBluetoothHeadsetConnected: false");
                }
            } catch (Exception e) {
                promise.resolve(false);
                RTKLogger.e("InCallManager", "isBluetoothHeadsetConnected: false due to: " + e.getMessage(), e);
            }
        });
    }
    

    /**
     * Updates list of possible audio devices and make new device selection.
     */
    public void updateAudioDeviceState() {
        UiThreadUtil.runOnUiThread(() -> {
            // Prevent re-entrant calls to avoid infinite loop
            if (isUpdatingAudioDeviceState) {
                RTKLogger.d("InCallManager", "updateAudioDeviceState: Already updating, skipping re-entrant call");
                return;
            }
            isUpdatingAudioDeviceState = true;
            
            try {
                RTKLogger.d("InCallManager", "Bluetooth State: " + bluetoothManager.getState().toString());
            if(bluetoothManager.getState().toString().equals("UNINITIALIZED"))
            bluetoothManager.start();
            hasWiredHeadset= this.hasWiredHeadset();
            RTKLogger.d("InCallManager", "--- updateAudioDeviceState: "
                            + "wired headset=" + hasWiredHeadset + ", "
                            + "BT state=" + bluetoothManager.getState());
            RTKLogger.d("InCallManager", "Device status: "
                            + "available=" + audioDevices + ", "
                            + "selected=" + selectedAudioDevice + ", "
                            + "user selected=" + userSelectedAudioDevice);

            // Check if any Bluetooth headset is connected. The internal BT state will
            // change accordingly.
            if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
                    || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
                    || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_DISCONNECTING) {
                bluetoothManager.updateDevice();
            }

            // Update the set of available audio devices.
            Set<AudioDevice> newAudioDevices = new HashSet<>();

            // always assume device has speaker phone
            newAudioDevices.add(AudioDevice.SPEAKER_PHONE);

            if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
                    || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
                    || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
                newAudioDevices.add(AudioDevice.BLUETOOTH);
            }

            if (hasWiredHeadset) {
                newAudioDevices.add(AudioDevice.WIRED_HEADSET);
            }

            if (hasEarpiece()) {
                newAudioDevices.add(AudioDevice.EARPIECE);
            }

            // --- check whether user selected audio device is available
            if (userSelectedAudioDevice != null
                    && userSelectedAudioDevice != AudioDevice.NONE
                    && !newAudioDevices.contains(userSelectedAudioDevice)) {
                userSelectedAudioDevice = AudioDevice.NONE;
            }

            // Store state which is set to true if the device list has changed.
            boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices);
            // Update the existing audio device set.
            audioDevices = newAudioDevices;

            AudioDevice newAudioDevice = getPreferredAudioDevice();

            // --- stop bluetooth if needed
            if (selectedAudioDevice == AudioDevice.BLUETOOTH
                    && newAudioDevice != AudioDevice.BLUETOOTH
                    && (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
                        || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
                    ) {
                bluetoothManager.stopScoAudio();
                bluetoothManager.updateDevice();
            }

            // --- start bluetooth if needed
            // Start SCO if: switching to Bluetooth OR already on Bluetooth but SCO not connected yet
            if (newAudioDevice == AudioDevice.BLUETOOTH
                    && bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
                // Attempt to start Bluetooth SCO audio (takes a few second to start).
                RTKLogger.d("InCallManager", "Starting Bluetooth SCO audio...");
                if (!bluetoothManager.startScoAudio()) {
                    // Remove BLUETOOTH from list of available devices since SCO failed.
                    RTKLogger.e("InCallManager", "Failed to start Bluetooth SCO audio");
                    audioDevices.remove(AudioDevice.BLUETOOTH);
                    audioDeviceSetUpdated = true;
                    if (userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
                        userSelectedAudioDevice = AudioDevice.NONE;
                    }
                    newAudioDevice = getPreferredAudioDevice();
                } else {
                    RTKLogger.d("InCallManager", "Bluetooth SCO audio start initiated");
                }
            }
            
            // Only fall back from Bluetooth if user didn't explicitly select it
            // If user selected Bluetooth, wait for SCO connection instead of falling back
            if (newAudioDevice == AudioDevice.BLUETOOTH
                    && bluetoothManager.getState() != AppRTCBluetoothManager.State.SCO_CONNECTED
                    && userSelectedAudioDevice != AudioDevice.BLUETOOTH) {
                newAudioDevice = getPreferredAudioDevice(true); // --- skip bluetooth
            }

            // Switch to new device but only if there has been any changes.
            if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {

                // Do the required device switch.
                setAudioDeviceInternal(newAudioDevice);
                RTKLogger.d("InCallManager", "New device status: "
                                + "available=" + audioDevices + ", "
                                + "selected=" + newAudioDevice);
                /*
                if (audioManagerEvents != null) {
                    // Notify a listening client that audio device has been changed.
                    audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
                }
                */
                sendEvent("onAudioDeviceChanged", getAudioDeviceStatusMap());
            }
            RTKLogger.d("InCallManager", "--- updateAudioDeviceState done");
            } finally {
                isUpdatingAudioDeviceState = false;
            }
        });
    }

    private WritableMap getAudioDeviceStatusMap() {
        WritableMap data = Arguments.createMap();
        String audioDevicesJson = "[";
        for (AudioDevice s: audioDevices) {
            audioDevicesJson += s.name() + ",";
        }

        // --- strip the last `,`
        if (audioDevicesJson.length() > 1) {
            audioDevicesJson = audioDevicesJson.substring(0, audioDevicesJson.length() - 1);
        }
        audioDevicesJson += "]";

        data.putString("availableAudioDeviceList", audioDevicesJson);
        data.putString("selectedAudioDevice", (selectedAudioDevice == null) ? "" : selectedAudioDevice.name());

        return data;
    }

    private AudioDevice getPreferredAudioDevice() {
        return getPreferredAudioDevice(false);
    }

    private AudioDevice getPreferredAudioDevice(boolean skipBluetooth) {
        final AudioDevice newAudioDevice;

        if (userSelectedAudioDevice != null && userSelectedAudioDevice != AudioDevice.NONE) {
            newAudioDevice = userSelectedAudioDevice;
        } else if (!skipBluetooth && audioDevices.contains(AudioDevice.BLUETOOTH)) {
            // If a Bluetooth is connected, then it should be used as output audio
            // device. Note that it is not sufficient that a headset is available;
            // an active SCO channel must also be up and running.
            newAudioDevice = AudioDevice.BLUETOOTH;
        } else if (audioDevices.contains(AudioDevice.WIRED_HEADSET)) {
            // If a wired headset is connected, but Bluetooth is not, then wired headset is used as
            // audio device.
            newAudioDevice = AudioDevice.WIRED_HEADSET;
        } else if (audioDevices.contains(defaultAudioDevice)) {
            newAudioDevice = defaultAudioDevice;
        } else {
            newAudioDevice = AudioDevice.SPEAKER_PHONE;
        }

        return newAudioDevice;
    }
}

