from util import pmap

class Directory(object):
    """A Directory is a collection of related Services and Settings.
    Once a Directory has been populated, it should be linked and then
    all services should be setup.  After this, services can be started
    and stopped in some service-dependent order.

    The link phase may assume that all objects have been initialized,
    but not linked.  This phase is meant for discovering related
    services and settings in the directory.

    The setup phase can assume that all objects have been linked, but
    not setup.  This phase is meant for installing files and preparing
    a host to run the service.  Because there are no dependency
    assumptions, all setup methods can be run in parallel."""

    def __init__(self):
        self.__services = {}
        self.__allServices = []
        self.__settings = {}
        self.__allSettings = []

    def __iadd__(self, obj):
        """Add a Service or Setting.  Services must be unique by class
        and host.  Settings must be unique by class.  Settings are
        registered under *all* of their superclasses leading up to the
        Setting superclass itself."""

        if isinstance(obj, Service):
            # XXX This doesn't handle services that can be
            # distinguished by port on the same host.  If we made this
            # explicit, we could also automatically generate service
            # names.
            hostMap = self.__services.setdefault(type(obj), {})
            existing = hostMap.get(obj.host)
            if existing:
                raise RuntimeError(
                    "Service %s already running on %s; cannot add %s" %
                    (existing.name, obj.host, obj.name))
            hostMap[obj.host] = obj
            self.__allServices.append(obj)
        elif isinstance(obj, Setting):
            for cls in type(obj).__mro__:
                if cls == Setting:
                    break
                if cls in self.__settings:
                    raise RuntimeError("Settings %s already registered" % cls)
                self.__settings[cls] = obj
            self.__allSettings.append(obj)
        else:
            raise RuntimeError(
                "Cannot add object of %s to directory" %
                type(obj))
        return self

    def __getitem__(self, cls):
        """Get a Service or Setting of the given class.  For Services,
        this will raise a KeyError if the service is not unique.
        Raises KeyError for unknown services and settings."""

        if issubclass(cls, Service):
            hostMap = self.getService(cls)
            if len(hostMap) != 1:
                raise KeyError("Multiple registered services of %s" % cls)
            return hostMap.values()[0]
        elif issubclass(cls, Setting):
            if cls not in self.__settings:
                raise KeyError("No registered settings of %s" % cls)
            return self.__settings[cls]
        else:
            raise KeyError("Unknown directory request type, %s" % cls)

    def __contains__(self, cls):
        try:
            self[cls]
        except KeyError:
            return False
        return True

    def getService(self, cls):
        """Return all services of the given class as a dictionary from
        host to service object."""

        if issubclass(cls, Service):
            if cls not in self.__services:
                raise KeyError("No registered services of %s" % cls)
            return self.__services[cls]
        else:
            raise KeyError("Unknown directory request type, %s" % cls)

    def get(self, cls, host):
        """Return the service of the given class running on HOST."""

        if issubclass(cls, Service):
            hostMap = self.getService(cls)
            if host not in hostMap:
                raise KeyError("No service of %s on host %s" % (cls, host))
            return hostMap[host]
        else:
            raise KeyError("Unknown directory request type, %s" % cls)

    def allServices(self):
        """Return a list of all services."""

        return self.__allServices

    def allSettings(self):
        """Return a list of all settings."""

        return self.__allSettings

    def link(self):
        """Link this directory.  This calls the link method on all
        registered Settings, then calls the link method on all
        registered Services."""

        for s in self.allSettings():
            s.link(self)
        for s in self.allServices():
            s.link(self)

    def setup(self):
        """Setup all Services in this directory, in parallel."""

        pmap(lambda s: s.setup(), self.allServices())

    def startAll(self, cls):
        """Start all Services of the given class, in parallel."""

        pmap(lambda s: s.start(), self.getService(cls).values())

    def stopAll(self, cls):
        """Stop all Services of the given class, in parallel."""

        pmap(lambda s: s.stop(), self.getService(cls).values())

class Configurable(object):
    def getName(self):
        return type(self).__name__

    def getConfig(self):
        config = []
        nameSet = set()
        for cls in reversed(type(self).__mro__):
            if hasattr(cls, "__config__"):
                for name in cls.__config__:
                    if name in nameSet:
                        continue
                    nameSet.add(name)
                    config.append((name, getattr(self, name)))
        return config

class Service(Configurable):
    __config__ = ["host"]

    def __init__(self, name, host):
        self.name = name
        self.host = host

    def getName(self):
        return self.name

    def link(self, directory):
        pass

    def setup(self):
        pass

    def start(self):
        pass

    def stop(self):
        pass

class Setting(Configurable):
    def link(self, directory):
        pass
