#!/usr/bin/env python

# ###########################################################################
#
# This file is part of Taurus
#
# http://taurus-scada.org
#
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
#
# Taurus is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Taurus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Taurus.  If not, see <http://www.gnu.org/licenses/>.
#
# ###########################################################################

"""This module provides a very simple API for starting and killing device
servers

It is not a replacement of the Tango Starter Device Server since this is much
more limited in scope.
"""

import os
import time
import subprocess
import PyTango
from taurus.core.util.log import Logger

__all__ = ["Starter", "ProcessStarter"]
__docformat__ = "restructuredtext"

_log = Logger("Starter")


class Starter(object):
    """Abstract class for managing (starting, stopping, registering and
    removing) a Tango Device Server.

    Derived classes should provide the methods for starting and stopping a
    device.
    """

    def __init__(self, ds_name):
        """
        :param ds_name: Device Server name in the form "server/instance"
        :type ds_name: str
        """
        self.ds_name = ds_name
        self.dserver_name = "dserver/%s" % ds_name
        try:
            self.dserver = PyTango.DeviceProxy(self.dserver_name)
            self.serverExisted = True
        except PyTango.DevFailed:  # not registered?
            self.dserver = None
            self.serverExisted = False
        self._addedDevices = []

    def hardKill(self):
        raise NotImplementedError("hardKill method is mandatory")

    def terminate(self):
        raise NotImplementedError("terminate method is mandatory")

    def start(self):
        raise NotImplementedError("start method is mandatory")

    def stopDs(self, synch=True, hard_kill=False, wait_seconds=10):
        if hard_kill:
            _log.info("Hard killing server %s..." % self.ds_name)
            self.hardKill()
        else:
            _log.info("Stopping server %s..." % self.ds_name)
            self.terminate()
        if not synch:
            return
        _log.debug("Waiting for server %s to get stopped" % self.ds_name)
        try:
            self.process.wait(wait_seconds)
        except subprocess.TimeoutExpired:
            _log.warning(
                "Server %s did not stop within %d seconds"
                % (self.ds_name, wait_seconds)
            )
        else:
            _log.info("Server %s has been stopped" % self.ds_name)

    def startDs(self, synch=True, wait_seconds=10):
        if self.isRunning():
            _log.warning("Server already running")
            return
        _log.info("Starting server %s..." % self.ds_name)
        self.start()
        if not synch:
            return
        for i in range(wait_seconds):
            _log.debug(
                "Waiting for server %s to get started... %d"
                % (self.ds_name, i)
            )
            if self.isRunning():
                _log.info("Server %s has been started" % self.ds_name)
                ##############################################################
                # Workaround to avoid race conditions
                # TODO: Find root cause of race condition and fix
                _wait = float(os.environ.get("TAURUS_STARTER_WAIT", 0))
                if _wait:
                    _log.info("Waiting %g s after start" % _wait)
                    time.sleep(_wait)
                ##############################################################
                return
            else:
                time.sleep(1)
        _log.warning(
            "Server %s did not start within %d seconds"
            % (self.ds_name, wait_seconds)
        )

    def addNewDevice(self, device, klass=None):
        """
        Register a device of this server in the DB (register the server if
        not present)
        e.g. to create Starter in an init script::

            addNewDevice('sys/tg_test/foobar', klass='TangoTest')

        :param klass: class name. If None passed, it defaults to the server
            name (without instance name)
        """
        if device in self._addedDevices:
            _log.warning("%s already added. Skipping" % device)
            return
        if klass is None:
            klass = self.ds_name.split("/")[0]
        # in case the device is already defined, skipping...
        db = PyTango.Database()
        try:
            db.import_device(device)
            _log.warning("%s already exists. Skipping" % device)
            return
        except Exception:
            pass
        # register the device,
        # in case the server did not exist before this will define it
        dev_info = PyTango.DbDevInfo()
        dev_info.name = device
        dev_info.klass = klass
        dev_info.server = self.ds_name
        db.add_device(dev_info)
        # create proxy to dserver
        self.dserver = PyTango.DeviceProxy(self.dserver_name)
        # keep track of added devices
        self._addedDevices.append(device)

    def cleanDb(self, force=False):
        """removes devices which have been added by :meth:`addNewDevice`
        and then removes the server if it was registered by this starter
        (or, if force is True, it removes the server in any case)

        :param force: force removing of the Server even if it was not
            registered within this starter
        :type force: bool
        """
        for device in self._addedDevices:
            PyTango.Database().delete_device(device)
            _log.info("Deleted device %s" % device)
        if (self.serverExisted or len(self._addedDevices) == 0) and not force:
            msg = (
                "%s was not registered by this starter. Not removing. "
                + "Use %s.cleanDb(force=True) to force cleanup"
            ) % (self.ds_name, self.__class__.__name__)
            _log.warning(msg)
        else:
            self.stopDs(hard_kill=True)
            PyTango.Database().delete_server(self.ds_name)
            _log.info("Deleted Server %s" % self.ds_name)

    def isRunning(self):
        # TODO: In case the sleeps in startDS and stopDS need to be re-added,
        #       we should study another implementation for this method.
        if self.dserver is None:
            return False
        try:
            self.dserver.ping()
        except PyTango.DevFailed:
            return False
        return True


class ProcessStarter(Starter):
    """A :class:`Starter` which uses subprocess to start and stop a device
    server.
    """

    def __init__(self, execname, ds_name):
        """
        :param execname: path to the executable to launch the server
        :type execname: str
        :param ds_name: Device Server name in the form "server/instance"
        :type ds_name: str
        """
        super(ProcessStarter, self).__init__(ds_name)
        self.ds_instance = ds_name.split("/")[1]
        self.exec_name = os.path.abspath(execname)
        self.process = None

    def start(self):
        dev_null = open(os.devnull, "wb")
        args = [self.exec_name, self.ds_instance]
        self.process = subprocess.Popen(args, stdout=dev_null, stderr=dev_null)

    def terminate(self):
        if self.process:
            try:
                self.dserver.Kill()
            except Exception:
                self.process.terminate()
        else:
            _log.warning("Process not started, cannot terminate it.")

    def hardKill(self):
        if self.process:
            try:
                self.dserver.Kill()  # no hard kill in Tango DS, simply kill it
            except Exception:
                self.process.kill()
        else:
            _log.warning("Process not started, cannot terminate it.")


if __name__ == "__main__":

    from taurus.test.resource import getResourcePath

    exe = getResourcePath("taurus.core.tango.test.res", "TangoSchemeTest")
    s = ProcessStarter(exe, "TangoSchemeTest/test_removeme")
    devname = "testing/tangoschemetest/temp-1"
    s.addNewDevice(devname, klass="TangoSchemeTest")
    s.startDs()
    try:
        print("Is running:", s.isRunning())
        print("ping:", PyTango.DeviceProxy(devname).ping())
    except Exception as e:
        print(e)
    s.stopDs()
    s.cleanDb(force=False)
