class Driver(object):
    """This is generic Driver class implementation."""

    platform = 'generic'
    inventory_cmd = None
    users_cmd = None
    target_prompt_components = ['prompt_dynamic']
    prepare_terminal_session = ['terminal len 0']
    config_cmd = 'configure terminal'
    families = {}

    def __init__(self, device):
        """Initialize the Driver object."""
        self.device = device

        # FIXME: Do something with this, it's insane
        self.prompt_re = pattern_manager.pattern(self.platform, 'prompt')
        self.syntax_error_re = pattern_manager.pattern(self.platform, 'syntax_error')
        self.connection_closed_re = pattern_manager.pattern(self.platform, 'connection_closed')
        self.press_return_re = pattern_manager.pattern(self.platform, 'press_return')
        self.more_re = pattern_manager.pattern(self.platform, 'more')
        self.rommon_re = pattern_manager.pattern(self.platform, 'rommon')
        self.buffer_overflow_re = pattern_manager.pattern(self.platform, 'buffer_overflow')

        self.username_re = pattern_manager.pattern(self.platform, 'username')
        self.password_re = pattern_manager.pattern(self.platform, 'password')
        self.authentication_error_re = pattern_manager.pattern(self.platform, 'authentication_error')
        self.unable_to_connect_re = pattern_manager.pattern(self.platform, 'unable_to_connect')
        self.timeout_re = pattern_manager.pattern(self.platform, 'timeout')
        self.standby_re = pattern_manager.pattern(self.platform, 'standby')

        self.pid2platform_re = pattern_manager.pattern(self.platform, 'pid2platform')
        self.platform_re = pattern_manager.pattern(self.platform, 'platform', compiled=False)
        self.version_re = pattern_manager.pattern(self.platform, 'version', compiled=False)
        self.vty_re = pattern_manager.pattern(self.platform, 'vty')
        self.console_re = pattern_manager.pattern(self.platform, 'console')

        self.plane = 'sdr'

        self.log = device.chain.connection.log
        self.platform_string = ""
        self.raw_family = None

    def __repr__(self):
        """Return the string representation of the driver class."""
        return str(self.platform)

    def get_version_text(self):
        """Return the version information from the device."""
        show_version_brief_not_supported = False
        version_text = None
        try:
            version_text = self.device.send("show version brief", timeout=120)
        except CommandError:
            show_version_brief_not_supported = True

        if show_version_brief_not_supported:
            try:
                # IOS Hack - need to check if show version brief is supported on IOS/IOS XE
                version_text = self.device.send("show version", timeout=120)
            except CommandError as exc:
                exc.command = 'show version'
                raise exc

        return version_text

    def get_inventory_text(self):
        """Return the inventory information from the device."""
        inventory_text = None
        if self.inventory_cmd:
            try:
                inventory_text = self.device.send(self.inventory_cmd, timeout=120)
                self.log('Inventory collected')
            except CommandError:
                self.log('Unable to collect inventory')
        else:
            self.log('No inventory command for {}'.format(self.platform))
        return inventory_text

    def get_hostname_text(self):  # pylint: disable=no-self-use
        """Return the hostname information from the device."""
        return None

    def get_users_text(self):
        """Return the users logged in information from the device."""
        users_text = None
        if self.users_cmd:
            try:
                users_text = self.device.send(self.users_cmd, timeout=60)
            except CommandError:
                self.log('Unable to collect connected users information')
        else:
            self.log('No users command for {}'.format(self.platform))
        return users_text

    def get_os_type(self, version_text):  # pylint: disable=no-self-use
        """Return the OS type information from the device."""
        os_type = None
        if version_text is None:
            return os_type

        match = re.search("(XR|XE|NX-OS)", version_text)
        if match:
            os_type = match.group(1)
        else:
            os_type = 'IOS'

        if os_type == "XR":
            match = re.search("Build Information", version_text)
            if match:
                os_type = "eXR"
            match = re.search("XR Admin Software", version_text)
            if match:
                os_type = "Calvados"
        return os_type

    def get_os_version(self, version_text):
        """Return the OS version information from the device."""
        os_version = None
        if version_text is None:
            return os_version
        match = re.search(self.version_re, version_text, re.MULTILINE)
        if match:
            os_version = match.group(1)

        return os_version

    def get_hw_family(self, version_text):
        """Return the HW family information from the device."""
        family = None
        if version_text is None:
            return family

        match = re.search(self.platform_re, version_text, re.MULTILINE)
        if match:
            self.platform_string = match.group()
            self.log("Platform string: {}".format(match.group()))
            self.raw_family = match.group(1)
            # sort keys on len reversed (longest first)
            for key in sorted(self.families, key=len, reverse=True):
                if self.raw_family.startswith(key):
                    family = self.families[key]
                    break
            else:
                self.log("Platform {} not supported".format(family))
        else:
            self.log("Platform string not present. Refer to CSCux08958")
        return family

    def get_hw_platform(self, udi):
        """Return th HW platform information from the device."""
        platform = None
        try:
            pid = udi['pid']
            if pid == '':
                self.log("Empty PID. Use the hw family from the platform string.")
                return self.raw_family
            match = re.search(self.pid2platform_re, pid)
            if match:
                platform = match.group(1)
        except KeyError:
            pass
        return platform

    def is_console(self, users_text):
        """Return if device is connected over console."""
        if users_text is None:
            self.log("Console information not collected")
            return None

        for line in users_text.split('\n'):
            if '*' in line:
                match = re.search(self.vty_re, line)
                if match:
                    self.log("Detected connection to vty")
                    return False
                else:
                    match = re.search(self.console_re, line)
                    if match:
                        self.log("Detected connection to console")
                        return True

        self.log("Connection port unknown")
        return None

    def update_driver(self, prompt):
        """Update driver based on the prompt."""
        # Do not update the driver if not target device
        return pattern_manager.platform(prompt) if self.device.is_target else None

    def wait_for_string(self, expected_string, timeout=60):
        """Wait for string FSM."""
        #                    0                         1                        2                        3
        events = [self.syntax_error_re, self.connection_closed_re, expected_string, self.press_return_re,
                  #        4           5                 6                7
                  self.more_re, pexpect.TIMEOUT, pexpect.EOF, self.buffer_overflow_re]

        # add detected prompts chain
        events += self.device.get_previous_prompts()  # without target prompt

        self.log("Expecting: {}".format(pattern_to_str(expected_string)))

        transitions = [
            (self.syntax_error_re, [0], -1, CommandSyntaxError("Command unknown", self.device.hostname), 0),
            (self.connection_closed_re, [0], 1, a_connection_closed, 10),
            (pexpect.TIMEOUT, [0], -1, CommandTimeoutError("Timeout waiting for prompt", self.device.hostname), 0),
            (pexpect.EOF, [0, 1], -1, ConnectionError("Unexpected device disconnect", self.device.hostname), 0),
            (self.more_re, [0], 0, partial(a_send, " "), 10),
            (expected_string, [0, 1], -1, a_expected_prompt, 0),
            (self.press_return_re, [0], -1, a_stays_connected, 0),
            # TODO: Customize in XR driver
            (self.buffer_overflow_re, [0], -1, CommandSyntaxError("Command too long", self.device.hostname), 0)
        ]

        for prompt in self.device.get_previous_prompts():
            transitions.append((prompt, [0, 1], 0, a_unexpected_prompt, 0))

        fsm = FSM("WAIT-4-STRING", self.device, events, transitions, timeout=timeout)
        return fsm.run()

    # def send_xml(self, command, timeout=60):
    #     """
    #     Handle error i.e.
    #     ERROR: 0x24319600 'XML-TTY' detected the 'informational' condition
    #     'The XML TTY Agent has not yet been started.
    #     Check that the configuration 'xml agent tty' has been committed.'
    #     """
    #     self._debug("Starting XML TTY Agent")
    #     result = self.send("xml")
    #     self._info("XML TTY Agent started")
    #
    #     result = self.send(command, timeout=timeout)
    #     self.ctrl.sendcontrol('c')
    #     return result

    # def netconf(self, command):
    #     """
    #     Handle error i.e.
    #     ERROR: 0x24319600 'XML-TTY' detected the 'informational' condition
    #     'The XML TTY Agent has not yet been started.
    #     Check that the configuration 'xml agent tty' has been committed.'
    #     """
    #     self._debug("Starting XML TTY Agent")
    #     result = self.send("netconf", wait_for_string=']]>]]>')
    #     self._info("XML TTY Agent started")
    #
    #     self.ctrl.send(command)
    #     self.ctrl.send("\r\n")
    #     self.ctrl.expect("]]>]]>")
    #     result = self.ctrl.before
    #     self.ctrl.sendcontrol('c')
    #     self.send()
    #     return result

    def enable(self, enable_password):
        """Change the device mode to privileged.

        If device does not support privileged mode the
        the informational message to the log will be posted.

        Args:
            enable_password (str): The privileged mode password. This is optional parameter. If password is not
                provided but required the password from url will be used. Refer to :class:`condoor.Connection`
        """
        self.log("Privileged mode not supported on {} platform".format(self.platform))

    def reload(self, reload_timeout=300, save_config=True):
        """Reload the device and waits for device to boot up.

        It posts the informational message to the log if not implemented by device driver.
        """
        self.log("Reload not implemented on {} platform".format(self.platform))

    def after_connect(self):
        """Execute right after connecting to the device."""
        pass

    def base_prompt(self, prompt):
        """Extract the base prompt pattern."""
        if prompt is None:
            return None
        if not self.device.is_target:
            return prompt
        pattern = pattern_manager.pattern(self.platform, "prompt_dynamic", compiled=False)
        pattern = pattern.format(prompt="(?P<prompt>.*?)")
        result = re.search(pattern, prompt)
        if result:
            base = result.group("prompt") + "#"
            self.log("base prompt: {}".format(base))
            return base
        else:
            self.log("Unable to extract the base prompt")
            return prompt

    def make_dynamic_prompt(self, prompt):
        """Extend prompt with flexible mode handling regexp."""
        patterns = ["[\r\n]" + pattern_manager.pattern(
            self.platform, pattern_name, compiled=False) for pattern_name in self.target_prompt_components]

        hostname = self.update_hostname(prompt)
        # patterns_re = "|".join(patterns).format(prompt=re.escape(prompt[:-1]))
        patterns_re = "|".join(patterns).format(hostname=re.escape(hostname))

        try:
            prompt_re = re.compile(patterns_re, re.MULTILINE)
        except re.error as e:  # pylint: disable=invalid-name
            raise RuntimeError("Pattern compile error: {} ({}:{})".format(e.message, self.platform, patterns_re))

        self.log("Platform: {} -> Dynamic prompt: '{}'".format(self.platform, repr(prompt_re.pattern)))
        return prompt_re

    def update_config_mode(self, prompt):  # pylint: disable=no-self-use
        """Update config mode based on the prompt analysis."""
        mode = 'global'
        if prompt:
            if 'config' in prompt:
                mode = 'config'
            elif 'admin' in prompt:
                mode = 'admin'

        self.log("Mode: {}".format(mode))
        return mode

    def update_hostname(self, prompt):
        """Update the hostname based on the prompt analysis."""
        result = re.search(self.prompt_re, prompt)
        if result:
            hostname = result.group('hostname')
            self.log("Hostname detected: {}".format(hostname))
        else:
            hostname = self.device.hostname
            self.log("Hostname not set: {}".format(prompt))
        return hostname

    def config(self, text, plane):
        """Apply config."""
        self.log("Device configuration not supported.")
        return None

    def rollback(self, label, plane):
        """Rollback config."""
        self.log("Device configuration rollback not supported.")
        return None

    def enter_plane(self, plane):
        """Enter the device plane.

        Enter the device plane a.k.a. mode, i.e. admin, qnx, calvados
        """
        try:
            cmd = CONF['driver'][self.platform]['planes'][plane]
            self.plane = plane
        except KeyError:
            cmd = None

        if cmd:
            self.log("Entering the {} plane".format(plane))
            self.device.send(cmd)

    def exit_plane(self):
        """Exit the device plane."""
        if self.plane != 'sdr':
            self.device.send('exit') @action
