# this will go to src/common/xmpp later, for now it is in src/common
# -*- coding:utf-8 -*-
## src/common/dataforms.py
##
## Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
## Copyright (C) 2006-2017 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
##
## 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/>.
##

"""
This module contains wrappers for different parts of data forms (JEP 0004). For
information how to use them, read documentation
"""

import nbxmpp
import helpers

# exceptions used in this module
# base class
class Error(Exception): pass
# when we get nbxmpp.Node which we do not understand
class UnknownDataForm(Error): pass
# when we get nbxmpp.Node which contains bad fields
class WrongFieldValue(Error): pass

# helper class to change class of already existing object
class ExtendedNode(nbxmpp.Node, object):
    @classmethod
    def __new__(cls,  *a, **b):
        if 'extend' not in b.keys() or not b['extend']:
            return object.__new__(cls)

        extend = b['extend']
        assert issubclass(cls, extend.__class__)
        extend.__class__ = cls
        return extend

# helper decorator to create properties in cleaner way
def nested_property(f):
    ret = f()
    p = {'doc': f.__doc__}
    for v in ('fget', 'fset', 'fdel', 'doc'):
        if v in ret.keys(): p[v]=ret[v]
    return property(**p)

# helper to create fields from scratch
def Field(typ, **attrs):
    ''' Helper function to create a field of given type. '''
    f = {
            'boolean': BooleanField,
            'fixed': StringField,
            'hidden': StringField,
            'text-private': StringField,
            'text-single': StringField,
            'jid-multi': JidMultiField,
            'jid-single': JidSingleField,
            'list-multi': ListMultiField,
            'list-single': ListSingleField,
            'text-multi': TextMultiField,
    }[typ](typ=typ, **attrs)
    return f

def ExtendField(node):
    """
    Helper function to extend a node to field of appropriate type
    """
    # when validation (XEP-122) will go in, we could have another classes
    # like DateTimeField - so that dicts in Field() and ExtendField() will
    # be different...
    typ=node.getAttr('type')
    f = {
            'boolean': BooleanField,
            'fixed': StringField,
            'hidden': StringField,
            'text-private': StringField,
            'text-single': StringField,
            'jid-multi': JidMultiField,
            'jid-single': JidSingleField,
            'list-multi': ListMultiField,
            'list-single': ListSingleField,
            'text-multi': TextMultiField,
    }
    if typ not in f:
        typ = 'text-single'
    return f[typ](extend=node)

def ExtendForm(node):
    """
    Helper function to extend a node to form of appropriate type
    """
    if node.getTag('reported') is not None:
        return MultipleDataForm(extend=node)
    else:
        return SimpleDataForm(extend=node)

