/*
 * Copyright (C) 2016 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.android.settings.bluetooth;

import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.text.Editable;
import android.util.Log;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;

import androidx.annotation.VisibleForTesting;

import com.android.settings.R;
import com.android.settings.bluetooth.BluetoothPairingDialogFragment.BluetoothPairingDialogListener;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfile;

import java.util.Locale;

/**
 * A controller used by {@link BluetoothPairingDialog} to manage connection state while we try to
 * pair with a bluetooth device. It includes methods that allow the
 * {@link BluetoothPairingDialogFragment} to interrogate the current state as well.
 */
public class BluetoothPairingController implements OnCheckedChangeListener,
        BluetoothPairingDialogListener {

    private static final String TAG = "BTPairingController";

    // Different types of dialogs we can map to
    public static final int INVALID_DIALOG_TYPE = -1;
    public static final int USER_ENTRY_DIALOG = 0;
    public static final int CONFIRMATION_DIALOG = 1;
    public static final int DISPLAY_PASSKEY_DIALOG = 2;

    private static final int BLUETOOTH_PIN_MAX_LENGTH = 16;
    private static final int BLUETOOTH_PASSKEY_MAX_LENGTH = 6;

    // Bluetooth dependencies for the connection we are trying to establish
    LocalBluetoothManager mBluetoothManager;
    private BluetoothDevice mDevice;
    @VisibleForTesting
    int mType;
    private String mUserInput;
    private String mPasskeyFormatted;
    private int mPasskey;
    private int mInitiator;
    private String mDeviceName;
    private LocalBluetoothProfile mPbapClientProfile;
    private boolean mPbapAllowed;
    private boolean mIsCoordinatedSetMember;
    private boolean mIsLeAudio;
    private boolean mIsLateBonding;

    /**
     * Creates an instance of a BluetoothPairingController.
     *
     * @param intent - must contain {@link BluetoothDevice#EXTRA_PAIRING_VARIANT}, {@link
     * BluetoothDevice#EXTRA_PAIRING_KEY}, and {@link BluetoothDevice#EXTRA_DEVICE}. Missing extra
     * will lead to undefined behavior.
     */
    public BluetoothPairingController(Intent intent, Context context) {
        mBluetoothManager = Utils.getLocalBtManager(context);
        mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

        String message = "";
        if (mBluetoothManager == null) {
            throw new IllegalStateException("Could not obtain LocalBluetoothManager");
        } else if (mDevice == null) {
            throw new IllegalStateException("Could not find BluetoothDevice");
        }

        mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR);
        mPasskey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR);
        mInitiator =
                intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_INITIATOR, BluetoothDevice.ERROR);
        mDeviceName = mBluetoothManager.getCachedDeviceManager().getName(mDevice);
        mPbapClientProfile = mBluetoothManager.getProfileManager().getPbapClientProfile();
        mPasskeyFormatted = formatKey(mPasskey);
        mIsLateBonding = mBluetoothManager.getCachedDeviceManager().isLateBonding(mDevice);

        final CachedBluetoothDevice cachedDevice =
                mBluetoothManager.getCachedDeviceManager().findDevice(mDevice);

        mIsCoordinatedSetMember = false;
        mIsLeAudio = false;
        if (cachedDevice != null) {
            mIsCoordinatedSetMember = cachedDevice.isCoordinatedSetMemberDevice();

            for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
                if (profile.getProfileId() == BluetoothProfile.LE_AUDIO) {
                    mIsLeAudio = true;
                }
            }
            Log.d(TAG, "isCooridnatedSetMember: " + mIsCoordinatedSetMember);
        }
    }

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (isChecked) {
            mPbapAllowed = true;
        } else {
            mPbapAllowed = false;
        }
    }

    @Override
    public void onDialogPositiveClick(BluetoothPairingDialogFragment dialog) {
        if (mPbapAllowed) {
            mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
        } else {
            mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
        }

        if (getDialogType() == USER_ENTRY_DIALOG) {
            onPair(mUserInput);
        } else {
            onPair(null);
        }
    }

    @Override
    public void onDialogNegativeClick(BluetoothPairingDialogFragment dialog) {
        mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
        onCancel();
    }

    /**
     * A method for querying which bluetooth pairing dialog fragment variant this device requires.
     *
     * @return - The dialog view variant needed for this device.
     */
    public int getDialogType() {
        switch (mType) {
            case BluetoothDevice.PAIRING_VARIANT_PIN:
            case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
                return USER_ENTRY_DIALOG;

            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
            case BluetoothDevice.PAIRING_VARIANT_CONSENT:
            case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
                return CONFIRMATION_DIALOG;

            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
                return DISPLAY_PASSKEY_DIALOG;

            default:
                return INVALID_DIALOG_TYPE;
        }
    }

    /**
     * @return - A string containing the name provided by the device.
     */
    public String getDeviceName() {
        return mDeviceName;
    }

    /**
     * A method for querying if the bluetooth device is a LE coordinated set member device.
     *
     * @return - A boolean indicating if the device is a CSIP supported device.
     */
    public boolean isCoordinatedSetMemberDevice() {
        return mIsCoordinatedSetMember;
    }

    /**
     * A method for querying if the bluetooth device from a coordinated set is bonding late.
     *
     * @return - A boolean indicating if the device is bonding late.
     */
    public boolean isLateBonding() {
        return mIsLateBonding;
    }

    /**
     * A method for querying if the bluetooth device has a profile already set up on this device.
     *
     * @return - A boolean indicating if the device has previous knowledge of a profile for this
     * device.
     */
    public boolean isProfileReady() {
        return mPbapClientProfile != null && mPbapClientProfile.isProfileReady();
    }

    @VisibleForTesting
    boolean isLeAudio() {
        return mIsLeAudio;
    }

    /**
     * A method whether the device allows to show the le audio's contact sharing.
     *
     * @return A boolean whether the device allows to show the contact sharing.
     */
    public boolean isContactSharingVisible() {
        return !isProfileReady();
    }

    /**
     * A method for querying if the bluetooth device has access to contacts on the device.
     *
     * @return - A boolean indicating if the bluetooth device has permission to access the device
     * contacts
     */
    public boolean getContactSharingState() {
        switch (mDevice.getPhonebookAccessPermission()) {
            case BluetoothDevice.ACCESS_ALLOWED:
                return true;
            case BluetoothDevice.ACCESS_REJECTED:
                return false;
            default:
                if (BluetoothUtils.isDeviceClassMatched(
                        mDevice, BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE)) {
                    return BluetoothDevice.EXTRA_PAIRING_INITIATOR_FOREGROUND == mInitiator;
                }
                return false;
        }
    }

    /**
     * Update Phone book permission
     *
     */
     public void setContactSharingState() {
         final int permission = mDevice.getPhonebookAccessPermission();
         if (permission == BluetoothDevice.ACCESS_ALLOWED
                 || (permission == BluetoothDevice.ACCESS_UNKNOWN
                 && BluetoothUtils.isDeviceClassMatched(mDevice,
                 BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE))) {
             onCheckedChanged(null, true);
         } else {
             onCheckedChanged(null, false);
         }

     }

    /**
     * A method for querying if the provided editable is a valid passkey/pin format for this device.
     *
     * @param s - The passkey/pin
     * @return - A boolean indicating if the passkey/pin is of the correct format.
     */
    public boolean isPasskeyValid(Editable s) {
        boolean requires16Digits = mType == BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS;
        return s.length() >= 16 && requires16Digits || s.length() > 0 && !requires16Digits;
    }

    /**
     * A method for querying what message should be shown to the user as additional text in the
     * dialog for this device. Returns -1 to indicate a device type that does not use this message.
     *
     * @return - The message ID to show the user.
     */
    public int getDeviceVariantMessageId() {
        switch (mType) {
            case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
            case BluetoothDevice.PAIRING_VARIANT_PIN:
                return R.string.bluetooth_enter_pin_other_device;

            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
                return R.string.bluetooth_enter_passkey_other_device;

            default:
                return INVALID_DIALOG_TYPE;
        }
    }

    /**
     * A method for querying what message hint should be shown to the user as additional text in the
     * dialog for this device. Returns -1 to indicate a device type that does not use this message.
     *
     * @return - The message ID to show the user.
     */
    public int getDeviceVariantMessageHintId() {
        switch (mType) {
            case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
                return R.string.bluetooth_pin_values_hint_16_digits;

            case BluetoothDevice.PAIRING_VARIANT_PIN:
            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
                return R.string.bluetooth_pin_values_hint;

            default:
                return INVALID_DIALOG_TYPE;
        }
    }

    /**
     * A method for querying the maximum passkey/pin length for this device.
     *
     * @return - An int indicating the maximum length
     */
    public int getDeviceMaxPasskeyLength() {
        switch (mType) {
            case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
            case BluetoothDevice.PAIRING_VARIANT_PIN:
                return BLUETOOTH_PIN_MAX_LENGTH;

            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
                return BLUETOOTH_PASSKEY_MAX_LENGTH;

            default:
                return 0;
        }

    }

    /**
     * A method for querying if the device uses an alphanumeric passkey.
     *
     * @return - a boolean indicating if the passkey can be alphanumeric.
     */
    public boolean pairingCodeIsAlphanumeric() {
        switch (mType) {
            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
                return false;

            default:
                return true;
        }
    }

    /**
     * A method used by the dialogfragment to notify the controller that the dialog has been
     * displayed for bluetooth device types that just care about it being displayed.
     */
    protected void notifyDialogDisplayed() {
        // send an OK to the framework, indicating that the dialog has been displayed.
        if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
            mDevice.setPairingConfirmation(true);
        } else if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) {
            mDevice.setPin(mPasskeyFormatted);
        }
    }

    /**
     * A method for querying if this bluetooth device type has a key it would like displayed
     * to the user.
     *
     * @return - A boolean indicating if a key exists which should be displayed to the user.
     */
    public boolean isDisplayPairingKeyVariant() {
        switch (mType) {
            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
            case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
                return true;
            default:
                return false;
        }
    }

    /**
     * A method for querying if this bluetooth device type has other content it would like displayed
     * to the user.
     *
     * @return - A boolean indicating if content exists which should be displayed to the user.
     */
    public boolean hasPairingContent() {
        switch (mType) {
            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
                return true;

            default:
                return false;
        }
    }

    /**
     * A method for obtaining any additional content this bluetooth device has for displaying to the
     * user.
     *
     * @return - A string containing the additional content, null if none exists.
     * @see {@link BluetoothPairingController#hasPairingContent()}
     */
    public String getPairingContent() {
        if (hasPairingContent()) {
            return mPasskeyFormatted;
        } else {
            return null;
        }
    }

    /**
     * A method that exists to allow the fragment to update the controller with input the user has
     * provided in the fragment.
     *
     * @param input - A string containing the user input.
     */
    protected void updateUserInput(String input) {
        mUserInput = input;
    }

    /**
     * Returns the provided passkey in a format that this device expects. Only works for numeric
     * passkeys/pins.
     *
     * @param passkey - An integer containing the passkey to format.
     * @return - A string containing the formatted passkey/pin
     */
    private String formatKey(int passkey) {
        switch (mType) {
            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
                return String.format(Locale.getDefault(), "%06d", passkey);

            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
                return String.format("%04d", passkey);

            default:
                return null;
        }
    }

    /**
     * handles the necessary communication with the bluetooth device to establish a successful
     * pairing
     *
     * @param passkey - The passkey we will attempt to pair to the device with.
     */
    private void onPair(String passkey) {
        Log.d(TAG, "Pairing dialog accepted");
        switch (mType) {
            case BluetoothDevice.PAIRING_VARIANT_PIN:
            case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
                mDevice.setPin(passkey);
                break;


            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
            case BluetoothDevice.PAIRING_VARIANT_CONSENT:
                mDevice.setPairingConfirmation(true);
                break;

            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
            case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
                // Do nothing.
                break;

            default:
                Log.e(TAG, "Incorrect pairing type received");
        }
    }

    /**
     * A method for properly ending communication with the bluetooth device. Will be called by the
     * {@link BluetoothPairingDialogFragment} when it is dismissed.
     */
    public void onCancel() {
        Log.d(TAG, "Pairing dialog canceled");
        mDevice.cancelBondProcess();
    }

    /**
     * A method for checking if this device is equal to another device.
     *
     * @param device - The other device being compared to this device.
     * @return - A boolean indicating if the devices were equal.
     */
    public boolean deviceEquals(BluetoothDevice device) {
        return mDevice == device;
    }

    @VisibleForTesting
    void mockPbapClientProfile(LocalBluetoothProfile mockPbapClientProfile) {
        mPbapClientProfile = mockPbapClientProfile;
    }
}
