# -*- coding: utf-8 -*-

import functools
from twisted.internet import defer

from txtorcon.interface import ITorControlProtocol


class MagicContainer:
    """
    This merely contains 1 or more methods or further MagicContainer
    instances; see _do_setup in TorInfo.

    Once _setup_complete() is called, this behaves differently so that
    one can get nicer access to GETINFO things from TorInfo --
    specifically dir() and so forth pretend that there are only
    methods/attributes that pertain to actual Tor GETINFO keys.

    See TorInfo.
    """

    def __init__(self, n):
        self._txtorcon_name = n
        self.attrs = {}
        self._setup = False

    def _setup_complete(self):
        self._setup = True

    def _add_attribute(self, n, v):
        self.attrs[n] = v

    def __repr__(self):
        return object.__getattribute__(self, '_txtorcon_name')

    def __getitem__(self, idx):
        return list(object.__getattribute__(self, 'attrs').items())[idx][1]

    def __len__(self):
        return len(object.__getattribute__(self, 'attrs'))

    def __dir__(self):
        return list(object.__getattribute__(self, 'attrs').keys())

    def __getattribute__(self, name):
        sup = super(MagicContainer, self)
        if sup.__getattribute__('_setup') is False:
            return sup.__getattribute__(name)

        attrs = sup.__getattribute__('attrs')
        if name == '__members__':
            return list(attrs.keys())

        else:
            if name.startswith('__'):
                return sup.__getattribute__(name)

            try:
                return attrs[name]
            except KeyError:
                if name in ['dump']:
                    return object.__getattribute__(self, name)
                raise AttributeError(name)

    def dump(self, prefix):
        prefix = prefix + '.' + object.__getattribute__(self, '_txtorcon_name')
        for x in list(object.__getattribute__(self, 'attrs').values()):
            x.dump(prefix)


class ConfigMethod(object):
    def __init__(self, info_key, protocol, takes_arg=False):
        self.info_key = info_key
        self.proto = protocol
        self.takes_arg = takes_arg

    def dump(self, prefix):
        n = self.info_key.replace('/', '.')
        n = n.replace('-', '_')
        s = '%s(%s)' % (n, 'arg' if self.takes_arg else '')
        return s

    def __call__(self, *args):
        if self.takes_arg:
            if len(args) != 1:
                raise TypeError(
                    '"%s" takes exactly one argument' % self.info_key
                )
            req = '%s/%s' % (self.info_key, str(args[0]))

        else:
            if len(args) != 0:
                raise TypeError('"%s" takes no arguments' % self.info_key)

            req = self.info_key

        def stripper(key, arg):
            # strip "keyname="
            # sometimes keyname= is followed by a newline, so final .strip()
            return arg.strip()[len(key) + 1:].strip()

        d = self.proto.get_info_raw(req)
        d.addCallback(functools.partial(stripper, req))
        return d

    def __str__(self):
        arg = ''
        if self.takes_arg:
            arg = 'arg'
        return '%s(%s)' % (self.info_key.replace('-', '_'), arg)


