# Copyright 2016 Cloudbase Solutions Srl
# All Rights Reserved.
#
#    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.

from eventlet import patcher
from os_win.utils.io import ioutils
from os_win import utilsfactory
from oslo_config import cfg
from oslo_log import log as logging

from nova.console import serial as serial_console
from nova.console import type as ctype
from nova import exception
from nova.i18n import _, _LI
from nova.virt.hyperv import constants
from nova.virt.hyperv import pathutils
from nova.virt.hyperv import serialproxy

CONF = cfg.CONF
LOG = logging.getLogger(__name__)

threading = patcher.original('threading')


class SerialConsoleHandler(object):
    """Handles serial console ops related to a given instance."""
    def __init__(self, instance_name):
        self._vmutils = utilsfactory.get_vmutils()
        self._pathutils = pathutils.PathUtils()

        self._instance_name = instance_name
        self._log_path = self._pathutils.get_vm_console_log_paths(
            self._instance_name)[0]

        self._client_connected = None
        self._input_queue = None
        self._output_queue = None

        self._serial_proxy = None
        self._workers = []

    def start(self):
        self._setup_handlers()

        for worker in self._workers:
            worker.start()

    def stop(self):
        for worker in self._workers:
            worker.stop()

        if self._serial_proxy:
            serial_console.release_port(self._listen_host,
                                        self._listen_port)

    def _setup_handlers(self):
        if CONF.serial_console.enabled:
            self._setup_serial_proxy_handler()

        self._setup_named_pipe_handlers()

    def _setup_serial_proxy_handler(self):
        self._listen_host = (
            CONF.serial_console.proxyclient_address)
        self._listen_port = serial_console.acquire_port(
            self._listen_host)

        LOG.info(_LI('Initializing serial proxy on '
                     '%(addr)s:%(port)s, handling connections '
                     'to instance %(instance_name)s.'),
                     {'addr': self._listen_host,
                      'port': self._listen_port,
                      'instance_name': self._instance_name})

        # Use this event in order to manage
        # pending queue operations.
        self._client_connected = threading.Event()
        self._input_queue = ioutils.IOQueue(
            client_connected=self._client_connected)
        self._output_queue = ioutils.IOQueue(
            client_connected=self._client_connected)

        self._serial_proxy = serialproxy.SerialProxy(
            self._instance_name, self._listen_host,
            self._listen_port, self._input_queue,
            self._output_queue, self._client_connected)

        self._workers.append(self._serial_proxy)

    def _setup_named_pipe_handlers(self):
        # At most 2 named pipes will be used to access the vm serial ports.
        #
        # The named pipe having the 'ro' suffix will be used only for logging
        # while the 'rw' pipe will be used for interactive sessions, logging
        # only when there is no 'ro' pipe.
        serial_port_mapping = self._get_vm_serial_port_mapping()
        log_rw_pipe_output = not serial_port_mapping.get(
            constants.SERIAL_PORT_TYPE_RO)

        for pipe_type, pipe_path in serial_port_mapping.items():
            enable_logging = (pipe_type == constants.SERIAL_PORT_TYPE_RO or
                              log_rw_pipe_output)
            handler = self._get_named_pipe_handler(
                pipe_path,
                pipe_type=pipe_type,
                enable_logging=enable_logging)
            self._workers.append(handler)

    def _get_named_pipe_handler(self, pipe_path, pipe_type,
                                enable_logging):
        kwargs = {}
        if pipe_type == constants.SERIAL_PORT_TYPE_RW:
            kwargs = {'input_queue': self._input_queue,
                      'output_queue': self._output_queue,
                      'connect_event': self._client_connected}
        if enable_logging:
            kwargs['log_file'] = self._log_path

        handler = utilsfactory.get_named_pipe_handler(pipe_path, **kwargs)
        return handler

    def _get_vm_serial_port_mapping(self):
        serial_port_conns = self._vmutils.get_vm_serial_port_connections(
            self._instance_name)

        if not serial_port_conns:
            err_msg = _("No suitable serial port pipe was found "
                        "for instance %(instance_name)s")
            raise exception.NovaException(
                err_msg % {'instance_name': self._instance_name})

        serial_port_mapping = {}
        # At the moment, we tag the pipes by using a pipe path suffix
        # as we can't use the serial port ElementName attribute because of
        # a Hyper-V bug.
        for pipe_path in serial_port_conns:
            # expected pipe_path:
            # '\\.\pipe\fc1bcc91-c7d3-4116-a210-0cd151e019cd_rw'
            port_type = pipe_path[-2:]
            if port_type in [constants.SERIAL_PORT_TYPE_RO,
                             constants.SERIAL_PORT_TYPE_RW]:
                serial_port_mapping[port_type] = pipe_path
            else:
                serial_port_mapping[constants.SERIAL_PORT_TYPE_RW] = pipe_path

        return serial_port_mapping

    def get_serial_console(self):
        if not CONF.serial_console.enabled:
            raise exception.ConsoleTypeUnavailable(console_type='serial')
        return ctype.ConsoleSerial(host=self._listen_host,
                                   port=self._listen_port)