def a_send_password(password, ctx):
    """Send the password text.

    Before sending the password local echo is disabled.
    If password not provided it disconnects from the device and raises ConnectionAuthenticationError exception.
    """
    if password:
        ctx.ctrl.send_command(password, password=True)
        return True
    else:
        ctx.ctrl.disconnect()
        raise ConnectionAuthenticationError("Password not provided", ctx.ctrl.hostname) @action
def a_expected_prompt(ctx):
    """Update driver, config mode and hostname when received an expected prompt."""
    prompt = ctx.ctrl.match.group(0)
    ctx.device.update_driver(prompt)
    ctx.device.update_config_mode()
    ctx.device.update_hostname()
    ctx.finished = True
    return True @action
def a_send_line(text, ctx):
    """Send text line to the controller followed by `os.linesep`."""
    if hasattr(text, '__iter__'):
        try:
            ctx.ctrl.sendline(text.next())
        except StopIteration:
            ctx.finished = True
    else:
        ctx.ctrl.sendline(text)
    return True @action
def a_send(text, ctx):
    """Send text line to the controller."""
    ctx.ctrl.send(text)
    return True @action
def a_disconnect(ctx):
    """Disconnect from the device when device is reloading."""
    ctx.msg = "Device is reloading"
    ctx.ctrl.disconnect()
    return True @action