class TorInfo(object):
    """Implements some attribute magic over top of TorControlProtocol so
    that all the available GETINFO values are gettable in a little
    easier fashion. Dashes are replaced by underscores (since dashes
    aren't valid in method/attribute names for Python). Some of the
    magic methods will take a single string argument if the
    corresponding Tor GETINFO would take one (in 'GETINFO info/names'
    it will end with '/*', and the same in torspec). In either case,
    the method returns a Deferred which will callback with the
    requested value, always a string.

    For example (see also examples/tor_info.py):

        proto = TorControlProtocol()
        #...
        def cb(arg):
            print arg
        info = TorInfo(proto)
        info.traffic.written().addCallback(cb)
        info.ip_to_country('8.8.8.8').addCallback(cb)

    For interactive use -- or even checking things progammatically --
    TorInfo pretends it only has attributes that coorespond to valid
    GETINFO calls.  So for example, dir(info) will only return all the
    currently valid top-level things. In the above example this might
    be ['traffic', 'ip_to_country'] (of course in practice this is a
    much longer list). And "dir(info.traffic)" might return ['read',
    'written']

    For something similar to this for configuration (GETCONF, SETCONF)
    see TorConfig which is quite a lot more complicated (internally)
    since you can set config items.

    NOTE that 'GETINFO config/*' is not supported as it's the only
    case that's not a leaf, but theoretically a method.

    """

    def __init__(self, control, errback=None):
        self._setup = False
        self.attrs = {}
        '''After _setup is True, these are all we show as attributes.'''

        self.protocol = ITorControlProtocol(control)
        self.errback = errback

        self.post_bootstrap = defer.Deferred()
        if self.protocol.post_bootstrap:
            self.protocol.post_bootstrap.addCallback(self.bootstrap)

        else:
            self.bootstrap()

    def _add_attribute(self, n, v):
        self.attrs[n] = v

    # iterator protocol

    def __getitem__(self, idx):
        sup = super(TorInfo, self)
        if sup.__getattribute__('_setup') is True:
            return list(object.__getattribute__(self, 'attrs').items())[idx][1]
        raise TypeError("No __getitem__ until we've setup.")

    def __len__(self):
        sup = super(TorInfo, self)
        if sup.__getattribute__('_setup') is True:
            return len(object.__getattribute__(self, 'attrs'))
        raise TypeError("No length until we're setup.")

    # change our attribute behavior based on the value of _setup

    def __dir__(self):
        sup = super(TorInfo, self)
        if sup.__getattribute__('_setup') is True:
            return list(sup.__getattribute__('attrs').keys())
        return list(sup.__getattribute__('__dict__').keys())

    def __getattribute__(self, name):
        sup = super(TorInfo, self)
        if sup.__getattribute__('_setup') is False:
            return sup.__getattribute__(name)

        attrs = sup.__getattribute__('attrs')
        # are there other "special" attributes we need to consider..?
        if name == '__members__':
            return list(attrs.keys())
        if name == '__class__':
            return sup.__class__

        else:
            try:
                return attrs[name]

            except KeyError:
                if name == 'dump':
                    return object.__getattribute__(self, name)

        raise AttributeError(name)

    def bootstrap(self, *args):
        d = self.protocol.get_info_raw("info/names")
        d.addCallback(self._do_setup)
        if self.errback:
            d.addErrback(self.errback)
        d.addCallback(self._setup_complete)
        return d

    def dump(self):
        for x in object.__getattribute__(self, 'attrs').values():
            x.dump('')

    def _do_setup(self, data):
        # FIXME figure out why network-status doesn't work (get
        # nothing back from Tor it seems, although stem does get an
        # answer). this is a space-separated list of ~2500 OR id's;
        # could it be that LineReceiver can't handle it?
        added_magic = []
        for line in data.split('\n'):
            if line == "info/names=" or line.strip() == '':
                continue

            (name, documentation) = line.split(' ', 1)
            # FIXME think about this -- this is the only case where
            # there's something that's a directory
            # (i.e. MagicContainer) AND needs to be a ConfigMethod as
            # well...but doesn't really seem very useful. Somewhat
            # simpler to not support this case for now...
            if name == 'config/*':
                continue

            if name.endswith('/*'):
                # this takes an arg, so make a method
                bits = name[:-2].split('/')
                takes_arg = True

            else:
                bits = name.split('/')
                takes_arg = False

            mine = self
            for bit in bits[:-1]:
                bit = bit.replace('-', '_')
                if bit in mine.attrs:
                    mine = mine.attrs[bit]
                    if not isinstance(mine, MagicContainer):
                        raise RuntimeError(
                            "Already had something: %s for %s" % (bit, name)
                        )

                else:
                    c = MagicContainer(bit)
                    added_magic.append(c)
                    mine._add_attribute(bit, c)
                    mine = c
            n = bits[-1].replace('-', '_')
            if n in mine.attrs:
                raise RuntimeError(
                    "Already had something: %s for %s" % (n, name)
                )
            mine._add_attribute(n, ConfigMethod('/'.join(bits),
                                                self.protocol, takes_arg))

        for c in added_magic:
            c._setup_complete()
        return None

    def _setup_complete(self, *args):
        pb = self.post_bootstrap
        self._setup = True
        pb.callback(self)
