#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2016, Riccardo Metere
# Copyright (c) 2018 - 2020, Lars Heuer
# All rights reserved.
#
# License: BSD License
#
"""
Generation of special-purpose text for Qr codes.
"""
from __future__ import absolute_import, unicode_literals, print_function
import warnings

# <https://wiki.python.org/moin/PortingToPy3k/BilingualQuickRef#New_Style_Classes>
__metaclass__ = type


class QrSpecial:
    """
    Special-purpose text for QR codes.

    Implements the special text generated by the ZXing project for QR codes.
    Likely, these are correctly handled by software using the this library.

    Of note:

    - the `Event` special text is not supported here, but it can be handled by
      using the `icalendar` package [https://pypi.python.org/pypi/icalendar].
    - the `vCard` contact format is not supported here (only MeCard),
      but a number of packages for handling vCards are available in PyPI.
    """
    _kv_sep = ':'
    _sep = ';'
    _token_fmt = '{{}}{kv_sep}{{}}'.format(kv_sep=_kv_sep)
    _end_tag = ';;'

    _start_tag = ''
    _fields = ()
    _field_tags = {}
    _multi_fields = ()

    _is_simple = True

    def __init__(self, **kws):
        """
        Base class to encode/decode special-purpose text for QR codes.

        This class is not meant to be used directly, except for the 'parse'
        method, which can be used as a constructor for the base classes.

        Args:
            **kws (dict): Input info to generate the QrSpecial object.
        """
        cls = type(self)
        cls._is_simple = len(cls._field_tags) == 0
        # cls._fields = signature(cls).parameters.keys()  # Py3-only
        # first argument is always `self`
        if not cls._fields:
            cls._fields = cls.__init__.__code__.co_varnames[
                          1:cls.__init__.__code__.co_argcount]
        self.data = {}
        for field in cls._fields:
            if field in kws:
                if kws[field] is not None:
                    self.data[field] = str(kws[field]) \
                        if cls._is_simple or field not in cls._multi_fields \
                        else cls._to_list(kws[field])
            else:
                raise NameError('Invalid keyword argument ``'.format(field))

    def __repr__(self):
        """
        Generate the human-friendly representation of the object.

        Returns:
            text (str|unicode): The representation of the object.
        """
        cls = type(self)
        if cls._is_simple:
            lines = [
                '{}: {}'.format(field, str(self.data[field]))
                for field in cls._fields if field in self.data]
        else:
            lines = []
            for field in cls._fields:
                if field in self.data:
                    if isinstance(self.data[field], list):
                        values = self.data[field]
                        for value in values:
                            lines.append('{}: {}'.format(field, str(value)))
                    else:
                        lines.append('{}: {}'.format(
                            field, str(self.data[field])))
        text = '<{}>'.format(cls.__name__)
        if lines:
            text += '\n' + '\n'.join(lines)
        else:
            text += ': <EMPTY>'
        return text

    def __str__(self):
        """
        Generate QR-ready text, i.e. the text to be used in QR codes.

        Returns:
            text (str|unicode): The output text.
        """
        cls = type(self)
        if cls._is_simple:
            values = [
                str(self.data[field])
                for field in cls._fields if field in self.data]
            text = cls._start_tag + cls._sep.join(values)
        else:
            tokens = []
            for field in cls._fields:
                if field in self.data:
                    if isinstance(self.data[field], list):
                        tokens.extend(
                            [cls._token_fmt.format(
                                cls._field_tags[field], str(value))
                             for value in self.data[field]])
                    else:
                        tokens.append(cls._token_fmt.format(
                            cls._field_tags[field], str(self.data[field])))

            text = (
                cls._start_tag + cls._sep.join(tokens) + cls._end_tag)
        return text

    def __eq__(self, other):
        """
        Generic implementation of equality.

        Args:
            other: The object to compare to.

        Returns:
            result (bool): True if contains the same data, False otherwise.
        """
        if isinstance(other, type(self)):
            return self.__dict__ == other.__dict__
        else:
            return False

    def __ne__(self, other):
        """
        Generic implementation of inequality.

        Args:
            other: The object to compare to.

        Returns:
            result (bool): False if contains the same data, True otherwise.
        """
        return not self.__eq__(other)

    def __bool__(self):
        """
        Determines if the object contains data.

        Returns:
            result (bool): True if the object contains data, False otherwise.
        """
        return len(self.data) > 0

    def __nonzero__(self):
        return self.__bool__()

    @classmethod
    def from_str(cls, text, strict=True, strip=True):
        """
        Construct a QrSpecial object from its QR-ready text.

        This is conceptually the inverse operation of the 'to_str' method.

        Args:
            text (str|unicode): The input text.
            strict (bool): Raises an error if tags are missing.
            strip (bool): Strip from whitespaces before parsing.

        Returns:
            obj (QrSpecial): The QrSpecial object.
        """
        if strip:
            text = text.strip()
        kws = {}
        if text:
            if text.startswith(cls._start_tag):
                text = text[len(cls._start_tag):]
            elif strict:
                raise ValueError(
                    'Starting tag `{}` not found'.format(cls._start_tag))
            if text.endswith(cls._end_tag):
                text = text[:-len(cls._end_tag)]
            elif strict and not cls._is_simple:
                raise ValueError(
                    'Ending tag `{}` not found'.format(cls._end_tag))
            tokens = text.split(cls._sep)
            if cls._is_simple:
                kws = {field: token
                       for field, token in zip(cls._fields, tokens)}
            else:
                fields = {v: k for k, v in cls._field_tags.items()}
                for token in tokens:
                    tag, value = token.split(cls._kv_sep, 1)
                    if tag in fields:
                        if fields[tag] not in kws:
                            kws[fields[tag]] = value \
                                if fields[tag] not in cls._multi_fields \
                                else [value]
                        elif fields[tag] in cls._multi_fields:
                            kws[fields[tag]].append(value)
                        else:
                            raise KeyError(
                                'Field `{}` already used'.format(fields[tag]))
                    else:
                        raise ValueError(
                            'Unknown field identifier `{}`'.format(tag))
        return cls(**kws)

    @staticmethod
    def _to_list(obj):
        """
        Ensure that an object is a list.

        If the object is already a list, nothing is done.
        Otherwise, if it is a tuple, it is converted to a list, else a list
        cointaining the object (with only 1 element) is returned.

        Args:
            obj: The input object.

        Returns:
            result (list): a list-ified version of the object.
        """
        if isinstance(obj, list):
            return obj
        elif isinstance(obj, tuple):
            return list(obj)
        else:
            return [obj]

    @staticmethod
    def parse(text):
        """
        Construct a QrSpecial-derived object from a text.

        This can be useful for determining whether a given input is a valid
        QrSpecial-derived object.

        Args:
            text (str|unicode): The input text.

        Returns:
            obj (QrSpecial): The QrSpecial-derived object.
        """
        # Construct a QrSpecial
        subclasses = (QrShortMessage, QrGeolocation, QrMeCard, QrWifi)
        obj = QrSpecial()
        for cls in subclasses:
            try:
                obj = cls.from_str(text)
            except ValueError:
                pass
            if obj:
                break
        return obj