def a_reconnect(ctx):
    """Reconnect."""
    ctx.device.connect(ctx.ctrl)
    return True class ConnectionAuthenticationError(ConnectionError):
    """Connection authentication error.""" class ConnectionError(GeneralError):
    """General connection error.""" class CommandError(GeneralError):
    """Command execution error."""

    def __init__(self, message=None, host=None, command=None):
        """Initialize CommandError object.

        Args:
            message (str): Custom message to be passed to the exceptions. Defaults to *None*.
                If *None* then the general class *__doc__* is used.
            host (str): Custom string which can be used to enhance the exception message by adding the "`host`: "
                prefix to the message string. Defaults to *None*. If `host` is *None* then message stays unchanged.
            command (str): Custom string which can be used enhance the exception message by adding the
                "`command`" suffix to the message string. Defaults to *None*. If `command` is *None* then the message
                stays unchanged.
        """
        GeneralError.__init__(self, message, host)
        self.command = command

    def __str__(self):
        """Return string representative of the exception."""
        message = self.message or self.__class__.__doc__
        message = "{}: '{}'".format(message, self.command) \
            if self.command else message
        message = "{}: {}".format(self.hostname, message) \
            if self.hostname else message
        return message class CommandSyntaxError(CommandError):
    """Command syntax error."""

    pass class CommandTimeoutError(CommandError):
    """Command timeout error."""

    pass class FSM(object):
    """This class represents Finite State Machine for the current device connection.

    Here is the example of usage::

        to be done


    The example action::

        def send_newline(ctx):
            ctx.ctrl.sendline()
            return True

        def error(ctx):
            ctx.message = "Filesystem error"
            return False

        def readonly(ctx):
            ctx.message = "Filesystem is readonly"
            return False

    The ctx object description refer to :class:`condoor.controllers.fsm.FSM`.

    If the action returns True then the FSM continues processing. If the action returns False then FSM stops
    and the error message passed back to the ctx object is posted to the log.


    The FSM state is the integer number. The FSM starts with initial ``state=0`` and finishes if the ``next_state``
    is set to -1.

    If action returns False then FSM returns False. FSM returns True if reaches the -1 state.

    """

    class Context(object):
        """FSM Context class."""

        _slots__ = ('fsm_name', 'ctrl', 'event', 'state', 'finished', 'msg', 'pattern', 'device')

        def __init__(self, fsm_name, device):
            """Initialize the FSM context object.

            Args:
                fsm_name (str): Name of the FSM. This is used for logging.
                device (object): The device object.
            """
            self.device = device
            self.ctrl = device.ctrl
            self.fsm_name = fsm_name
            self.event = None
            self.state = 0
            self.finished = False
            self.msg = ""
            self.pattern = None

        def __str__(self):
            """Return the string representing the FSM context."""
            return "FSM Context:E={},S={},FI={},M='{}'".format(
                self.event, self.state, self.finished, self.msg)

    def __init__(self, name, device, events, transitions, init_pattern=None, timeout=300, searchwindowsize=None,
                 max_transitions=20):
        """Initialize FSM object.

        Args:
            name (str): Name of the state machine used for logging purposes. Can't be *None*
            ctrl (object): Controller class representing the connection to the device
            events (list): List of expected strings or pexpect.TIMEOUT exception expected from the device.
            transitions (list): List of tuples in defining the state machine transitions.
            init_pattern (str): The pattern that was expected in the previous operation.
            timeout (int): Timeout between states in seconds. Defaults to 300 seconds.
            searchwindowsize (int): The size of search window. Defaults to -1.
            max_transitions (int): Max number of transitions allowed before quiting the FSM.

        The transition tuple format is as follows::

            (event, [list_of_states], next_state, action, timeout)

        - event (str): string from the `events` list which is expected to be received from device.
        - list_of_states (list): List of FSM states that triggers the action in case of event occurrence.
        - next_state (int): Next state for FSM transition.
        - action (func): function to be executed if the current FSM state belongs to `list_of_states` and the `event`
          occurred. The action can be also *None* then FSM transits to the next state without any action. Action
          can be also the exception, which is raised and FSM stops.
        """
        self.events = events
        self.device = device
        self.ctrl = device.ctrl
        self.timeout = timeout
        self.searchwindowsize = searchwindowsize
        self.name = name
        self.init_pattern = init_pattern
        self.max_transitions = max_transitions
        self.log = device.chain.connection.log

        self.transition_table = self._compile(transitions, events)

    def _compile(self, transitions, events):
        compiled = {}
        for transition in transitions:
            event, states, new_state, act, timeout = transition
            if not isinstance(states, list):
                states = list(states)
            try:
                event_index = events.index(event)
            except ValueError:
                self.log("Transition for non-existing event: {}".format(
                    event if isinstance(event, str) else event.pattern))
            else:
                for state in states:
                    key = (event_index, state)
                    compiled[key] = (new_state, act, timeout)

        return compiled

    def run(self):
        """Start the FSM.

        Returns:
            boolean: True if FSM reaches the last state or false if the exception or error message was raised

        """
        ctx = FSM.Context(self.name, self.device)
        transition_counter = 0
        timeout = self.timeout
        self.log("{} Start".format(self.name))
        while transition_counter < self.max_transitions:
            transition_counter += 1
            try:
                start_time = time()
                if self.init_pattern is None:
                    ctx.event = self.ctrl.expect(self.events, searchwindowsize=self.searchwindowsize, timeout=timeout)
                else:
                    self.log("INIT_PATTERN={}".format(pattern_to_str(self.init_pattern)))
                    try:
                        ctx.event = self.events.index(self.init_pattern)
                    except ValueError:
                        self.log("INIT_PATTERN unknown.")
                        continue
                    finally:
                        self.init_pattern = None

                finish_time = time() - start_time
                key = (ctx.event, ctx.state)
                ctx.pattern = self.events[ctx.event]

                if key in self.transition_table:
                    transition = self.transition_table[key]
                    next_state, action_instance, next_timeout = transition
                    self.log("E={},S={},T={},RT={:.2f}".format(ctx.event, ctx.state, timeout, finish_time))
                    if callable(action_instance) and not isclass(action_instance):
                        if not action_instance(ctx):
                            self.log("Error: {}".format(ctx.msg))
                            return False
                    elif isinstance(action_instance, Exception):
                        self.log("A=Exception {}".format(action_instance))
                        raise action_instance
                    elif action_instance is None:
                        self.log("A=None")
                    else:
                        self.log("FSM Action is not callable: {}".format(str(action_instance)))
                        raise RuntimeWarning("FSM Action is not callable")

                    if next_timeout != 0:  # no change if set to 0
                        timeout = next_timeout
                    ctx.state = next_state
                    self.log("NS={},NT={}".format(next_state, timeout))

                else:
                    self.log("Unknown transition: EVENT={},STATE={}".format(ctx.event, ctx.state))
                    continue

            except EOF:
                raise ConnectionError("Session closed unexpectedly", self.ctrl.hostname)

            if ctx.finished or next_state == -1:
                self.log("{} Stop at E={},S={}".format(self.name, ctx.event, ctx.state))
                return True

        # check while else if even exists
        self.log("FSM looped. Exiting")
        return False