class DataField(ExtendedNode):
    """
    Keeps data about one field - var, field type, labels, instructions... Base
    class for different kinds of fields. Use Field() function to construct one
    of these
    """

    def __init__(self, typ=None, var=None, value=None, label=None, desc=None,
                    required=False, options=None, extend=None):

        if extend is None:
            ExtendedNode.__init__(self, 'field')

            self.type_ = typ
            self.var = var
            if value is not None:
                self.value = value
            if label is not None:
                self.label = label
            if desc  is not None:
                self.desc = desc
            self.required = required
            self.options = options

    @nested_property
    def type_():
        """
        Type of field. Recognized values are: 'boolean', 'fixed', 'hidden',
        'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi',
        'text-private', 'text-single'. If you set this to something different,
        DataField will store given name, but treat all data as text-single
        """
        def fget(self):
            t = self.getAttr('type')
            if t is None:
                return 'text-single'
            return t

        def fset(self, value):
            assert isinstance(value, basestring)
            self.setAttr('type', value)

        return locals()

    @nested_property
    def var():
        """
        Field identifier
        """
        def fget(self):
            return self.getAttr('var')

        def fset(self, value):
            assert isinstance(value, basestring)
            self.setAttr('var', value)

        def fdel(self):
            self.delAttr('var')

        return locals()

    @nested_property
    def label():
        """
        Human-readable field name
        """
        def fget(self):
            l = self.getAttr('label')
            if not l:
                l = self.var
            return l

        def fset(self, value):
            assert isinstance(value, basestring)
            self.setAttr('label', value)

        def fdel(self):
            if self.getAttr('label'):
                self.delAttr('label')

        return locals()

    @nested_property
    def description():
        """
        Human-readable description of field meaning
        """
        def fget(self):
            return self.getTagData('desc') or u''

        def fset(self, value):
            assert isinstance(value, basestring)
            if value == '':
                fdel(self)
            else:
                self.setTagData('desc', value)

        def fdel(self):
            t = self.getTag('desc')
            if t is not None:
                self.delChild(t)

        return locals()

    @nested_property
    def required():
        """
        Controls whether this field required to fill. Boolean
        """
        def fget(self):
            return bool(self.getTag('required'))

        def fset(self, value):
            t = self.getTag('required')
            if t and not value:
                self.delChild(t)
            elif not t and value:
                self.addChild('required')

        return locals()

    @nested_property
    def media():
        """
        Media data
        """
        def fget(self):
            media = self.getTag('media', namespace=nbxmpp.NS_DATA_MEDIA)
            if media:
                return Media(media)

        def fset(self, value):
            fdel(self)
            self.addChild(node=value)

        def fdel(self):
            t = self.getTag('media')
            if t is not None:
                self.delChild(t)

        return locals()

    def is_valid(self):
        return True

class Uri(nbxmpp.Node):
    def __init__(self, uri_tag):
        nbxmpp.Node.__init__(self, node=uri_tag)

    @nested_property
    def type_():
        """
        uri type
        """
        def fget(self):
            return self.getAttr('type')

        def fset(self, value):
            self.setAttr('type', value)

        def fdel(self):
            self.delAttr('type')

        return locals()

    @nested_property
    def uri_data():
        """
        uri data
        """
        def fget(self):
            return self.getData()

        def fset(self, value):
            self.setData(value)

        def fdel(self):
            self.setData(None)

        return locals()

class Media(nbxmpp.Node):
    def __init__(self, media_tag):
        nbxmpp.Node.__init__(self, node=media_tag)

    @nested_property
    def uris():
        """
        URIs of the media element.
        """
        def fget(self):
            return map(Uri, self.getTags('uri'))

        def fset(self, values):
            fdel(self)
            for uri in values:
                self.addChild(node=uri)

        def fdel(self):
            for element in self.getTags('uri'):
                self.delChild(element)

        return locals()

class BooleanField(DataField):
    @nested_property
    def value():
        """
        Value of field. May contain True, False or None
        """
        def fget(self):
            v = self.getTagData('value')
            if v in ('0', 'false'):
                return False
            if v in ('1', 'true'):
                return True
            if v is None:
                return False # default value is False
            raise WrongFieldValue

        def fset(self, value):
            self.setTagData('value', value and '1' or '0')

        def fdel(self, value):
            t = self.getTag('value')
            if t is not None:
                self.delChild(t)

        return locals()

class StringField(DataField):
    """
    Covers fields of types: fixed, hidden, text-private, text-single
    """

    @nested_property
    def value():
        """
        Value of field. May be any unicode string
        """
        def fget(self):
            return self.getTagData('value') or u''

        def fset(self, value):
            assert isinstance(value, basestring)
            if value == '' and not self.required:
                return fdel(self)
            self.setTagData('value', value)

        def fdel(self):
            try:
                self.delChild(self.getTag('value'))
            except ValueError: # if there already were no value tag
                pass

        return locals()

