# 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.

import logging
import time
from collections import namedtuple
from contextlib import contextmanager

from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.cros.chameleon import chameleon

ChameleonPorts = namedtuple('ChameleonPorts', 'connected failed')


class ChameleonPortFinder(object):
    """
    Responsible for finding all ports connected to the chameleon board.

    It does not verify if these ports are connected to DUT.

    """

    def __init__(self, chameleon_board):
        """
        @param chameleon_board: a ChameleonBoard object representing the
                                Chameleon board whose ports we are interested
                                in finding.

        """
        self.chameleon_board = chameleon_board
        self.connected = None
        self.failed = None


    def find_all_ports(self):
        """
        @returns a named tuple ChameleonPorts() containing a list of connected
                 ports as the first element and failed ports as second element.

        """
        self.connected = self.chameleon_board.get_all_ports()
        self.failed = []

        return ChameleonPorts(self.connected, self.failed)


    def find_port(self, interface):
        """
        @param interface: string, the interface. e.g: HDMI, DP, VGA
        @returns a ChameleonPort object if port is found, else None.

        """
        connected_ports = self.find_all_ports().connected

        for port in connected_ports:
            if port.get_connector_type().lower() == interface.lower():
                return port

        return None


    def __str__(self):
        ports_to_str = lambda ports: ', '.join(
                '%s(%d)' % (p.get_connector_type(), p.get_connector_id())
                for p in ports)

        if self.connected is None:
            text = 'No port information. Did you run find_all_ports()?'
        elif self.connected == []:
            text = 'No port detected on the Chameleon board.'
        else:
            text = ('Detected %d connected port(s): %s. \t'
                    % (len(self.connected), ports_to_str(self.connected)))

        if self.failed:
            text += ('DUT failed to detect Chameleon ports: %s'
                     % ports_to_str(self.failed))

        return text


class ChameleonInputFinder(ChameleonPortFinder):
    """
    Responsible for finding all input ports connected to the chameleon board.

    """

    def find_all_ports(self):
        """
        @returns a named tuple ChameleonPorts() containing a list of connected
                 input ports as the first element and failed ports as second
                 element.

        """
        self.connected = self.chameleon_board.get_all_inputs()
        self.failed = []

        return ChameleonPorts(self.connected, self.failed)


class ChameleonOutputFinder(ChameleonPortFinder):
    """
    Responsible for finding all output ports connected to the chameleon board.

    """

    def find_all_ports(self):
        """
        @returns a named tuple ChameleonPorts() containing a list of connected
                 output ports as the first element and failed ports as the
                 second element.

        """
        self.connected = self.chameleon_board.get_all_outputs()
        self.failed = []

        return ChameleonPorts(self.connected, self.failed)


