##      common/resolver.py
##
## Copyright (C) 2006 Dimitur Kirov <dkirov@gmail.com>
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## Gajim 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 General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
##

import sys
import os
import re
import logging
log = logging.getLogger('gajim.c.resolver')

if __name__ == '__main__':
    sys.path.append('..')
    from common import i18n
    import common.configpaths
    common.configpaths.gajimpaths.init(None)
    common.configpaths.gajimpaths.init_profile()

from common import helpers
from nbxmpp.idlequeue import IdleCommand

# it is good to check validity of arguments, when calling system commands
ns_type_pattern = re.compile('^[a-z]+$')

# match srv host_name
host_pattern = re.compile('^[a-z0-9\-._]*[a-z0-9]\.[a-z]{2,}$')

try:
    #raise ImportError("Manually disabled libasync")
    import libasyncns
    USE_LIBASYNCNS = True
    log.info("libasyncns-python loaded")
except ImportError:
    USE_LIBASYNCNS = False
    log.debug("Import of libasyncns-python failed, getaddrinfo will block", exc_info=True)


def get_resolver(idlequeue):
    if USE_LIBASYNCNS:
        log.info('Using LibAsyncNSResolver')
        return LibAsyncNSResolver()
    else:
        if helpers.is_in_path('host'):
            log.info('Using HostResolver')
            return HostResolver(idlequeue)
        log.info('Using NSLookupResolver')
        return NSLookupResolver(idlequeue)

class CommonResolver():
    def __init__(self):
        # dict {"host+type" : list of records}
        self.resolved_hosts = {}
        # dict {"host+type" : list of callbacks}
        self.handlers = {}

    def resolve(self, host, on_ready, type_='srv'):
        host = host.lower()
        log.debug('resolve %s type=%s' % (host, type_))
        assert(type_ in ['srv', 'txt'])
        if not host:
            # empty host, return empty list of srv records
            on_ready([])
            return
        if self.resolved_hosts.has_key(host+type_):
            # host is already resolved, return cached values
            log.debug('%s already resolved: %s' % (host,
                self.resolved_hosts[host+type_]))
            on_ready(host, self.resolved_hosts[host+type_])
            return
        if self.handlers.has_key(host+type_):
            # host is about to be resolved by another connection,
            # attach our callback
            log.debug('already resolving %s' % host)
            self.handlers[host+type_].append(on_ready)
        else:
            # host has never been resolved, start now
            log.debug('Starting to resolve %s using %s' % (host, self))
            self.handlers[host+type_] = [on_ready]
            self.start_resolve(host, type_)

    def _on_ready(self, host, type_, result_list):
        # practically it is impossible to be the opposite, but who knows :)
        host = host.lower()
        log.debug('Resolving result for %s: %s' % (host, result_list))
        if not self.resolved_hosts.has_key(host+type_):
            self.resolved_hosts[host+type_] = result_list
        if self.handlers.has_key(host+type_):
            for callback in self.handlers[host+type_]:
                callback(host, result_list)
            del(self.handlers[host+type_])

    def start_resolve(self, host, type_):
        pass

# FIXME: API usage is not consistent! This one requires that process is called
class LibAsyncNSResolver(CommonResolver):
    """
    Asynchronous resolver using libasyncns-python. process() method has to be
    called in order to proceed the pending requests. Based on patch submitted by
    Damien Thebault.
    """

    def __init__(self):
        self.asyncns = libasyncns.Asyncns()
        CommonResolver.__init__(self)

    def start_resolve(self, host, type_):
        type_ = libasyncns.ns_t_srv
        if type_ == 'txt': type_ = libasyncns.ns_t_txt
        resq = self.asyncns.res_query(host, libasyncns.ns_c_in, type_)
        resq.userdata = {'host':host, 'type':type_}

    # getaddrinfo to be done
    #def resolve_name(self, dname, callback):
        #resq = self.asyncns.getaddrinfo(dname)
        #resq.userdata = {'callback':callback, 'dname':dname}

    def _on_ready(self, host, type_, result_list):
        if type_ == libasyncns.ns_t_srv: type_ = 'srv'
        elif type_ == libasyncns.ns_t_txt: type_ = 'txt'

        CommonResolver._on_ready(self, host, type_, result_list)

    def process(self):
        try:
            self.asyncns.wait(False)
            resq = self.asyncns.get_next()
        except:
            return True
        if type(resq) == libasyncns.ResQuery:
            # TXT or SRV result
            while resq is not None:
                try:
                    rl = resq.get_done()
                except Exception:
                    rl = []
                hosts = []
                requested_type = resq.userdata['type']
                requested_host = resq.userdata['host']
                if rl:
                    for r in rl:
                        if r['type'] != requested_type:
                            # Answer doesn't contain valid SRV data
                            continue
                        r['prio'] = r['pref']
                        hosts.append(r)
                self._on_ready(host=requested_host, type_=requested_type,
                        result_list=hosts)
                try:
                    resq = self.asyncns.get_next()
                except Exception:
                    resq = None
        elif type(resq) == libasyncns.AddrInfoQuery:
            # getaddrinfo result (A or AAAA)
            rl = resq.get_done()
            resq.userdata['callback'](resq.userdata['dname'], rl)
        return True


