# coding: utf-8
"""QuantLab command handler"""

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from __future__ import print_function

import atexit
import logging
import os
import re
import signal
import sys
import threading
import time
import weakref

from tornado import gen

from .qlpmapp import which, subprocess

try:
    import pty
except ImportError:
    pty = False

if sys.platform == 'win32':
    list2cmdline = subprocess.list2cmdline
else:
    def list2cmdline(cmd_list):
        import pipes
        return ' '.join(map(pipes.quote, cmd_list))


logging.basicConfig(format='%(message)s', level=logging.INFO)


class Process(object):
    """A wrapper for a child process.
    """
    _procs = weakref.WeakSet()
    _pool = None

    def __init__(self, cmd, logger=None, cwd=None, kill_event=None,
                 env=None):
        """Start a subprocess that can be run asynchronously.
        Parameters
        ----------
        cmd: list
            The command to run.
        logger: :class:`~logger.Logger`, optional
            The logger instance.
        cwd: string, optional
            The cwd of the process.
        env: dict, optional
            The environment for the process.
        kill_event: :class:`~threading.Event`, optional
            An event used to kill the process operation.
        """
        if not isinstance(cmd, (list, tuple)):
            raise ValueError('Command must be given as a list')

        if kill_event and kill_event.is_set():
            raise ValueError('Process aborted')

        self.logger = logger = logger or logging.getLogger('quantlab')
        self._last_line = ''
        self.logger.info('> ' + list2cmdline(cmd))
        self.cmd = cmd

        self.proc = self._create_process(cwd=cwd, env=env)
        self._kill_event = kill_event or threading.Event()

        Process._procs.add(self)

    def terminate(self):
        """Terminate the process and return the exit code.
        """
        proc = self.proc

        # Kill the process.
        if proc.poll() is None:
            try:
                os.kill(proc.pid, signal.SIGTERM)
            except Exception as e:
                self.logger.error(str(e))

        # Wait for the process to close.
        try:
            proc.wait()
        except Exception as e:
            self.logger.error(e)
        finally:
            Process._procs.remove(self)

        return proc.returncode

    def wait(self):
        """Wait for the process to finish.
        Returns
        -------
        The process exit code.
        """
        proc = self.proc
        kill_event = self._kill_event
        try:
            while proc.poll() is None:
                if kill_event.is_set():
                    self.terminate()
                    raise ValueError('Process Aborted')
                time.sleep(1.)
        except subprocess.CalledProcessError as error:
            output = error.output.decode('utf-8')
            self.logger.error(output)
            self.terminate()
            raise error

        return self.terminate()

    @gen.coroutine
    def wait_async(self):
        """Asynchronously wait for the process to finish.
        """
        proc = self.proc
        kill_event = self._kill_event
        try:
            while proc.poll() is None:
                if kill_event.is_set():
                    self.terminate()
                    raise ValueError('Process Aborted')
                yield gen.sleep(1.)
        except subprocess.CalledProcessError as error:
            output = error.output.decode('utf-8')
            self.logger.error(output)
            self.terminate()
            raise error

        raise gen.Return(self.terminate())

    def _create_process(self, **kwargs):
        """Create the process.
        """
        cmd = self.cmd
        kwargs.setdefault('stderr', subprocess.STDOUT)

        if os.name == 'nt':
            kwargs['shell'] = True

        cmd[0] = which(cmd[0], kwargs.get('env'))

        try:
            proc = subprocess.Popen(cmd, **kwargs)
        except subprocess.CalledProcessError as error:
            output = error.output.decode('utf-8')
            self.logger.error(output)
            raise error

        return proc

    @classmethod
    def _cleanup(cls):
        """Clean up the started subprocesses at exit.
        """
        for proc in list(cls._procs):
            proc.terminate()


class WatchHelper(Process):
    """A process helper for a watch process.
    """

    def __init__(self, cmd, startup_regex, logger=None, cwd=None,
            kill_event=None, env=None):
        """Initialize the process helper.
        Parameters
        ----------
        cmd: list
            The command to run.
        startup_regex: string
            The regex to wait for at startup.
        logger: :class:`~logger.Logger`, optional
            The logger instance.
        cwd: string, optional
            The cwd of the process.
        env: dict, optional
            The environment for the process.
        kill_event: callable, optional
            A function to call to check if we should abort.
        """
        super(WatchHelper, self).__init__(cmd, logger=logger,
            cwd=cwd, kill_event=kill_event, env=env)

        if not pty:
            self._stdout = self.proc.stdout

        while 1:
            line = self._stdout.readline().decode('utf-8')
            if not line:
                raise RuntimeError('Process ended improperly')
            print(line.rstrip())
            if re.match(startup_regex, line):
                break

        self._read_thread = threading.Thread(target=self._read_incoming)
        self._read_thread.setDaemon(True)
        self._read_thread.start()

    def terminate(self):
        """Terminate the process.
        """
        proc = self.proc

        if proc.poll() is None:
            if os.name != 'nt':
                # Kill the process group if we started a new session.
                os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
            else:
                os.kill(proc.pid, signal.SIGTERM)

        # Close stdout.
        try:
            self._stdout.close()
        except Exception as e:
            pass

        # Wait for the process to close.
        try:
            proc.wait()
        except Exception as e:
            print('on close')
            self.logger.error(e)
        finally:
            Process._procs.remove(self)

        return proc.returncode

    def _read_incoming(self):
        """Run in a thread to read stdout and print"""
        fileno = self._stdout.fileno()
        while 1:
            try:
                buf = os.read(fileno, 1024)
            except OSError as e:
                self.logger.debug('Read incoming error %s', e)
                return

            if not buf:
                return

            print(buf.decode('utf-8'), end='')

    def _create_process(self, **kwargs):
        """Create the watcher helper process.
        """
        kwargs['bufsize'] = 0

        if pty:
            master, slave = pty.openpty()
            kwargs['stderr'] = kwargs['stdout'] = slave
            kwargs['start_new_session'] = True
            self._stdout = os.fdopen(master, 'rb')
        else:
            kwargs['stdout'] = subprocess.PIPE

            if os.name == 'nt':
                startupinfo = subprocess.STARTUPINFO()
                startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
                kwargs['startupinfo'] = startupinfo
                kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP

        return super(WatchHelper, self)._create_process(**kwargs)


# Register the cleanup handler.
atexit.register(Process._cleanup)
