# Copyright 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Facade to access the audio-related functionality."""

import functools
import glob
import logging
import os
import tempfile

from autotest_lib.client.cros import constants
from autotest_lib.client.cros.audio import audio_helper
from autotest_lib.client.cros.audio import cmd_utils
from autotest_lib.client.cros.audio import cras_dbus_utils
from autotest_lib.client.cros.audio import cras_utils
from autotest_lib.client.cros.multimedia import audio_extension_handler


class AudioFacadeNativeError(Exception):
    """Error in AudioFacadeNative."""
    pass


def check_arc_resource(func):
    """Decorator function for ARC related functions in AudioFacadeNative."""
    @functools.wraps(func)
    def wrapper(instance, *args, **kwargs):
        """Wrapper for the methods to check _arc_resource.

        @param instance: Object instance.

        @raises: AudioFacadeNativeError if there is no ARC resource.

        """
        if not instance._arc_resource:
            raise AudioFacadeNativeError('There is no ARC resource.')
        return func(instance, *args, **kwargs)
    return wrapper


class AudioFacadeNative(object):
    """Facede to access the audio-related functionality.

    The methods inside this class only accept Python native types.

    """
    _CAPTURE_DATA_FORMATS = [
            dict(file_type='raw', sample_format='S16_LE',
                 channel=1, rate=48000),
            dict(file_type='raw', sample_format='S16_LE',
                 channel=2, rate=48000)]

    _PLAYBACK_DATA_FORMAT = dict(
            file_type='raw', sample_format='S16_LE', channel=2, rate=48000)

    def __init__(self, resource, arc_resource=None):
        """Initializes an audio facade.

        @param resource: A FacadeResource object.
        @param arc_resource: An ArcResource object.

        """
        self._resource = resource
        self._recorder = None
        self._player = None
        self._counter = None
        self._loaded_extension_handler = None
        self._arc_resource = arc_resource


    @property
    def _extension_handler(self):
        """Multimedia test extension handler."""
        if not self._loaded_extension_handler:
            extension = self._resource.get_extension(
                    constants.MULTIMEDIA_TEST_EXTENSION)
            logging.debug('Loaded extension: %s', extension)
            self._loaded_extension_handler = (
                    audio_extension_handler.AudioExtensionHandler(extension))
        return self._loaded_extension_handler


    def get_audio_devices(self):
        """Returns the audio devices from chrome.audio API.

        @returns: Checks docstring of get_audio_devices of AudioExtensionHandler.

        """
        return self._extension_handler.get_audio_devices()


    def set_chrome_active_volume(self, volume):
        """Sets the active audio output volume using chrome.audio API.

        @param volume: Volume to set (0~100).

        """
        self._extension_handler.set_active_volume(volume)


    def set_chrome_mute(self, mute):
        """Mutes the active audio output using chrome.audio API.

        @param mute: True to mute. False otherwise.

        """
        self._extension_handler.set_mute(mute)


    def get_chrome_active_volume_mute(self):
        """Gets the volume state of active audio output using chrome.audio API.

        @param returns: A tuple (volume, mute), where volume is 0~100, and mute
                        is True if node is muted, False otherwise.

        """
        return self._extension_handler.get_active_volume_mute()


    def set_chrome_active_node_type(self, output_node_type, input_node_type):
        """Sets active node type through chrome.audio API.

        The node types are defined in cras_utils.CRAS_NODE_TYPES.
        The current active node will be disabled first if the new active node
        is different from the current one.

        @param output_node_type: A node type defined in
                                 cras_utils.CRAS_NODE_TYPES. None to skip.
        @param input_node_type: A node type defined in
                                 cras_utils.CRAS_NODE_TYPES. None to skip

        """
        if output_node_type:
            node_id = cras_utils.get_node_id_from_node_type(
                    output_node_type, False)
            self._extension_handler.set_active_node_id(node_id)
        if input_node_type:
            node_id = cras_utils.get_node_id_from_node_type(
                    input_node_type, True)
            self._extension_handler.set_active_node_id(node_id)


    def cleanup(self):
        """Clean up the temporary files."""
        for path in glob.glob('/tmp/playback_*'):
            os.unlink(path)

        for path in glob.glob('/tmp/capture_*'):
            os.unlink(path)

        if self._recorder:
            self._recorder.cleanup()
        if self._player:
            self._player.cleanup()

        if self._arc_resource:
            self._arc_resource.cleanup()


    def playback(self, file_path, data_format, blocking=False):
        """Playback a file.

        @param file_path: The path to the file.
        @param data_format: A dict containing data format including
                            file_type, sample_format, channel, and rate.
                            file_type: file type e.g. 'raw' or 'wav'.
                            sample_format: One of the keys in
                                           audio_data.SAMPLE_FORMAT.
                            channel: number of channels.
                            rate: sampling rate.
        @param blocking: Blocks this call until playback finishes.

        @returns: True.

        @raises: AudioFacadeNativeError if data format is not supported.

        """
        logging.info('AudioFacadeNative playback file: %r. format: %r',
                     file_path, data_format)

        if data_format != self._PLAYBACK_DATA_FORMAT:
            raise AudioFacadeNativeError(
                    'data format %r is not supported' % data_format)

        self._player = Player()
        self._player.start(file_path, blocking)

        return True


    def stop_playback(self):
        """Stops playback process."""
        self._player.stop()


    def start_recording(self, data_format):
        """Starts recording an audio file.

        Currently the format specified in _CAPTURE_DATA_FORMATS is the only
        formats.

        @param data_format: A dict containing:
                            file_type: 'raw'.
                            sample_format: 'S16_LE' for 16-bit signed integer in
                                           little-endian.
                            channel: channel number.
                            rate: sampling rate.


        @returns: True

        @raises: AudioFacadeNativeError if data format is not supported.

        """
        logging.info('AudioFacadeNative record format: %r', data_format)

        if data_format not in self._CAPTURE_DATA_FORMATS:
            raise AudioFacadeNativeError(
                    'data format %r is not supported' % data_format)

        self._recorder = Recorder()
        self._recorder.start(data_format)

        return True


    def stop_recording(self):
        """Stops recording an audio file.

        @returns: The path to the recorded file.

        """
        self._recorder.stop()
        return self._recorder.file_path


    def set_selected_output_volume(self, volume):
        """Sets the selected output volume.

        @param volume: the volume to be set(0-100).

        """
        cras_utils.set_selected_output_node_volume(volume)


    def set_input_gain(self, gain):
        """Sets the system capture gain.

        @param gain: the capture gain in db*100 (100 = 1dB)

        """
        cras_utils.set_capture_gain(gain)


    def set_selected_node_types(self, output_node_types, input_node_types):
        """Set selected node types.

        The node types are defined in cras_utils.CRAS_NODE_TYPES.

        @param output_node_types: A list of output node types.
                                  None to skip setting.
        @param input_node_types: A list of input node types.
                                 None to skip setting.

        """
        cras_utils.set_selected_node_types(output_node_types, input_node_types)


    def get_selected_node_types(self):
        """Gets the selected output and input node types.

        @returns: A tuple (output_node_types, input_node_types) where each
                  field is a list of selected node types defined in
                  cras_utils.CRAS_NODE_TYPES.

        """
        return cras_utils.get_selected_node_types()


    def get_plugged_node_types(self):
        """Gets the plugged output and input node types.

        @returns: A tuple (output_node_types, input_node_types) where each
                  field is a list of plugged node types defined in
                  cras_utils.CRAS_NODE_TYPES.

        """
        return cras_utils.get_plugged_node_types()


    def dump_diagnostics(self, file_path):
        """Dumps audio diagnostics results to a file.

        @param file_path: The path to dump results.

        @returns: True

        """
        with open(file_path, 'w') as f:
            f.write(audio_helper.get_audio_diagnostics())
        return True


    def start_counting_signal(self, signal_name):
        """Starts counting DBus signal from Cras.

        @param signal_name: Signal of interest.

        """
        if self._counter:
            raise AudioFacadeNativeError('There is an ongoing counting.')
        self._counter = cras_dbus_utils.CrasDBusBackgroundSignalCounter()
        self._counter.start(signal_name)


    def stop_counting_signal(self):
        """Stops counting DBus signal from Cras.

        @returns: Number of signals starting from last start_counting_signal
                  call.

        """
        if not self._counter:
            raise AudioFacadeNativeError('Should start counting signal first')
        result = self._counter.stop()
        self._counter = None
        return result


    def wait_for_unexpected_nodes_changed(self, timeout_secs):
        """Waits for unexpected nodes changed signal.

        @param timeout_secs: Timeout in seconds for waiting.

        """
        cras_dbus_utils.wait_for_unexpected_nodes_changed(timeout_secs)


    @check_arc_resource
    def start_arc_recording(self):
        """Starts recording using microphone app in container."""
        self._arc_resource.microphone.start_microphone_app()


    @check_arc_resource
    def stop_arc_recording(self):
        """Checks the recording is stopped and gets the recorded path.

        The recording duration of microphone app is fixed, so this method just
        copies the recorded result from container to a path on Cros device.

        """
        _, file_path = tempfile.mkstemp(prefix='capture_', suffix='.amr-nb')
        self._arc_resource.microphone.stop_microphone_app(file_path)
        return file_path


    @check_arc_resource
    def set_arc_playback_file(self, file_path):
        """Copies the audio file to be played into container.

        User should call this method to put the file into container before
        calling start_arc_playback.

        @param file_path: Path to the file to be played on Cros host.

        @returns: Path to the file in container.

        """
        return self._arc_resource.play_music.set_playback_file(file_path)


    @check_arc_resource
    def start_arc_playback(self, path):
        """Start playback through Play Music app.

        Before calling this method, user should call set_arc_playback_file to
        put the file into container.

        @param path: Path to the file in container.

        """
        self._arc_resource.play_music.start_playback(path)


    @check_arc_resource
    def stop_arc_playback(self):
        """Stop playback through Play Music app."""
        self._arc_resource.play_music.stop_playback()


