#!/usr/bin/env python3
#
# 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.
"""
Test script to automate the Bluetooth Audio Funhaus.
"""
from acts.keys import Config
from acts_contrib.test_utils.bt.BtMetricsBaseTest import BtMetricsBaseTest
from acts_contrib.test_utils.bt.bt_test_utils import bluetooth_enabled_check
from acts.utils import bypass_setup_wizard
from acts.utils import exe_cmd
from acts.utils import sync_device_time
import time
import os

BT_CONF_PATH = "/data/misc/bluedroid/bt_config.conf"


class BtFunhausBaseTest(BtMetricsBaseTest):
    """
    Base class for Bluetooth A2DP audio tests, this class is in charge of
    pushing link key to Android device so that it could be paired with remote
    A2DP device, pushing music to Android device, playing audio, monitoring
    audio play, and stop playing audio
    """
    music_file_to_play = ""
    device_fails_to_connect_list = []

    def __init__(self, controllers):
        BtMetricsBaseTest.__init__(self, controllers)
        self.ad = self.android_devices[0]
        self.dongle = self.relay_devices[0]

    def _pair_devices(self):
        self.ad.droid.bluetoothStartPairingHelper(False)
        self.dongle.enter_pairing_mode()

        self.ad.droid.bluetoothBond(self.dongle.mac_address)

        end_time = time.time() + 20
        self.ad.log.info("Verifying devices are bonded")
        while time.time() < end_time:
            bonded_devices = self.ad.droid.bluetoothGetBondedDevices()

            for d in bonded_devices:
                if d['address'] == self.dongle.mac_address:
                    self.ad.log.info("Successfully bonded to device.")
                    self.log.info("Bonded devices:\n{}".format(bonded_devices))
                return True
        self.ad.log.info("Failed to bond devices.")
        return False

    def setup_test(self):
        super(BtFunhausBaseTest, self).setup_test()
        self.dongle.setup()
        tries = 5
        # Since we are not concerned with pairing in this test, try 5 times.
        while tries > 0:
            if self._pair_devices():
                return True
            else:
                tries -= 1
        return False

    def teardown_test(self):
        super(BtFunhausBaseTest, self).teardown_test()
        self.dongle.clean_up()
        return True

    def on_fail(self, test_name, begin_time):
        self.dongle.clean_up()
        self._collect_bluetooth_manager_dumpsys_logs(self.android_devices)
        super(BtFunhausBaseTest, self).on_fail(test_name, begin_time)

    def setup_class(self):
        if not super(BtFunhausBaseTest, self).setup_class():
            return False
        for ad in self.android_devices:
            sync_device_time(ad)
            # Disable Bluetooth HCI Snoop Logs for audio tests
            ad.adb.shell("setprop persist.bluetooth.btsnoopenable false")
            if not bypass_setup_wizard(ad):
                self.log.debug(
                    "Failed to bypass setup wizard, continuing test.")
                # Add music to the Android device
        return self._add_music_to_android_device(ad)

    def _add_music_to_android_device(self, ad):
        """
        Add music to Android device as specified by the test config
        :param ad: Android device
        :return: True on success, False on failure
        """
        self.log.info("Pushing music to the Android device.")
        music_path_str = "bt_music"
        android_music_path = "/sdcard/Music/"
        if music_path_str not in self.user_params:
            self.log.error("Need music for audio testcases...")
            return False
        music_path = self.user_params[music_path_str]
        if type(music_path) is list:
            self.log.info("Media ready to push as is.")
        elif not os.path.isdir(music_path):
            music_path = os.path.join(
                self.user_params[Config.key_config_path.value], music_path)
            if not os.path.isdir(music_path):
                self.log.error(
                    "Unable to find music directory {}.".format(music_path))
                return False
        if type(music_path) is list:
            for item in music_path:
                self.music_file_to_play = item
                ad.adb.push("{} {}".format(item, android_music_path))
        else:
            for dirname, dirnames, filenames in os.walk(music_path):
                for filename in filenames:
                    self.music_file_to_play = filename
                    file = os.path.join(dirname, filename)
                    # TODO: Handle file paths with spaces
                    ad.adb.push("{} {}".format(file, android_music_path))
        ad.reboot()
        return True

    def _collect_bluetooth_manager_dumpsys_logs(self, ads):
        """
        Collect "adb shell dumpsys bluetooth_manager" logs
        :param ads: list of active Android devices
        :return: None
        """
        for ad in ads:
            serial = ad.serial
            out_name = "{}_{}".format(serial, "bluetooth_dumpsys.txt")
            dumpsys_path = ''.join((ad.log_path, "/BluetoothDumpsys"))
            os.makedirs(dumpsys_path, exist_ok=True)
            cmd = ''.join(
                ("adb -s ", serial, " shell dumpsys bluetooth_manager > ",
                 dumpsys_path, "/", out_name))
            exe_cmd(cmd)

    def start_playing_music_on_all_devices(self):
        """
        Start playing music
        :return: None
        """
        self.ad.droid.mediaPlayOpen("file:///sdcard/Music/{}".format(
            self.music_file_to_play.split("/")[-1]))
        self.ad.droid.mediaPlaySetLooping(True)
        self.ad.log.info("Music is now playing.")

    def monitor_music_play_util_deadline(self, end_time, sleep_interval=1):
        """
        Monitor music play on all devices, if a device's Bluetooth adapter is
        OFF or if a device is not connected to any remote Bluetooth devices,
        we add them to failure lists bluetooth_off_list and
        device_not_connected_list respectively
        :param end_time: The deadline in epoch floating point seconds that we
            must stop playing
        :param sleep_interval: How often to monitor, too small we may drain
            too much resources on Android, too big the deadline might be passed
            by a maximum of this amount
        :return:
            status: False iff all devices are off or disconnected otherwise True
            bluetooth_off_list: List of ADs that have Bluetooth at OFF state
            device_not_connected_list: List of ADs with no remote device
                                        connected
        """
        device_not_connected_list = []
        while time.time() < end_time:
            if not self.ad.droid.bluetoothCheckState():
                self.ad.log.error("Device {}'s Bluetooth state is off.".format(
                    self.ad.serial))
                return False
            if self.ad.droid.bluetoothGetConnectedDevices() == 0:
                self.ad.log.error(
                    "Bluetooth device not connected. Failing test.")
            time.sleep(sleep_interval)
        return True

    def play_music_for_duration(self, duration, sleep_interval=1):
        """
        A convenience method for above methods. It starts run music on all
        devices, monitors the health of music play and stops playing them when
        time passes the duration
        :param duration: Duration in floating point seconds
        :param sleep_interval: How often to check the health of music play
        :return:
            status: False iff all devices are off or disconnected otherwise True
            bluetooth_off_list: List of ADs that have Bluetooth at OFF state
            device_not_connected_list: List of ADs with no remote device
                                        connected
        """
        start_time = time.time()
        end_time = start_time + duration
        self.start_playing_music_on_all_devices()
        status = self.monitor_music_play_util_deadline(end_time,
                                                       sleep_interval)
        self.ad.droid.mediaPlayStopAll()
        return status
