#!/usr/pkg/bin/python3.13
"""
Syncthing-GTK - DaemonProcess

Runs syncthing daemon process as subprocess of application
"""


import logging
import os
from collections import deque

from gi.repository import Gio, GLib, GObject

from syncthing_gtk.tools import IS_WINDOWS

log = logging.getLogger("DaemonProcess")

HAS_SUBPROCESS = hasattr(Gio, "Subprocess")
if IS_WINDOWS:
    # POpen is used on Windows
    from subprocess import PIPE, STARTF_USESHOWWINDOW, STARTUPINFO, Popen

    from syncthing_gtk.windows import WinPopenReader, nice_to_priority_class
elif not HAS_SUBPROCESS:
    # Gio.Subprocess is not available in Gio < 3.12
    from subprocess import PIPE, Popen


class DaemonProcess(GObject.GObject):
    __gsignals__ = {
        # line(text) - emitted when process outputs full line
        "line": (GObject.SIGNAL_RUN_FIRST, None, (object,)),
        # exit(code) - emitted when process exits
        "exit": (GObject.SIGNAL_RUN_FIRST, None, (int,)),
        # failed(exception) - emitted if process fails to start
        "failed": (GObject.SIGNAL_RUN_FIRST, None, (object,)),
    }
    SCROLLBACK_SIZE = 500  # Maximum number of output lines stored in memory
    PRIORITY_LOWEST = 19
    PRIORITY_LOW = 10
    PRIORITY_NORMAL = 0
    PRIORITY_HIGH = -10
    PRIORITY_HIGHEST = -20

    def __init__(self, cmdline, priority=PRIORITY_NORMAL, max_cpus=0, env={}):
        """cmdline should be list of arguments"""
        GObject.GObject.__init__(self)
        self.cmdline = cmdline
        self.priority = priority
        self.env = {x: env[x] for x in env}
        self.env["STNORESTART"] = "1"  # see syncthing --help
        self.env["STNOUPGRADE"] = "1"
        if max_cpus > 0:
            self.env["GOMAXPROCS"] = str(max_cpus)
        self._proc = None

    def start(self):
        for x in self.env:
            os.environ[x] = self.env[x]
        try:
            self._cancel = Gio.Cancellable()
            if IS_WINDOWS:
                # Windows
                sinfo = STARTUPINFO()
                sinfo.dwFlags = STARTF_USESHOWWINDOW
                sinfo.wShowWindow = 0
                cflags = nice_to_priority_class(self.priority)
                self._proc = Popen(
                    self.cmdline, stdin=PIPE, stdout=PIPE, stderr=PIPE, startupinfo=sinfo, creationflags=cflags
                )
                self._stdout = WinPopenReader(self._proc.stdout)
                self._check = GLib.timeout_add_seconds(1, self._cb_check_alive)
            elif HAS_SUBPROCESS:
                # New Gio
                flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE
                if self.priority == 0:
                    self._proc = Gio.Subprocess.new(self.cmdline, flags)
                else:
                    # I just really do hope that there is no distro w/out nice command
                    self._proc = Gio.Subprocess.new(["nice", "-n", "%s" % self.priority] + self.cmdline, flags)
                self._proc.wait_check_async(None, self._cb_finished)
                self._stdout = self._proc.get_stdout_pipe()
            else:
                # Gio < 3.12 - Gio.Subprocess is missing :(
                if self.priority == 0:
                    self._proc = Popen(self.cmdline, stdout=PIPE)
                else:
                    # still hoping
                    self._proc = Popen(["nice", "-n", "%s" % self.priority], stdout=PIPE)
                self._stdout = Gio.UnixInputStream.new(self._proc.stdout.fileno(), False)
                self._check = GLib.timeout_add_seconds(1, self._cb_check_alive)
        except Exception as e:
            # Startup failed
            self.emit("failed", e)
            return
        self._lines = deque([], DaemonProcess.SCROLLBACK_SIZE)
        self._buffer = ""
        self._stdout.read_bytes_async(256, 0, self._cancel, self._cb_read, ())

    def _cb_read(self, pipe, results, *a):
        """Handler for read_bytes_async"""
        try:
            response = pipe.read_bytes_finish(results)
        except Exception as e:
            if not self._cancel.is_cancelled():
                log.exception(e)
                GLib.idle_add(pipe.read_bytes_async, 256, 1, None, self._cb_read)
            return
        response = response.get_data().decode("utf-8")
        self._buffer = "%s%s" % (self._buffer, response)
        while "\n" in self._buffer:
            line, self._buffer = self._buffer.split("\n", 1)
            self._lines.append(line)
            self.emit("line", line)
        if not self._cancel.is_cancelled():
            GLib.idle_add(pipe.read_bytes_async, 256, 1, None, self._cb_read, ())

    def _cb_check_alive(self, *a):
        """
        Repeatedly check if process is still alive.
        Called only on windows
        """
        if self._proc is None:
            # Never started or killed really fast
            self.emit("exit", 1)
            self._cancel.cancel()
            if IS_WINDOWS:
                self._stdout.close()
            return False
        self._proc.poll()
        if self._proc.returncode is None:
            # Repeat until finished or canceled
            return not self._cancel.is_cancelled()
        # Child just died :)
        self.emit("exit", self._proc.returncode)
        self._cancel.cancel()
        if IS_WINDOWS:
            self._stdout.close()
        return False

    def _cb_finished(self, proc, results):
        """
        Callback for wait_check_async.
        With Gio < 3.12, timer and _cb_check_alive is used.
        """
        try:
            proc.wait_check_finish(results)
            log.info("Subprocess finished with code %s", proc.get_exit_status())
        except GLib.GError:
            # Exited with exit code
            log.info("Subprocess exited with code %s", proc.get_exit_status())
        if proc.get_exit_status() == 127:
            # Command not found
            self.emit("failed", Exception("Command not found"))
        else:
            self.emit("exit", proc.get_exit_status())
        if IS_WINDOWS:
            self._stdout.close()
        self._cancel.cancel()

    def terminate(self):
        """Terminates process (sends SIGTERM)"""
        if self._proc is not None:
            if IS_WINDOWS:
                # Windows
                self._proc.terminate()
            elif HAS_SUBPROCESS:
                # Gio.Subprocess
                self._proc.send_signal(15)
            else:
                # subprocess.Popen
                self._proc.terminate()
            self._proc = None
            if IS_WINDOWS:
                self._stdout.close()
            self._cancel.cancel()

    def kill(self):
        """Kills process (sends SIGTERM)"""
        if self._proc is not None:
            if IS_WINDOWS:
                # Windows - can't actually kill
                self._proc.terminate()
            elif HAS_SUBPROCESS:
                # Gio.Subprocess
                self._proc.force_exit()
            else:
                # subprocess.Popen
                self._proc.kill()
            self._proc = None
            if IS_WINDOWS:
                self._stdout.close()
            self._cancel.cancel()

    def get_output(self):
        """Returns process output as iterable list of lines"""
        return self._lines

    def get_commandline(self):
        """Returns commandline used to start process"""
        return self.cmdline