class NSLookupResolver(CommonResolver):
    """
    Asynchronous DNS resolver calling nslookup. Processing of pending requests
    is invoked from idlequeue which is watching file descriptor of pipe of
    stdout of nslookup process.
    """

    def __init__(self, idlequeue):
        self.idlequeue = idlequeue
        self.process = False
        CommonResolver.__init__(self)

    def parse_srv_result(self, fqdn, result):
        """
        Parse the output of nslookup command and return list of properties:
        'host', 'port','weight', 'priority'     corresponding to the found srv hosts
        """
        if os.name == 'nt':
            return self._parse_srv_result_nt(fqdn, result)
        elif os.name == 'posix':
            return self._parse_srv_result_posix(fqdn, result)

    def _parse_srv_result_nt(self, fqdn, result):
        # output from win32 nslookup command
        if not result:
            return []
        hosts = []
        lines = result.replace('\r', '').split('\n')
        current_host = None
        for line in lines:
            line = line.lstrip()
            if line == '':
                continue
            if line.startswith(fqdn):
                rest = line[len(fqdn):]
                if rest.find('service') > -1:
                    current_host = {}
            elif isinstance(current_host, dict):
                res = line.strip().split('=')
                if len(res) != 2:
                    if len(current_host) == 4:
                        hosts.append(current_host)
                    current_host = None
                    continue
                prop_type = res[0].strip()
                prop_value = res[1].strip()
                if prop_type.find('prio') > -1:
                    try:
                        current_host['prio'] = int(prop_value)
                    except ValueError:
                        continue
                elif prop_type.find('weight') > -1:
                    try:
                        current_host['weight'] = int(prop_value)
                    except ValueError:
                        continue
                elif prop_type.find('port') > -1:
                    try:
                        current_host['port'] = int(prop_value)
                    except ValueError:
                        continue
                elif prop_type.find('host') > -1:
                    # strip '.' at the end of hostname
                    if prop_value[-1] == '.':
                        prop_value = prop_value[:-1]
                    current_host['host'] = prop_value
                if len(current_host) == 4:
                    hosts.append(current_host)
                    current_host = None
        return hosts

    def _parse_srv_result_posix(self, fqdn, result):
        # typical output of bind-tools nslookup command:
        # _xmpp-client._tcp.jabber.org    service = 30 30 5222 jabber.org.
        if not result:
            return []
        ufqdn = helpers.ascii_to_idn(fqdn) # Unicode domain name
        hosts = []
        lines = result.split('\n')
        for line in lines:
            if line == '':
                continue
            domain = None
            if line.startswith(fqdn):
                domain = fqdn # For nslookup 9.5
            elif helpers.decode_string(line).startswith(ufqdn):
                line = helpers.decode_string(line)
                domain = ufqdn # For nslookup 9.6
            if domain:
                rest = line[len(domain):].split('=')
                if len(rest) != 2:
                    continue
                answer_type, props_str = rest
                if answer_type.strip() != 'service':
                    continue
                props = props_str.strip().split(' ')
                if len(props) < 4:
                    continue
                prio, weight, port, host  = props[-4:]
                if host[-1] == '.':
                    host = host[:-1]
                try:
                    prio = int(prio)
                    weight = int(weight)
                    port = int(port)
                except ValueError:
                    continue
                hosts.append({'host': host, 'port': port, 'weight': weight,
                        'prio': prio})
        return hosts

    def _on_ready(self, host, type_, result):
        # nslookup finished, parse the result and call the handlers
        result_list = self.parse_srv_result(host, result)
        CommonResolver._on_ready(self, host, type_, result_list)

    def start_resolve(self, host, type_):
        """
        Spawn new nslookup process and start waiting for results
        """
        ns = NsLookup(self._on_ready, host, type_)
        ns.set_idlequeue(self.idlequeue)
        ns.commandtimeout = 20
        ns.start()