class QrShortMessage(QrSpecial):
    """
    QrSpecial-derived short message (SMS).
    """
    _start_tag = 'smsto:'
    _sep = ':'

    def __init__(self, number=None, text=None):
        """
        Generate the QrSpecial-derived short message (SMS).

        Args:
            number (str|unicode|int): The number to send the text to.
            text (str|unicode): The text to send.

        Examples:
            >>> qrs = QrShortMessage('+39070653263', 'I like your code!')
            >>> print(repr(qrs))
            <QrShortMessage>
            number: +39070653263
            text: I like your code!
            >>> print(qrs)
            smsto:+39070653263:I like your code!
            >>> qrs == QrShortMessage.from_str(str(qrs))
            True
        """
        # QrSpecial.__init__(**locals())  # Py3-only
        kws = locals()
        kws.pop('self')
        QrSpecial.__init__(self, **kws)


class QrGeolocation(QrSpecial):
    """
    QrSpecial-derived geolocation.
    """
    _start_tag = 'geo:'
    _query_tag = '?q='
    _sep = ','

    def __init__(self, lat=None, lon=None, query=None):
        """
        Generate the QrSpecial-derived short message (SMS).

        Args:
            lat (str|unicode|float): The latitude coordinate.
            lon (str|unicode|float): The longitude coordinate.
            query (str|unicode): The associated query.
                It is present in ZXing QR implementation, but its usage is not
                well documented.
        Examples:
            >>> qrs = QrGeolocation(42.989, -71.465, 'www.python.org')
            >>> print(repr(qrs))
            <QrGeolocation>
            lat: 42.989
            lon: -71.465
            query: www.python.org
            >>> print(qrs)
            geo:42.989,-71.465?q=www.python.org
            >>> qrs == QrGeolocation.from_str(str(qrs))
            True

            >>> print(repr(QrGeolocation(47.68, -122.121)))
            <QrGeolocation>
            lat: 47.68
            lon: -122.121
        """
        if lat is None:
            lat = 0.0
        if lon is None:
            lon = 0.0
        # QrSpecial.__init__(**locals())  # Py3-only
        kws = locals()
        kws.pop('self')
        QrSpecial.__init__(self, **kws)

    def __str__(self):
        cls = type(self)
        text = QrSpecial.__str__(self)
        first_sep_pos = text.find(cls._sep)
        last_sep_pos = text.rfind(cls._sep)
        if first_sep_pos != last_sep_pos:
            text = (
                text[:last_sep_pos] + cls._query_tag +
                text[last_sep_pos + len(cls._sep):])
        return text

    @classmethod
    def from_str(cls, text, strict=True, strip=True):
        # replace only first occurrence of the query tag
        text = text.replace(cls._query_tag, cls._sep, 1)
        return super(QrGeolocation, cls).from_str(text, strict, strip)