class RecorderError(Exception):
    """Error in Recorder."""
    pass


class Recorder(object):
    """The class to control recording subprocess.

    Properties:
        file_path: The path to recorded file. It should be accessed after
                   stop() is called.

    """
    def __init__(self):
        """Initializes a Recorder."""
        _, self.file_path = tempfile.mkstemp(prefix='capture_', suffix='.raw')
        self._capture_subprocess = None


    def start(self, data_format):
        """Starts recording.

        Starts recording subprocess. It can be stopped by calling stop().

        @param data_format: A dict containing:
                            file_type: 'raw'.
                            sample_format: 'S16_LE' for 16-bit signed integer in
                                           little-endian.
                            channel: channel number.
                            rate: sampling rate.

        @raises: RecorderError: If recording subprocess is terminated
                 unexpectedly.

        """
        self._capture_subprocess = cmd_utils.popen(
                cras_utils.capture_cmd(
                        capture_file=self.file_path, duration=None,
                        channels=data_format['channel'],
                        rate=data_format['rate']))


    def stop(self):
        """Stops recording subprocess."""
        if self._capture_subprocess.poll() is None:
            self._capture_subprocess.terminate()
        else:
            raise RecorderError(
                    'Recording process was terminated unexpectedly.')


    def cleanup(self):
        """Cleanup the resources.

        Terminates the recording process if needed.

        """
        if self._capture_subprocess and self._capture_subprocess.poll() is None:
            self._capture_subprocess.terminate()


class PlayerError(Exception):
    """Error in Player."""
    pass


class Player(object):
    """The class to control audio playback subprocess.

    Properties:
        file_path: The path to the file to play.

    """
    def __init__(self):
        """Initializes a Player."""
        self._playback_subprocess = None


    def start(self, file_path, blocking):
        """Starts recording.

        Starts recording subprocess. It can be stopped by calling stop().

        @param file_path: The path to the file.
        @param blocking: Blocks this call until playback finishes.

        """
        self._playback_subprocess = cras_utils.playback(
                blocking, playback_file=file_path)


    def stop(self):
        """Stops playback subprocess."""
        cmd_utils.kill_or_log_returncode(self._playback_subprocess)


    def cleanup(self):
        """Cleanup the resources.

        Terminates the playback process if needed.

        """
        self.stop()