class HostResolver(CommonResolver):
    """
    Asynchronous DNS resolver calling host. Processing of pending requests
    is invoked from idlequeue which is watching file descriptor of pipe of
    stdout of host process.
    """

    def __init__(self, idlequeue):
        self.idlequeue = idlequeue
        self.process = False
        CommonResolver.__init__(self)

    def parse_srv_result(self, fqdn, result):
        """
        Parse the output of host command and return list of properties:
        'host', 'port','weight', 'priority' corresponding to the found srv hosts
        """
        # typical output of host command:
        # _xmpp-client._tcp.jabber.org has SRV record 30 30 5222 jabber.org.
        if not result:
            return []
        ufqdn = helpers.ascii_to_idn(fqdn) # Unicode domain name
        hosts = []
        lines = result.split('\n')
        for line in lines:
            if line == '':
                continue
            domain = None
            if line.startswith(fqdn):
                domain = fqdn # For nslookup 9.5
            elif helpers.decode_string(line).startswith(ufqdn):
                line = helpers.decode_string(line)
                domain = ufqdn # For nslookup 9.6
            if domain:
                # add 4 for ' has' after domain name
                rest = line[len(domain)+4:].split('record')
                if len(rest) != 2:
                    continue
                answer_type, props_str = rest
                if answer_type.strip() != 'SRV':
                    continue
                props = props_str.strip().split(' ')
                if len(props) < 4:
                    continue
                prio, weight, port, host  = props[-4:]
                if host[-1] == '.':
                    host = host[:-1]
                try:
                    prio = int(prio)
                    weight = int(weight)
                    port = int(port)
                except ValueError:
                    continue
                hosts.append({'host': host, 'port': port, 'weight': weight,
                        'prio': prio})
        return hosts

    def _on_ready(self, host, type_, result):
        # host finished, parse the result and call the handlers
        result_list = self.parse_srv_result(host, result)
        CommonResolver._on_ready(self, host, type_, result_list)

    def start_resolve(self, host, type_):
        """
        Spawn new nslookup process and start waiting for results
        """
        ns = Host(self._on_ready, host, type_)
        ns.set_idlequeue(self.idlequeue)
        ns.commandtimeout = 20
        ns.start()

class NsLookup(IdleCommand):
    def __init__(self, on_result, host='_xmpp-client', type_='srv'):
        IdleCommand.__init__(self, on_result)
        self.commandtimeout = 10
        self.host = host.lower()
        self.type_ = type_.lower()
        if not host_pattern.match(self.host):
            # invalid host name
            log.error('Invalid host: %s' % self.host)
            self.canexecute = False
            return
        if not ns_type_pattern.match(self.type_):
            log.error('Invalid querytype: %s' % self.type_)
            self.canexecute = False
            return

    def _compose_command_args(self):
        return ['nslookup', '-type=' + self.type_, self.host]

    def _return_result(self):
        if self.result_handler:
            self.result_handler(self.host, self.type_, self.result)
        self.result_handler = None


class Host(NsLookup):
    def _compose_command_args(self):
        return ['host', '-t', self.type_, self.host]

# below lines is on how to use API and assist in testing
if __name__ == '__main__':
    import gobject
    import gtk
    from nbxmpp import idlequeue

    idlequeue = idlequeue.get_idlequeue()
    resolver = get_resolver(idlequeue)

    def clicked(widget):
        global resolver
        host = text_view.get_text()
        def on_result(host, result_array):
            print 'Result:\n' + repr(result_array)
        resolver.resolve(host, on_result)
    win = gtk.Window()
    win.set_border_width(6)
    text_view = gtk.Entry()
    text_view.set_text('_xmpp-client._tcp.jabber.org')
    hbox = gtk.HBox()
    hbox.set_spacing(3)
    but = gtk.Button(' Lookup SRV ')
    hbox.pack_start(text_view, 5)
    hbox.pack_start(but, 0)
    but.connect('clicked', clicked)
    win.add(hbox)
    win.show_all()
    gobject.timeout_add(200, idlequeue.process)
    if USE_LIBASYNCNS:
        gobject.timeout_add(200, resolver.process)
    gtk.main()