class ListField(DataField):
    """
    Covers fields of types: jid-multi, jid-single, list-multi, list-single
    """

    @nested_property
    def options():
        """
        Options
        """
        def fget(self):
            options = []
            for element in self.getTags('option'):
                v = element.getTagData('value')
                if v is None:
                    raise WrongFieldValue
                l = element.getAttr('label')
                if not l:
                    l = v
                options.append((l, v))
            return options

        def fset(self, values):
            fdel(self)
            for value, label in values:
                self.addChild('option', {'label': label}).setTagData('value', value)

        def fdel(self):
            for element in self.getTags('option'):
                self.delChild(element)

        return locals()

    def iter_options(self):
        for element in self.iterTags('option'):
            v = element.getTagData('value')
            if v is None:
                raise WrongFieldValue
            l = element.getAttr('label')
            if not l:
                l = v
            yield (v, l)

class ListSingleField(ListField, StringField):
    """
    Covers list-single field
    """
    def is_valid(self):
        if not self.required:
            return True
        if not self.value:
            return False
        return True

class JidSingleField(ListSingleField):
    """
    Covers jid-single fields
    """
    def is_valid(self):
        if self.value:
            try:
                helpers.parse_jid(self.value)
                return True
            except:
                return False
        if self.required:
            return False
        return True

class ListMultiField(ListField):
    """
    Covers list-multi fields
    """

    @nested_property
    def values():
        """
        Values held in field
        """
        def fget(self):
            values = []
            for element in self.getTags('value'):
                values.append(element.getData())
            return values

        def fset(self, values):
            fdel(self)
            for value in values:
                self.addChild('value').setData(value)

        def fdel(self):
            for element in self.getTags('value'):
                self.delChild(element)

        return locals()

    def iter_values(self):
        for element in self.getTags('value'):
            yield element.getData()

    def is_valid(self):
        if not self.required:
            return True
        if not self.values:
            return False
        return True

class JidMultiField(ListMultiField):
    """
    Covers jid-multi fields
    """
    def is_valid(self):
        if len(self.values):
            for value in self.values:
                try:
                    helpers.parse_jid(value)
                except:
                    return False
            return True
        if self.required:
            return False
        return True

class TextMultiField(DataField):
    @nested_property
    def value():
        """
        Value held in field
        """
        def fget(self):
            value = u''
            for element in self.iterTags('value'):
                value += '\n' + element.getData()
            return value[1:]

        def fset(self, value):
            fdel(self)
            if value == '':
                return
            for line in value.split('\n'):
                self.addChild('value').setData(line)

        def fdel(self):
            for element in self.getTags('value'):
                self.delChild(element)

        return locals()

class DataRecord(ExtendedNode):
    """
    The container for data fields - an xml element which has DataField elements
    as children
    """
    def __init__(self, fields=None, associated=None, extend=None):
        self.associated = associated
        self.vars = {}
        if extend is None:
            # we have to build this object from scratch
            nbxmpp.Node.__init__(self)

            if fields is not None:
                self.fields = fields
        else:
            # we already have nbxmpp.Node inside - try to convert all
            # fields into DataField objects
            if fields is None:
                for field in self.iterTags('field'):
                    if not isinstance(field, DataField):
                        ExtendField(field)
                    self.vars[field.var] = field
            else:
                for field in self.getTags('field'):
                    self.delChild(field)
                self.fields = fields

    @nested_property
    def fields():
        """
        List of fields in this record
        """
        def fget(self):
            return self.getTags('field')

        def fset(self, fields):
            fdel(self)
            for field in fields:
                if not isinstance(field, DataField):
                    ExtendField(extend=field)
                self.addChild(node=field)

        def fdel(self):
            for element in self.getTags('field'):
                self.delChild(element)

        return locals()

    def iter_fields(self):
        """
        Iterate over fields in this record. Do not take associated into account
        """
        for field in self.iterTags('field'):
            yield field

    def iter_with_associated(self):
        """
        Iterate over associated, yielding both our field and associated one
        together
        """
        for field in self.associated.iter_fields():
            yield self[field.var], field

    def __getitem__(self, item):
        return self.vars[item]

    def is_valid(self):
        for f in self.iter_fields():
            if not f.is_valid():
                return False
        return True