class QrMeCard(QrSpecial):
    """
    QrSpecial-derived contact information (MeCard).
    """
    _start_tag = 'MECARD:'
    _field_tags = dict((
        ('name', 'N'),
        ('reading', 'SOUND'),
        ('tel', 'TEL'),
        ('telav', 'TEL-AV'),
        ('email', 'EMAIL'),
        ('memo', 'NOTE'),
        ('birthday', 'BDAY'),
        ('address', 'ADR'),
        ('url', 'URL'),
        ('nickname', 'NICKNAME'),
        ('company', 'ORG'),
    ))

    _multi_fields = {
        'tel', 'telav', 'email', 'address', 'url', 'nickname', 'company'}

    def __init__(self, name=None, reading=None, tel=None, telav=None, email=None,
                 memo=None, birthday=None, address=None, url=None, nickname=None,
                 company=None):
        """
        Generate the QrSpecial-derived contact information (MeCard).

        This is implementation contains both elements of the NTT Docomo
        specification (1) and the ZXing implementation (2).
        In particular:

            - (1) allow for some fields to have multiple entries.
            - (2) defines some additional fields, in analogy to vCard.
            - (1) specify the i-mode compatible bar code recognition
              function version; this is indicated for each field.

        Args:
            name (str|unicode): The contact name.
                When a field is divided by a comma (,), the first half is
                treated as the last name and the second half is treated as
                the first name. This is not enforced by this implementation.
                NTT Docomo v.>=1.
            reading (str|unicode): The name pronunciation.
                When a field is divided by a comma (,), the first half is
                treated as the last name and the second half is treated as
                the first name. This is not enforced by this implementation.
                NTT Docomo v.>=1.
            tel (str|unicode|iterable): The contact phone number.
                Multiple entries are allowed and should be inserted as a list.
                Expects 1 to 24 digits, but this is not enforced.
                NTT Docomo v.>=1.
            telav (str|unicode|iterable): The contact video-phone number.
                This is meant to specify a preferental number for video calls.
                Multiple entries are allowed and should be inserted as a list.
                Expects 1 to 24 digits, but this is not enforced.
                NTT Docomo v.>=2.
            email (str|unicode|iterable): The contact e-mail address.
                Multiple entries are allowed and should be inserted as a list.
                NTT Docomo v.>=1.
            memo (str|unicode): A memo text.
                NTT Docomo v.>=1.
            birthday (str|unicode|int): The contact birthday.
                The ISO 8601 basic format (date only) is expected: YYYYMMDD.
                This comply with the NTT Docomo specifications.
                NTT Docomo v.>=3.
            address (str|unicode|iterable): The contact address.
                The fields divided by commas (,) denote PO box, room number,
                house number, city, prefecture, zip code and country, in order.
                This is not enforced by this implementation.
                NTT Docomo v.>=3.
            url (str|unicode|iterable): The contact website.
                NTT Docomo v.>=3.
            nickname (str|unicode|iterable): The contact nickname.
                NTT Docomo v.>=3.
            company (str|unicode|iterable): The contact company.
                NTT Docomo v.N/A.

        Examples:
            >>> qrs = QrMeCard('Py Thon', email=('py@py.org', 'thon@py.org'))
            >>> print(repr(qrs))
            <QrMeCard>
            name: Py Thon
            email: py@py.org
            email: thon@py.org
            >>> print(str(qrs))
            MECARD:N:Py Thon;EMAIL:py@py.org;EMAIL:thon@py.org;;
            >>> qrs == QrMeCard.from_str(str(qrs))
            True

            >>> qrs = QrMeCard('QrSpecial', birthday=20160904)
            >>> print(repr(qrs))
            <QrMeCard>
            name: QrSpecial
            birthday: 20160904
            >>> print(str(qrs))
            MECARD:N:QrSpecial;BDAY:20160904;;

        See Also:
            https://en.wikipedia.org/wiki/MeCard
            https://www.nttdocomo.co.jp/english/service/developer/make
            /content/barcode/function/application/addressbook/index.html
            https://zxingnet.codeplex.com/
            https://zxing.appspot.com/generator
        """
        if birthday:
            birthday = str(int(birthday))[:8]
        # QrSpecial.__init__(**locals())  # Py3-only
        kws = locals()
        kws.pop('self')
        QrSpecial.__init__(self, **kws)