class ChameleonVideoInputFinder(ChameleonInputFinder):
    """
    Responsible for finding all video inputs connected to the chameleon board.

    It also verifies if these ports are connected to DUT.

    """

    REPLUG_DELAY_SEC = 1

    def __init__(self, chameleon_board, display_facade):
        """
        @param chameleon_board: a ChameleonBoard object representing the
                                Chameleon board whose ports we are interested
                                in finding.
        @param display_facade: a display facade object, to access the DUT
                               display functionality, either locally or
                               remotely.

        """
        super(ChameleonVideoInputFinder, self).__init__(chameleon_board)
        self.display_facade = display_facade
        self._TIMEOUT_VIDEO_STABLE_PROBE = 10


    def _yield_all_ports(self, failed_ports=None, raise_error=False):
        """
        Yields all connected video ports and ensures every of them plugged.

        @param failed_ports: A list to append the failed port or None.
        @param raise_error: True to raise TestFail if no connected video port.
        @yields every connected ChameleonVideoInput which is ensured plugged
                before yielding.

        @raises TestFail if raise_error is True and no connected video port.

        """
        yielded = False
        all_ports = super(ChameleonVideoInputFinder, self).find_all_ports()

        # unplug all ports
        for port in all_ports.connected:
            if port.has_video_support():
                chameleon.ChameleonVideoInput(port).unplug()
                # This is the workaround for samus with hdmi connection.
                self.display_facade.reset_connector_if_applicable(
                        port.get_connector_type())

        for port in all_ports.connected:
            # Skip the non-video port.
            if not port.has_video_support():
                continue

            video_port = chameleon.ChameleonVideoInput(port)
            connector_type = video_port.get_connector_type()
            # Plug the port to make it visible.
            video_port.plug()
            try:
                # DUT takes some time to respond. Wait until the video signal
                # to stabilize and wait for the connector change.
                video_stable = video_port.wait_video_input_stable(
                        self._TIMEOUT_VIDEO_STABLE_PROBE)
                output = utils.wait_for_value_changed(
                        self.display_facade.get_external_connector_name,
                        old_value=False)

                if not output:
                    logging.warn('Maybe flaky that no display detected. Retry.')
                    video_port.unplug()
                    time.sleep(self.REPLUG_DELAY_SEC)
                    video_port.plug()
                    video_stable = video_port.wait_video_input_stable(
                            self._TIMEOUT_VIDEO_STABLE_PROBE)
                    output = utils.wait_for_value_changed(
                            self.display_facade.get_external_connector_name,
                            old_value=False)

                logging.info('CrOS detected external connector: %r', output)

                if output:
                    yield video_port
                    yielded = True
                else:
                    if failed_ports is not None:
                       failed_ports.append(video_port)
                    logging.error('CrOS failed to see any external display')
                    if not video_stable:
                        logging.warn('Chameleon timed out waiting CrOS video')
            finally:
                # Unplug the port not to interfere with other tests.
                video_port.unplug()

        if raise_error and not yielded:
            raise error.TestFail('No connected video port found between CrOS '
                                 'and Chameleon.')


    def iterate_all_ports(self):
        """
        Iterates all connected video ports and ensures every of them plugged.

        It is used via a for statement, like the following:

            finder = ChameleonVideoInputFinder(chameleon_board, display_facade)
            for chameleon_port in finder.iterate_all_ports()
                # chameleon_port is automatically plugged before this line.
                do_some_test_on(chameleon_port)
                # chameleon_port is automatically unplugged after this line.

        @yields every connected ChameleonVideoInput which is ensured plugged
                before yeilding.

        @raises TestFail if no connected video port.

        """
        return self._yield_all_ports(raise_error=True)


    @contextmanager
    def use_first_port(self):
        """
        Use the first connected video port and ensures it plugged.

        It is used via a with statement, like the following:

            finder = ChameleonVideoInputFinder(chameleon_board, display_facade)
            with finder.use_first_port() as chameleon_port:
                # chameleon_port is automatically plugged before this line.
                do_some_test_on(chameleon_port)
                # chameleon_port is automatically unplugged after this line.

        @yields the first connected ChameleonVideoInput which is ensured plugged
                before yeilding.

        @raises TestFail if no connected video port.

        """
        for port in self._yield_all_ports(raise_error=True):
            yield port
            break


    def find_all_ports(self):
        """
        @returns a named tuple ChameleonPorts() containing a list of connected
                 video inputs as the first element and failed ports as second
                 element.

        """
        dut_failed_ports = []
        connected_ports = list(self._yield_all_ports(dut_failed_ports))
        self.connected = connected_ports
        self.failed = dut_failed_ports

        return ChameleonPorts(connected_ports, dut_failed_ports)


class ChameleonAudioInputFinder(ChameleonInputFinder):
    """
    Responsible for finding all audio inputs connected to the chameleon board.

    It does not verify if these ports are connected to DUT.

    """

    def find_all_ports(self):
        """
        @returns a named tuple ChameleonPorts() containing a list of connected
                 audio inputs as the first element and failed ports as second
                 element.

        """
        all_ports = super(ChameleonAudioInputFinder, self).find_all_ports()
        self.connected = [chameleon.ChameleonAudioInput(port)
                          for port in all_ports.connected
                          if port.has_audio_support()]
        self.failed = []

        return ChameleonPorts(self.connected, self.failed)


class ChameleonAudioOutputFinder(ChameleonOutputFinder):
    """
    Responsible for finding all audio outputs connected to the chameleon board.

    It does not verify if these ports are connected to DUT.

    """

    def find_all_ports(self):
        """
        @returns a named tuple ChameleonPorts() containing a list of connected
                 audio outputs as the first element and failed ports as second
                 element.

        """
        all_ports = super(ChameleonAudioOutputFinder, self).find_all_ports()
        self.connected = [chameleon.ChameleonAudioOutput(port)
                          for port in all_ports.connected
                          if port.has_audio_support()]
        self.failed = []

        return ChameleonPorts(self.connected, self.failed)