from functools import partial
from condoor.drivers.generic import Driver as Generic
from condoor.actions import a_send_password, a_expected_prompt, a_send_line, a_send, a_disconnect, a_reconnect
from condoor.exceptions import ConnectionAuthenticationError, ConnectionError, CommandError, CommandSyntaxError, \
    CommandTimeoutError
from condoor.fsm import FSM
import re
import pexpect
"""This is IOS river implementation."""




SAVE_CONFIG = re.compile(re.escape("System configuration has been modified. Save? [yes/no]: "))
PROCEED = re.compile(re.escape("Proceed with reload? [confirm]"))


class Driver(Generic):
    """This is a Driver class implementation for IOS."""

    platform = 'IOS'
    inventory_cmd = 'show inventory'
    users_cmd = 'show users'
    enable_cmd = 'enable'
    reload_cmd = 'reload'
    target_prompt_components = ['prompt_dynamic', 'prompt_default', 'rommon']
    prepare_terminal_session = ['terminal len 0', 'terminal width 0']
    families = {
        'A9': 'ASR900',
        'IOSv': 'IOSv',
    }

    def __init__(self, device):
        """Initialize the IOS Driver object."""
        super(Driver, self).__init__(device)

    def get_version_text(self):
        """Return the version information from IOS device."""
        version_text = None
        try:
            version_text = self.device.send("show version", timeout=120)
        except CommandError as exc:
            exc.command = 'show version'
            raise exc

        return version_text

    def enable(self, enable_password):
        """Change to the privilege mode."""
        if self.device.prompt[-1] == '#':
            self.log("Device is already in privileged mode")
            return

        events = [self.password_re, self.device.prompt_re, pexpect.TIMEOUT, pexpect.EOF]
        transitions = [
            (self.password_re, [0], 1, partial(a_send_password, enable_password), 10),
            (self.password_re, [1], -1, ConnectionAuthenticationError("Incorrect enable password",
                                                                      self.device.hostname), 0),
            (self.device.prompt_re, [0, 1, 2, 3], -1, a_expected_prompt, 0),

            (pexpect.TIMEOUT, [0, 1, 2], -1, ConnectionAuthenticationError("Unable to get privileged mode",