class QrWifi(QrSpecial):
    """
    QrSpecial-derived WiFi network.
    """
    _start_tag = 'WIFI:'
    _field_tags = dict((
        ('ssid', 'S'),
        ('security', 'T'),
        ('password', 'P'),
        ('hidden', 'H'),
    ))
    _modes = 'WEP', 'WPA', 'WPA2'

    def __init__(self, ssid=None, security=None, password=None, hidden=None):
        """
        Generate the QrSpecial-derived WiFi network.

        This is useful for sharing WiFi network authentication information.

        Args:
            ssid (str|unicode): The SSID of the WiFi network.
            security (str|unicode|None): The security standard to use.
                Accepted values are: [WEP|WPA|WPA2]
                Note that `WPA2` is not defined in the standard but it is
                accepted by the ZXing implementation.
                If None, no encryption is used.
            password (str|unicode): The password of the WiFi network.
                If security is None, it is forced to be empty.
            hidden (bool): Determine the SSID broadcast mode.
                If True, the SSID is not expected to be broadcast.

        Examples:
            >>> qrs = QrWifi('Python', 'WEP', 'Monty', True)
            >>> print(repr(qrs))
            <QrWifi>
            ssid: Python
            security: WEP
            password: Monty
            hidden: true
            >>> print(str(qrs))
            WIFI:S:Python;T:WEP;P:Monty;H:true;;
            >>> qrs == QrWifi.from_str(str(qrs))
            True

            >>> qrs = QrWifi('Python', 'WEP', 'Monty')
            >>> print(repr(qrs))
            <QrWifi>
            ssid: Python
            security: WEP
            password: Monty
            >>> print(str(qrs))
            WIFI:S:Python;T:WEP;P:Monty;;

            >>> with warnings.catch_warnings(record=True) as w:
            ...     qrs = QrWifi('Python', 'X', 'Monty')
            ...     assert(len(w) == 1)
            ...     'Unknown WiFi security' in str(w[-1].message)
            True
            >>> print(repr(qrs))
            <QrWifi>
            ssid: Python
            >>> print(str(qrs))
            WIFI:S:Python;;
        """
        if hidden:
            hidden = 'true'
        else:
            hidden = None
        if security:
            security = security.upper()
            if security not in type(self)._modes:
                warnings.warn('Unknown WiFi security `{}`'.format(security))
                security = None
        if password and not security:
            password = None
        # QrSpecial.__init__(**locals())  # Py3-only
        kws = locals()
        kws.pop('self')
        QrSpecial.__init__(self, **kws)


if __name__ == '__main__':
    import doctest
    doctest.testmod()