class DataForm(ExtendedNode):
    def __init__(self, type_=None, title=None, instructions=None, extend=None):
        if extend is None:
            # we have to build form from scratch
            nbxmpp.Node.__init__(self, 'x', attrs={'xmlns': nbxmpp.NS_DATA})

        if type_ is not None:
            self.type_=type_
        if title is not None:
            self.title=title
        if instructions is not None:
            self.instructions=instructions

    @nested_property
    def type_():
        """
        Type of the form. Must be one of: 'form', 'submit', 'cancel', 'result'.
        'form' - this form is to be filled in; you will be able soon to do:
        filledform = DataForm(replyto=thisform)
        """
        def fget(self):
            return self.getAttr('type')

        def fset(self, type_):
            assert type_ in ('form', 'submit', 'cancel', 'result')
            self.setAttr('type', type_)

        return locals()

    @nested_property
    def title():
        """
        Title of the form

        Human-readable, should not contain any \\r\\n.
        """
        def fget(self):
            return self.getTagData('title')

        def fset(self, title):
            self.setTagData('title', title)

        def fdel(self):
            try:
                self.delChild('title')
            except ValueError:
                pass

        return locals()

    @nested_property
    def instructions():
        """
        Instructions for this form

        Human-readable, may contain \\r\\n.
        """
        # TODO: the same code is in TextMultiField. join them
        def fget(self):
            value = u''
            for valuenode in self.getTags('instructions'):
                value += '\n' + valuenode.getData()
            return value[1:]

        def fset(self, value):
            fdel(self)
            if value == '': return
            for line in value.split('\n'):
                self.addChild('instructions').setData(line)

        def fdel(self):
            for value in self.getTags('instructions'):
                self.delChild(value)

        return locals()

class SimpleDataForm(DataForm, DataRecord):
    def __init__(self, type_=None, title=None, instructions=None, fields=None, \
    extend=None):
        DataForm.__init__(self, type_=type_, title=title,
                instructions=instructions, extend=extend)
        DataRecord.__init__(self, fields=fields, extend=self, associated=self)

    def get_purged(self):
        c = SimpleDataForm(extend=self)
        del c.title
        c.instructions = ''
        to_be_removed = []
        for f in c.iter_fields():
            if f.required:
                # add <value> if there is not
                if hasattr(f, 'value') and  not f.value:
                    f.value = ''
                # Keep all required fields
                continue
            if (hasattr(f, 'value') and not f.value and f.value != 0) or (
            hasattr(f, 'values') and len(f.values) == 0):
                to_be_removed.append(f)
            else:
                del f.label
                del f.description
                del f.media
        for f in to_be_removed:
            c.delChild(f)
        return c

class MultipleDataForm(DataForm):
    def __init__(self, type_=None, title=None, instructions=None, items=None,
    extend=None):
        DataForm.__init__(self, type_=type_, title=title,
                instructions=instructions, extend=extend)
        # all records, recorded into DataRecords
        if extend is None:
            if items is not None:
                self.items = items
        else:
            # we already have nbxmpp.Node inside - try to convert all
            # fields into DataField objects
            if items is None:
                self.items = list(self.iterTags('item'))
            else:
                for item in self.getTags('item'):
                    self.delChild(item)
                self.items = items
        reported_tag = self.getTag('reported')
        self.reported = DataRecord(extend=reported_tag)

    @nested_property
    def items():
        """
        A list of all records
        """
        def fget(self):
            return list(self.iter_records())

        def fset(self, records):
            fdel(self)
            for record in records:
                if not isinstance(record, DataRecord):
                    DataRecord(extend=record)
                self.addChild(node=record)

        def fdel(self):
            for record in self.getTags('item'):
                self.delChild(record)

        return locals()

    def iter_records(self):
        for record in self.getTags('item'):
            yield record

#       @nested_property
#       def reported():
#               """
#               DataRecord that contains descriptions of fields in records
#               """
#               def fget(self):
#                       return self.getTag('reported')
#               def fset(self, record):
#                       try:
#                               self.delChild('reported')
#                       except:
#                               pass
#
#                       record.setName('reported')
#                       self.addChild(node=record)
#               return locals()
