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

# contact object class
# vcard version 3.0: https://tools.ietf.org/html/rfc2426
# vcard version 4.0: https://tools.ietf.org/html/rfc6350

import datetime
import locale
import os
import re
import sys

from atomicwrites import atomic_write
import vobject

import ruamel.yaml
from ruamel.yaml import YAML

from . import helpers
from .object_type import ObjectType

class CarddavObject:

    # vcard v3.0 supports the following type values
    phone_types_v3 = ("bbs", "car", "cell", "fax", "home", "isdn", "msg",
                      "modem", "pager", "pcs", "video", "voice", "work")
    email_types_v3 = ("home", "internet", "work", "x400")
    address_types_v3 = ("dom", "intl", "home", "parcel", "postal", "work")
    # vcard v4.0 supports the following type values
    phone_types_v4 = ("text", "voice", "fax", "cell", "video", "pager",
                      "textphone", "home", "work")
    email_types_v4 = ("home", "internet", "work")
    address_types_v4 = ("home", "work")

    def __init__(self, address_book, filename, supported_private_objects,
                 vcard_version, localize_dates):
        """Initialize the vcard object.

        :param address_book: a reference to the address book where this vcard
            is stored
        :type address_book: khard.address_book.AddressBook
        :param filename: the path to the file where this vcard is stored or
            None
        :type filename: str or NoneType
        :param supported_private_objects: the list of private property names
            that will be loaded from the actual vcard and represented in this
            pobject
        :type supported_private_objects: list(str)
        :param vcard_version: str or None
        :type vcard_version: str
        :param localize_dates: should the formatted output of anniversary and
            birthday be localized or should the isoformat be used instead
        :type localize_dates: bool

        """
        self.vcard = None
        self.address_book = address_book
        self.filename = filename
        self.supported_private_objects = supported_private_objects
        self.localize_dates = localize_dates

        # load vcard
        if self.filename is None:
            # create new vcard object
            self.vcard = vobject.vCard()
            # add uid
            self.add_uid(helpers.get_random_uid())
            # use uid for vcard filename
            self.filename = os.path.join(address_book.path,
                                         self.get_uid() + ".vcf")
            # add preferred vcard version
            self._add_version(vcard_version)

        else:
            # create vcard from .vcf file
            with open(self.filename, "r") as file:
                contents = file.read()
            # create vcard object
            try:
                self.vcard = vobject.readOne(contents)
            except Exception:
                # if creation fails, try to repair some vcard attributes
                self.vcard = vobject.readOne(self._filter_invalid_tags(contents))

    #######################################
    # factory methods to create new contact
    #######################################

    @classmethod
    def new_contact(cls, address_book, supported_private_objects, version,
            localize_dates):
        """Use this to create a new and empty contact."""
        return cls(address_book, None, supported_private_objects, version,
                localize_dates)

    @classmethod
    def from_file(cls, address_book, filename, supported_private_objects,
            localize_dates):
        """
        Use this if you want to create a new contact from an existing .vcf
        file.
        """
        return cls(address_book, filename, supported_private_objects, None,
                localize_dates)

    @classmethod
    def from_user_input(cls, address_book, user_input,
                        supported_private_objects, version, localize_dates):
        """Use this if you want to create a new contact from user input."""
        contact = cls(address_book, None, supported_private_objects, version,
                localize_dates)
        contact._process_user_input(user_input)
        return contact

    @classmethod
    def from_existing_contact_with_new_user_input(cls, contact, user_input,
            localize_dates):
        """
        Use this if you want to clone an existing contact and  replace its data
        with new user input in one step.
        """
        contact = cls(contact.address_book, contact.filename,
                      contact.supported_private_objects, None, localize_dates)
        contact._process_user_input(user_input)
        return contact

    ######################################
    # overwrite some default class methods
    ######################################

    def __str__(self):
        return self.get_full_name()

    def __eq__(self, other):
        return isinstance(other, CarddavObject) and \
            self.print_vcard(show_address_book=False, show_uid=False) == \
            other.print_vcard(show_address_book=False, show_uid=False)

    def __ne__(self, other):
        return not self == other

    #####################
    # getters and setters
    #####################

    def _get_string_field(self, field):
        """Get a string field from the underlying vCard.

        :param field: the field value to get
        :type field: str
        :returns: the field value or the empty string
        :rtype: str

        """
        try:
            return getattr(self.vcard, field).value
        except AttributeError:
            return ""

    def _get_rev(self):
        return self._get_string_field("rev")

    def _add_rev(self, dt):
        rev_obj = self.vcard.add('rev')
        rev_obj.value = "%.4d%.2d%.2dT%.2d%.2d%.2dZ" % (
            dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)

    def get_uid(self):
        return self._get_string_field("uid")

    def add_uid(self, uid):
        uid_obj = self.vcard.add('uid')
        uid_obj.value = helpers.convert_to_vcard("uid", uid, ObjectType.string)

    def get_version(self):
        return self._get_string_field("version")

    def _add_version(self, vcard_version):
        version_obj = self.vcard.add('version')
        version_obj.value = helpers.convert_to_vcard("version", vcard_version,
                                                     ObjectType.string)

    def _get_names_part(self, part):
        """Get some part of the "N" entry in the vCard as a list

        :param part: the name to get e.g. "prefix" or "given"
        :type part: str
        :returns: a list of entries for this name part
        :rtype: list(str)

        """
        try:
            the_list = getattr(self.vcard.n.value, part)
        except AttributeError:
            return []
        else:
            # check if list only contains empty strings
            if not ''.join(the_list):
                return []
        return the_list if isinstance(the_list, list) else [the_list]

    def _get_name_prefixes(self):
        return self._get_names_part("prefix")

    def _get_first_names(self):
        return self._get_names_part("given")

    def _get_additional_names(self):
        return self._get_names_part("additional")

    def _get_last_names(self):
        return self._get_names_part("family")

    def _get_name_suffixes(self):
        return self._get_names_part("suffix")

    def get_full_name(self):
        return self._get_string_field("fn")

    def get_first_name_last_name(self):
        """
        :rtype: str
        """
        names = []
        if self._get_first_names():
            names += self._get_first_names()
        if self._get_additional_names():
            names += self._get_additional_names()
        if self._get_last_names():
            names += self._get_last_names()
        if names:
            return helpers.list_to_string(names, " ")
        else:
            return self.get_full_name()

    def get_last_name_first_name(self):
        """
        :rtype: str
        """
        last_names = []
        if self._get_last_names():
            last_names += self._get_last_names()
        first_and_additional_names = []
        if self._get_first_names():
            first_and_additional_names += self._get_first_names()
        if self._get_additional_names():
            first_and_additional_names += self._get_additional_names()
        if last_names and first_and_additional_names:
            return "{}, {}".format(
                helpers.list_to_string(last_names, " "),
                helpers.list_to_string(first_and_additional_names, " "))
        elif last_names:
            return helpers.list_to_string(last_names, " ")
        elif first_and_additional_names:
            return helpers.list_to_string(first_and_additional_names, " ")
        else:
            return self.get_full_name()

    def _add_name(self, prefix, first_name, additional_name, last_name,
                  suffix):
        # n
        name_obj = self.vcard.add('n')
        name_obj.value = vobject.vcard.Name(
            prefix=helpers.convert_to_vcard(
                "name prefix", prefix, ObjectType.string_or_list_with_strings),
            given=helpers.convert_to_vcard(
                "first name", first_name,
                ObjectType.string_or_list_with_strings),
            additional=helpers.convert_to_vcard(
                "additional name", additional_name,
                ObjectType.string_or_list_with_strings),
            family=helpers.convert_to_vcard(
                "last name", last_name, ObjectType.string_or_list_with_strings),
            suffix=helpers.convert_to_vcard(
                "name suffix", suffix, ObjectType.string_or_list_with_strings))
        # fn
        if not self.vcard.getChildValue("fn") and (self._get_first_names() or
                                                   self._get_last_names()):
            names = []
            if self._get_name_prefixes():
                names += self._get_name_prefixes()
            if self._get_first_names():
                names += self._get_first_names()
            if self._get_last_names():
                names += self._get_last_names()
            if self._get_name_suffixes():
                names += self._get_name_suffixes()
            name_obj = self.vcard.add('fn')
            name_obj.value = helpers.list_to_string(names, " ")

    def _get_organisations(self):
        """
        :returns: list of organisations, sorted alphabetically
        :rtype: list(list(str))
        """
        organisations = []
        for child in self.vcard.getChildren():
            if child.name == "ORG":
                organisations.append(child.value)
        return sorted(organisations)

    def _add_organisation(self, organisation):
        org_obj = self.vcard.add('org')
        org_obj.value = helpers.convert_to_vcard(
            "organisation", organisation, ObjectType.list_with_strings)
        # check if fn attribute is already present
        if not self.vcard.getChildValue("fn") and self._get_organisations():
            # if not, set fn to organisation name
            org_value = helpers.list_to_string(self._get_organisations()[0],
                                               ", ")
            name_obj = self.vcard.add('fn')
            name_obj.value = org_value.replace("\n", " ").replace("\\", "")
            showas_obj = self.vcard.add('x-abshowas')
            showas_obj.value = "COMPANY"

    def _get_titles(self):
        """
        :rtype: list(list(str))
        """
        titles = []
        for child in self.vcard.getChildren():
            if child.name == "TITLE":
                titles.append(child.value)
        return sorted(titles)

    def _add_title(self, title):
        title_obj = self.vcard.add('title')
        title_obj.value = helpers.convert_to_vcard("title", title,
                                                   ObjectType.string)

    def _get_roles(self):
        """
        :rtype: list(list(str))
        """
        roles = []
        for child in self.vcard.getChildren():
            if child.name == "ROLE":
                roles.append(child.value)
        return sorted(roles)

    def _add_role(self, role):
        role_obj = self.vcard.add('role')
        role_obj.value = helpers.convert_to_vcard("role", role,
                                                  ObjectType.string)

    def get_phone_numbers(self):
        """
        : returns: dict of type and phone number list
        :rtype: dict(str, list(str))
        """
        phone_dict = {}
        for child in self.vcard.getChildren():
            if child.name == "TEL":
                # phone types
                type = helpers.list_to_string(
                    self._get_types_for_vcard_object(child, "voice"), ", ")
                if type not in phone_dict:
                    phone_dict[type] = []
                # phone value
                #
                # vcard version 4.0 allows URI scheme "tel" in phone attribute value
                # Doc: https://tools.ietf.org/html/rfc6350#section-6.4.1
                # example: TEL;VALUE=uri;PREF=1;TYPE="voice,home":tel:+1-555-555-5555;ext=5555
                if child.value.lower().startswith("tel:"):
                    # cut off the "tel:" uri prefix
                    phone_dict[type].append(child.value[4:])
                else:
                    # free text field
                    phone_dict[type].append(child.value)
        # sort phone number lists
        for number_list in phone_dict.values():
            number_list.sort()
        return phone_dict

    def _add_phone_number(self, type, number):
        standard_types, custom_types, pref = self._parse_type_value(
            helpers.string_to_list(type, ","), number, self.phone_types_v4 if
            self.get_version() == "4.0" else self.phone_types_v3)
        if not standard_types and not custom_types and pref == 0:
            raise ValueError("Error: label for phone number " + number +
                             " is missing.")
        elif len(custom_types) > 1:
            raise ValueError("Error: phone number " + number + " got more "
                             "than one custom label: " +
                             helpers.list_to_string(custom_types, ", "))
        else:
            phone_obj = self.vcard.add('tel')
            if self.get_version() == "4.0":
                phone_obj.value = "tel:%s" % helpers.convert_to_vcard(
                    "phone number", number, ObjectType.string)
                phone_obj.params['VALUE'] = ["uri"]
                if pref > 0:
                    phone_obj.params['PREF'] = str(pref)
            else:
                phone_obj.value = helpers.convert_to_vcard(
                    "phone number", number, ObjectType.string)
                if pref > 0:
                    standard_types.append("pref")
            if standard_types:
                phone_obj.params['TYPE'] = standard_types
            if custom_types:
                number_of_custom_phone_number_labels = 0
                for label in self.vcard.getChildren():
                    if label.name == "X-ABLABEL" \
                            and label.group.startswith("itemtel"):
                        number_of_custom_phone_number_labels += 1
                group_name = "itemtel%d" % (
                    number_of_custom_phone_number_labels+1)
                phone_obj.group = group_name
                label_obj = self.vcard.add('x-ablabel')
                label_obj.group = group_name
                label_obj.value = custom_types[0]

    def get_email_addresses(self):
        """
        : returns: dict of type and email address list
        :rtype: dict(str, list(str))
        """
        email_dict = {}
        for child in self.vcard.getChildren():
            if child.name == "EMAIL":
                type = helpers.list_to_string(
                    self._get_types_for_vcard_object(child, "internet"), ", ")
                if type not in email_dict:
                    email_dict[type] = []
                email_dict[type].append(child.value)
        # sort email address lists
        for email_list in email_dict.values():
            email_list.sort()
        return email_dict

    def add_email_address(self, type, address):
        standard_types, custom_types, pref = self._parse_type_value(
            helpers.string_to_list(type, ","), address, self.email_types_v4 if
            self.get_version() == "4.0" else self.email_types_v3)
        if not standard_types and not custom_types and pref == 0:
            raise ValueError("Error: label for email address " + address +
                             " is missing.")
        elif len(custom_types) > 1:
            raise ValueError("Error: email address " + address + " got more "
                             "than one custom label: " +
                             helpers.list_to_string(custom_types, ", "))
        else:
            email_obj = self.vcard.add('email')
            email_obj.value = helpers.convert_to_vcard("email address", address,
                                                       ObjectType.string)
            if self.get_version() == "4.0":
                if pref > 0:
                    email_obj.params['PREF'] = str(pref)
            else:
                if pref > 0:
                    standard_types.append("pref")
            if standard_types:
                email_obj.params['TYPE'] = standard_types
            if custom_types:
                number_of_custom_email_labels = 0
                for label in self.vcard.getChildren():
                    if label.name == "X-ABLABEL" \
                            and label.group.startswith("itememail"):
                        number_of_custom_email_labels += 1
                group_name = "itememail%d" % (number_of_custom_email_labels+1)
                email_obj.group = group_name
                label_obj = self.vcard.add('x-ablabel')
                label_obj.group = group_name
                label_obj.value = custom_types[0]

    def get_post_addresses(self):
        """
        : returns: dict of type and post address list
        :rtype: dict(str, list(dict(str,list|str)))
        """
        post_adr_dict = {}
        for child in self.vcard.getChildren():
            if child.name == "ADR":
                type = helpers.list_to_string(
                    self._get_types_for_vcard_object(child, "home"), ", ")
                if type not in post_adr_dict:
                    post_adr_dict[type] = []
                post_adr_dict[type].append(
                    {
                        "box": child.value.box,
                        "extended": child.value.extended,
                        "street": child.value.street,
                        "code": child.value.code,
                        "city": child.value.city,
                        "region": child.value.region,
                        "country": child.value.country
                    })
        # sort post address lists
        for post_adr_list in post_adr_dict.values():
            post_adr_list.sort(key=lambda x: (
                helpers.list_to_string(x['city'], " ").lower(),
                helpers.list_to_string(x['street'], " ").lower()))
        return post_adr_dict

    def get_formatted_post_addresses(self):
        formatted_post_adr_dict = {}
        for type, post_adr_list in self.get_post_addresses().items():
            formatted_post_adr_dict[type] = []
            for post_adr in post_adr_list:
                strings = []
                if post_adr.get("street"):
                    strings.append(
                        helpers.list_to_string(post_adr.get("street"), "\n"))
                if post_adr.get("box") and post_adr.get("extended"):
                    strings.append("{} {}".format(
                        helpers.list_to_string(post_adr.get("box"), " "),
                        helpers.list_to_string(post_adr.get("extended"), " ")))
                elif post_adr.get("box"):
                    strings.append(
                        helpers.list_to_string(post_adr.get("box"), " "))
                elif post_adr.get("extended"):
                    strings.append(
                        helpers.list_to_string(post_adr.get("extended"), " "))
                if post_adr.get("code") and post_adr.get("city"):
                    strings.append("{} {}".format(
                        helpers.list_to_string(post_adr.get("code"), " "),
                        helpers.list_to_string(post_adr.get("city"), " ")))
                elif post_adr.get("code"):
                    strings.append(
                        helpers.list_to_string(post_adr.get("code"), " "))
                elif post_adr.get("city"):
                    strings.append(
                        helpers.list_to_string(post_adr.get("city"), " "))
                if post_adr.get("region") and post_adr.get("country"):
                    strings.append("{}, {}".format(
                        helpers.list_to_string(post_adr.get("region"), " "),
                        helpers.list_to_string(post_adr.get("country"), " ")))
                elif post_adr.get("region"):
                    strings.append(
                        helpers.list_to_string(post_adr.get("region"), " "))
                elif post_adr.get("country"):
                    strings.append(
                        helpers.list_to_string(post_adr.get("country"), " "))
                formatted_post_adr_dict[type].append('\n'.join(strings))
        return formatted_post_adr_dict

    def _add_post_address(self, type, box, extended, street, code, city,
                          region, country):
        standard_types, custom_types, pref = self._parse_type_value(
            helpers.string_to_list(type, ","), "{}, {}".format(street, city),
            self.address_types_v4 if self.get_version() == "4.0" else
            self.address_types_v3)
        if not standard_types and not custom_types and pref == 0:
            raise ValueError("Error: label for post address " + street +
                             " is missing.")
        elif len(custom_types) > 1:
            raise ValueError("Error: post address " + street + " got more "
                             "than one custom " "label: " +
                             helpers.list_to_string(custom_types, ", "))
        else:
            adr_obj = self.vcard.add('adr')
            adr_obj.value = vobject.vcard.Address(
                box=helpers.convert_to_vcard(
                    "box address field", box,
                    ObjectType.string_or_list_with_strings),
                extended=helpers.convert_to_vcard(
                    "extended address field", extended,
                    ObjectType.string_or_list_with_strings),
                street=helpers.convert_to_vcard(
                    "street", street, ObjectType.string_or_list_with_strings),
                code=helpers.convert_to_vcard(
                    "post code", code, ObjectType.string_or_list_with_strings),
                city=helpers.convert_to_vcard(
                    "city", city, ObjectType.string_or_list_with_strings),
                region=helpers.convert_to_vcard(
                    "region", region, ObjectType.string_or_list_with_strings),
                country=helpers.convert_to_vcard(
                    "country", country, ObjectType.string_or_list_with_strings))
            if self.get_version() == "4.0":
                if pref > 0:
                    adr_obj.params['PREF'] = str(pref)
            else:
                if pref > 0:
                    standard_types.append("pref")
            if standard_types:
                adr_obj.params['TYPE'] = standard_types
            if custom_types:
                number_of_custom_post_address_labels = 0
                for label in self.vcard.getChildren():
                    if label.name == "X-ABLABEL" \
                            and label.group.startswith("itemadr"):
                        number_of_custom_post_address_labels += 1
                group_name = "itemadr%d" % (
                    number_of_custom_post_address_labels+1)
                adr_obj.group = group_name
                label_obj = self.vcard.add('x-ablabel')
                label_obj.group = group_name
                label_obj.value = custom_types[0]

    def _get_categories(self):
        """
        :rtype: list(str) or list(list(str))
        """
        category_list = []
        for child in self.vcard.getChildren():
            if child.name == "CATEGORIES":
                value = child.value
                category_list.append(
                    value if isinstance(value, list) else [value])
        if len(category_list) == 1:
            return category_list[0]
        return sorted(category_list)

    def _add_category(self, categories):
        """ categories variable must be a list """
        categories_obj = self.vcard.add('categories')
        categories_obj.value = helpers.convert_to_vcard(
            "category", categories, ObjectType.list_with_strings)

    def get_nicknames(self):
        """
        :rtype: list(list(str))
        """
        nicknames = []
        for child in self.vcard.getChildren():
            if child.name == "NICKNAME":
                nicknames.append(child.value)
        return sorted(nicknames)

    def _add_nickname(self, nickname):
        nickname_obj = self.vcard.add('nickname')
        nickname_obj.value = helpers.convert_to_vcard("nickname", nickname,
                                                      ObjectType.string)

    def _get_notes(self):
        """
        :rtype: list(list(str))
        """
        notes = []
        for child in self.vcard.getChildren():
            if child.name == "NOTE":
                notes.append(child.value)
        return sorted(notes)

    def _add_note(self, note):
        note_obj = self.vcard.add('note')
        note_obj.value = helpers.convert_to_vcard("note", note,
                                                  ObjectType.string)

    def _get_private_objects(self):
        """
        :rtype: dict(str, list(str))
        """
        private_objects = {}
        for child in self.vcard.getChildren():
            if child.name.lower().startswith("x-"):
                try:
                    key_index = [
                        x.lower() for x in self.supported_private_objects
                    ].index(child.name[2:].lower())
                except ValueError:
                    pass
                else:
                    key = self.supported_private_objects[key_index]
                    if key not in private_objects:
                        private_objects[key] = []
                    private_objects[key].append(child.value)
        # sort private object lists
        for value in private_objects.values():
            value.sort()
        return private_objects

    def _add_private_object(self, key, value):
        private_obj = self.vcard.add('X-' + key.upper())
        private_obj.value = helpers.convert_to_vcard(key, value,
                                                     ObjectType.string)

    def _get_webpages(self):
        """
        :rtype: list(list(str))
        """
        urls = []
        for child in self.vcard.getChildren():
            if child.name == "URL":
                urls.append(child.value)
        return sorted(urls)

    def _add_webpage(self, webpage):
        webpage_obj = self.vcard.add('url')
        webpage_obj.value = helpers.convert_to_vcard("webpage", webpage,
                                                     ObjectType.string)

    def get_anniversary(self):
        """:returns: contacts anniversary or None if not available
            :rtype: datetime.datetime or str
        """
        # vcard 4.0 could contain a single text value
        try:
            if self.vcard.anniversary.params.get("VALUE")[0] == "text":
                return self.vcard.anniversary.value
        except (AttributeError, IndexError, TypeError):
            pass
        # else try to convert to a datetime object
        try:
            return helpers.string_to_date(self.vcard.anniversary.value)
        except (AttributeError, ValueError):
            # vcard 3.0: x-anniversary (private object)
            try:
                return helpers.string_to_date(self.vcard.x_anniversary.value)
            except (AttributeError, ValueError):
                pass
        return None

    def get_formatted_anniversary(self):
        return self._format_date_object(
                self.get_anniversary(), self.localize_dates)

    def _add_anniversary(self, date):
        if isinstance(date, str):
            if self.get_version() == "4.0":
                anniversary_obj = self.vcard.add('anniversary')
                anniversary_obj.params['VALUE'] = ["text"]
                anniversary_obj.value = date.strip()
        elif date.year == 1900 and date.month != 0 and date.day != 0 \
                and date.hour == 0 and date.minute == 0 and date.second == 0 \
                and self.get_version() == "4.0":
            anniversary_obj = self.vcard.add('anniversary')
            anniversary_obj.value = "--%.2d%.2d" % (date.month, date.day)
        elif date.tzname() and date.tzname()[3:]:
            if self.get_version() == "4.0":
                anniversary_obj = self.vcard.add('anniversary')
                anniversary_obj.value = "%.4d%.2d%.2dT%.2d%.2d%.2d%s" % (
                    date.year, date.month, date.day, date.hour, date.minute,
                    date.second, date.tzname()[3:])
            else:
                anniversary_obj = self.vcard.add('x-anniversary')
                anniversary_obj.value = "%.4d-%.2d-%.2dT%.2d:%.2d:%.2d%s" % (
                    date.year, date.month, date.day, date.hour, date.minute,
                    date.second, date.tzname()[3:])
        elif date.hour != 0 or date.minute != 0 or date.second != 0:
            if self.get_version() == "4.0":
                anniversary_obj = self.vcard.add('anniversary')
                anniversary_obj.value = "%.4d%.2d%.2dT%.2d%.2d%.2dZ" % (
                    date.year, date.month, date.day, date.hour, date.minute,
                    date.second)
            else:
                anniversary_obj = self.vcard.add('x-anniversary')
                anniversary_obj.value = "%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ" % (
                    date.year, date.month, date.day, date.hour, date.minute,
                    date.second)
        else:
            if self.get_version() == "4.0":
                anniversary_obj = self.vcard.add('anniversary')
                anniversary_obj.value = "%.4d%.2d%.2d" % (date.year, date.month,
                                                   date.day)
            else:
                anniversary_obj = self.vcard.add('x-anniversary')
                anniversary_obj.value = "%.4d-%.2d-%.2d" % (date.year,
                        date.month, date.day)

    def get_birthday(self):
        """:returns: contacts birthday or None if not available
            :rtype: datetime.datetime or str
        """
        # vcard 4.0 could contain a single text value
        try:
            if self.vcard.bday.params.get("VALUE")[0] == "text":
                return self.vcard.bday.value
        except (AttributeError, IndexError, TypeError):
            pass
        # else try to convert to a datetime object
        try:
            return helpers.string_to_date(self.vcard.bday.value)
        except (AttributeError, ValueError):
            pass
        return None

    def get_formatted_birthday(self):
        return self._format_date_object(
                self.get_birthday(), self.localize_dates)

    def _add_birthday(self, date):
        if isinstance(date, str):
            if self.get_version() == "4.0":
                bday_obj = self.vcard.add('bday')
                bday_obj.params['VALUE'] = ["text"]
                bday_obj.value = date.strip()
        elif date.year == 1900 and date.month != 0 and date.day != 0 \
                and date.hour == 0 and date.minute == 0 and date.second == 0 \
                and self.get_version() == "4.0":
            bday_obj = self.vcard.add('bday')
            bday_obj.value = "--%.2d%.2d" % (date.month, date.day)
        elif date.tzname() and date.tzname()[3:]:
            bday_obj = self.vcard.add('bday')
            if self.get_version() == "4.0":
                bday_obj.value = "%.4d%.2d%.2dT%.2d%.2d%.2d%s" % (
                    date.year, date.month, date.day, date.hour, date.minute,
                    date.second, date.tzname()[3:])
            else:
                bday_obj.value = "%.4d-%.2d-%.2dT%.2d:%.2d:%.2d%s" % (
                    date.year, date.month, date.day, date.hour, date.minute,
                    date.second, date.tzname()[3:])
        elif date.hour != 0 or date.minute != 0 or date.second != 0:
            bday_obj = self.vcard.add('bday')
            if self.get_version() == "4.0":
                bday_obj.value = "%.4d%.2d%.2dT%.2d%.2d%.2dZ" % (
                    date.year, date.month, date.day, date.hour, date.minute,
                    date.second)
            else:
                bday_obj.value = "%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ" % (
                    date.year, date.month, date.day, date.hour, date.minute,
                    date.second)
        else:
            bday_obj = self.vcard.add('bday')
            if self.get_version() == "4.0":
                bday_obj.value = "%.4d%.2d%.2d" % (date.year, date.month,
                                                   date.day)
            else:
                bday_obj.value = "%.4d-%.2d-%.2d" % (date.year, date.month,
                                                     date.day)

    #######################
    # object helper methods
    #######################

    @staticmethod
    def _format_date_object(date, localize):
        if date:
            if isinstance(date, str):
                return date
            elif date.year == 1900 and date.month != 0 and date.day != 0 \
                    and date.hour == 0 and date.minute == 0 \
                    and date.second == 0:
                return "--%.2d-%.2d" % (date.month, date.day)
            elif (date.tzname() and date.tzname()[3:]) or \
                    (date.hour != 0 or date.minute != 0 or date.second != 0):
                if localize:
                    return date.strftime(locale.nl_langinfo(locale.D_T_FMT))
                else:
                    utc_offset=-time.timezone/60/60
                    return date.strftime(
                            "%Y-%m-%dT%H:%M:%S+" + str(int(utc_offset)).zfill(2) + ":00")
            else:
                if localize:
                    return date.strftime(locale.nl_langinfo(locale.D_FMT))
                else:
                    return date.strftime("%Y-%m-%d")
        return ""

    @staticmethod
    def _filter_invalid_tags(contents):
        contents = re.sub('(?i)' + re.escape('X-messaging/aim-All'), 'X-AIM',
                          contents)
        contents = re.sub('(?i)' + re.escape('X-messaging/gadu-All'),
                          'X-GADUGADU', contents)
        contents = re.sub('(?i)' + re.escape('X-messaging/groupwise-All'),
                          'X-GROUPWISE', contents)
        contents = re.sub('(?i)' + re.escape('X-messaging/icq-All'), 'X-ICQ',
                          contents)
        contents = re.sub('(?i)' + re.escape('X-messaging/xmpp-All'),
                          'X-JABBER', contents)
        contents = re.sub('(?i)' + re.escape('X-messaging/msn-All'), 'X-MSN',
                          contents)
        contents = re.sub('(?i)' + re.escape('X-messaging/yahoo-All'),
                          'X-YAHOO', contents)
        contents = re.sub('(?i)' + re.escape('X-messaging/skype-All'),
                          'X-SKYPE', contents)
        contents = re.sub('(?i)' + re.escape('X-messaging/irc-All'), 'X-IRC',
                          contents)
        contents = re.sub('(?i)' + re.escape('X-messaging/sip-All'), 'X-SIP',
                          contents)
        return contents

    def _process_user_input(self, input):
        yaml_parser = YAML(typ='base')
        # parse user input string
        try:
            contact_data = yaml_parser.load(input)
        except (ruamel.yaml.parser.ParserError,
                ruamel.yaml.scanner.ScannerError) as err:
            raise ValueError(err)
        else:
            if contact_data is None:
                raise ValueError("Error: Found no contact information")

        # check for available data
        # at least enter name or organisation
        if not contact_data.get("First name") \
                and not contact_data.get("Last name") \
                and not contact_data.get("Organisation"):
            raise ValueError(
                "Error: You must either enter a name or an organisation")

        # update rev
        self.delete_vcard_object("REV")
        self._add_rev(datetime.datetime.now())

        # name
        self.delete_vcard_object("FN")
        self.delete_vcard_object("N")
        # although the "n" attribute is not explisitely required by the vcard
        # specification,
        # the vobject library throws an exception, if it doesn't exist
        # so add the name regardless if it's empty or not
        self._add_name(
            contact_data.get("Prefix", ""), contact_data.get("First name", ""),
            contact_data.get("Additional", ""),
            contact_data.get("Last name", ""), contact_data.get("Suffix", ""))
        # nickname
        self.delete_vcard_object("NICKNAME")
        if contact_data.get("Nickname"):
            if isinstance(contact_data.get("Nickname"), str):
                self._add_nickname(contact_data.get("Nickname"))
            elif isinstance(contact_data.get("Nickname"), list):
                for nickname in contact_data.get("Nickname"):
                    if nickname:
                        self._add_nickname(nickname)
            else:
                raise ValueError(
                    "Error: nickname must be a string or a list of strings")

        # organisation
        self.delete_vcard_object("ORG")
        self.delete_vcard_object("X-ABSHOWAS")
        if contact_data.get("Organisation"):
            if isinstance(contact_data.get("Organisation"), str):
                self._add_organisation([contact_data.get("Organisation")])
            elif isinstance(contact_data.get("Organisation"), list):
                for organisation in contact_data.get("Organisation"):
                    if organisation:
                        if isinstance(organisation, str):
                            self._add_organisation([organisation])
                        else:
                            self._add_organisation(organisation)
            else:
                raise ValueError("Error: organisation must be a string or a "
                                 "list of strings")

        # role
        self.delete_vcard_object("ROLE")
        if contact_data.get("Role"):
            if isinstance(contact_data.get("Role"), str):
                self._add_role(contact_data.get("Role"))
            elif isinstance(contact_data.get("Role"), list):
                for role in contact_data.get("Role"):
                    if role:
                        self._add_role(role)
            else:
                raise ValueError(
                    "Error: role must be a string or a list of strings")

        # title
        self.delete_vcard_object("TITLE")
        if contact_data.get("Title"):
            if isinstance(contact_data.get("Title"), str):
                self._add_title(contact_data.get("Title"))
            elif isinstance(contact_data.get("Title"), list):
                for title in contact_data.get("Title"):
                    if title:
                        self._add_title(title)
            else:
                raise ValueError(
                    "Error: title must be a string or a list of strings")

        # phone
        self.delete_vcard_object("TEL")
        if contact_data.get("Phone"):
            if isinstance(contact_data.get("Phone"), dict):
                for type, number_list in contact_data.get("Phone").items():
                    if isinstance(number_list, str):
                        number_list = [number_list]
                    if isinstance(number_list, list):
                        for number in number_list:
                            if number:
                                self._add_phone_number(type, number)
                    else:
                        raise ValueError(
                            "Error: got no number or list of numbers for the "
                            "phone number type " + type)
            else:
                raise ValueError(
                    "Error: missing type value for phone number field")

        # email
        self.delete_vcard_object("EMAIL")
        if contact_data.get("Email"):
            if isinstance(contact_data.get("Email"), dict):
                for type, email_list in contact_data.get("Email").items():
                    if isinstance(email_list, str):
                        email_list = [email_list]
                    if isinstance(email_list, list):
                        for email in email_list:
                            if email:
                                self.add_email_address(type, email)
                    else:
                        raise ValueError(
                            "Error: got no email or list of emails for the "
                            "email address type " + type)
            else:
                raise ValueError(
                    "Error: missing type value for email address field")

        # post addresses
        self.delete_vcard_object("ADR")
        if contact_data.get("Address"):
            if isinstance(contact_data.get("Address"), dict):
                for type, post_adr_list in contact_data.get("Address").items():
                    if isinstance(post_adr_list, dict):
                        post_adr_list = [post_adr_list]
                    if isinstance(post_adr_list, list):
                        for post_adr in post_adr_list:
                            if isinstance(post_adr, dict):
                                address_not_empty = False
                                for key, value in post_adr.items():
                                    if key in ["Box", "Extended", "Street",
                                               "Code", "City", "Region",
                                               "Country"] and value:
                                        address_not_empty = True
                                        break
                                if address_not_empty:
                                    self._add_post_address(
                                        type, post_adr.get("Box", ""),
                                        post_adr.get("Extended", ""),
                                        post_adr.get("Street", ""),
                                        post_adr.get("Code", ""),
                                        post_adr.get("City", ""),
                                        post_adr.get("Region", ""),
                                        post_adr.get("Country", ""))
                            else:
                                raise ValueError(
                                    "Error: one of the " + type + " type "
                                    "address list items does not contain an "
                                    "address")
                    else:
                        raise ValueError(
                            "Error: got no address or list of addresses for "
                            "the post address type " + type)
            else:
                raise ValueError(
                    "Error: missing type value for post address field")

        # categories
        self.delete_vcard_object("CATEGORIES")
        if contact_data.get("Categories"):
            if isinstance(contact_data.get("Categories"), str):
                self._add_category([contact_data.get("Categories")])
            elif isinstance(contact_data.get("Categories"), list):
                only_contains_strings = True
                for sub_category in contact_data.get("Categories"):
                    if not isinstance(sub_category, str):
                        only_contains_strings = False
                        break
                # if the category list only contains strings, pack all of them
                # in a single CATEGORIES vcard tag
                if only_contains_strings:
                    self._add_category(contact_data.get("Categories"))
                else:
                    for sub_category in contact_data.get("Categories"):
                        if sub_category:
                            if isinstance(sub_category, str):
                                self._add_category([sub_category])
                            else:
                                self._add_category(sub_category)
            else:
                raise ValueError(
                    "Error: category must be a string or a list of strings")

        # urls
        self.delete_vcard_object("URL")
        if contact_data.get("Webpage"):
            if isinstance(contact_data.get("Webpage"), str):
                self._add_webpage(contact_data.get("Webpage"))
            elif isinstance(contact_data.get("Webpage"), list):
                for webpage in contact_data.get("Webpage"):
                    if webpage:
                        self._add_webpage(webpage)
            else:
                raise ValueError(
                    "Error: webpage must be a string or a list of strings")

        # anniversary
        self.delete_vcard_object("ANNIVERSARY")
        self.delete_vcard_object("X-ANNIVERSARY")
        if contact_data.get("Anniversary"):
            if isinstance(contact_data.get("Anniversary"), str):
                if re.match(r"^text[\s]*=.*$", contact_data.get("Anniversary")):
                    l = [x.strip() for x in
                         re.split("text[\s]*=", contact_data.get("Anniversary"))
                         if x.strip()]
                    if self.get_version() == "4.0":
                        date = ', '.join(l)
                    else:
                        raise ValueError(
                            "Error: Free text format for anniversary only "
                            "usable with vcard version 4.0.")
                elif re.match(r"^--\d{4}$", contact_data.get("Anniversary")) \
                        and self.get_version() != "4.0":
                    raise ValueError(
                        "Error: Anniversary format --mmdd only usable with "
                        "vcard version 4.0. You may use 1900 as placeholder, "
                        "if the year of the anniversary is unknown.")
                elif re.match(
                        r"^--\d{2}-\d{2}$", contact_data.get("Anniversary")) \
                        and self.get_version() != "4.0":
                    raise ValueError(
                        "Error: Anniversary format --mm-dd only usable with "
                        "vcard version 4.0. You may use 1900 as placeholder, "
                        "if the year of the anniversary is unknown.")
                else:
                    try:
                        date = helpers.string_to_date(
                            contact_data.get("Anniversary"))
                    except ValueError:
                        raise ValueError(
                            "Error: Wrong anniversary format or invalid date\n"
                            "Use format yyyy-mm-dd or yyyy-mm-ddTHH:MM:SS")
                if date:
                    self._add_anniversary(date)
            else:
                raise ValueError("Error: anniversary must be a string object.")

        # birthday
        self.delete_vcard_object("BDAY")
        if contact_data.get("Birthday"):
            if isinstance(contact_data.get("Birthday"), str):
                if re.match(r"^text[\s]*=.*$", contact_data.get("Birthday")):
                    l = [x.strip() for x in
                         re.split("text[\s]*=", contact_data.get("Birthday"))
                         if x.strip()]
                    if self.get_version() == "4.0":
                        date = ', '.join(l)
                    else:
                        raise ValueError(
                            "Error: Free text format for birthday only usable "
                            "with vcard version 4.0.")
                elif re.match(r"^--\d{4}$", contact_data.get("Birthday")) \
                        and self.get_version() != "4.0":
                    raise ValueError(
                        "Error: Birthday format --mmdd only usable with "
                        "vcard version 4.0. You may use 1900 as placeholder, "
                        "if the year of birth is unknown.")
                elif re.match(
                        r"^--\d{2}-\d{2}$", contact_data.get("Birthday")) \
                        and self.get_version() != "4.0":
                    raise ValueError(
                        "Error: Birthday format --mm-dd only usable with "
                        "vcard version 4.0. You may use 1900 as placeholder, "
                        "if the year of birth is unknown.")
                else:
                    try:
                        date = helpers.string_to_date(
                            contact_data.get("Birthday"))
                    except ValueError:
                        raise ValueError(
                            "Error: Wrong birthday format or invalid date\n"
                            "Use format yyyy-mm-dd or yyyy-mm-ddTHH:MM:SS")
                if date:
                    self._add_birthday(date)
            else:
                raise ValueError("Error: birthday must be a string object.")

        # private objects
        for supported in self.supported_private_objects:
            self.delete_vcard_object("X-{}".format(supported.upper()))
        if contact_data.get("Private"):
            if isinstance(contact_data.get("Private"), dict):
                for key, value_list in contact_data.get("Private").items():
                    if key in self.supported_private_objects:
                        if isinstance(value_list, str):
                            value_list = [value_list]
                        if isinstance(value_list, list):
                            for value in value_list:
                                if value:
                                    self._add_private_object(key, value)
                        else:
                            raise ValueError(
                                "Error: got no value or list of values for "
                                "the private object " + key)
                    else:
                        raise ValueError(
                            "Error: private object key " + key + " was "
                            "changed.\nSupported private keys: " + ', '.join(
                                self.supported_private_objects))
            else:
                raise ValueError("Error: private objects must consist of a "
                                 "key : value pair.")

        # notes
        self.delete_vcard_object("NOTE")
        if contact_data.get("Note"):
            if isinstance(contact_data.get("Note"), str):
                self._add_note(contact_data.get("Note"))
            elif isinstance(contact_data.get("Note"), list):
                for note in contact_data.get("Note"):
                    if note:
                        self._add_note(note)
            else:
                raise ValueError(
                    "Error: note must be a string or a list of strings\n"
                    "Use the | character to create a multi-line note.")

    def get_template(self):
        strings = []
        for line in helpers.get_new_contact_template().splitlines():
            if line.startswith("#"):
                strings.append(line)
            elif line == "":
                strings.append(line)

            elif line.lower().startswith("prefix"):
                strings += helpers.convert_to_yaml(
                    "Prefix", self._get_name_prefixes(), 0, 11, True)
            elif line.lower().startswith("first name"):
                strings += helpers.convert_to_yaml(
                    "First name", self._get_first_names(), 0, 11, True)
            elif line.lower().startswith("additional"):
                strings += helpers.convert_to_yaml(
                    "Additional", self._get_additional_names(), 0, 11, True)
            elif line.lower().startswith("last name"):
                strings += helpers.convert_to_yaml(
                    "Last name", self._get_last_names(), 0, 11, True)
            elif line.lower().startswith("suffix"):
                strings += helpers.convert_to_yaml(
                    "Suffix", self._get_name_suffixes(), 0, 11, True)
            elif line.lower().startswith("nickname"):
                strings += helpers.convert_to_yaml(
                    "Nickname", self.get_nicknames(), 0, 9, True)

            elif line.lower().startswith("organisation"):
                strings += helpers.convert_to_yaml(
                    "Organisation", self._get_organisations(), 0, 13, True)
            elif line.lower().startswith("title"):
                strings += helpers.convert_to_yaml(
                    "Title", self._get_titles(), 0, 6, True)
            elif line.lower().startswith("role"):
                strings += helpers.convert_to_yaml(
                    "Role", self._get_roles(), 0, 6, True)

            elif line.lower().startswith("phone"):
                strings.append("Phone :")
                if not self.get_phone_numbers().keys():
                    strings.append("    cell : ")
                    strings.append("    home : ")
                else:
                    longest_key = max(self.get_phone_numbers().keys(), key=len)
                    for type, number_list in sorted(
                            self.get_phone_numbers().items(),
                            key=lambda k: k[0].lower()):
                        strings += helpers.convert_to_yaml(
                            type, number_list, 4, len(longest_key)+1, True)

            elif line.lower().startswith("email"):
                strings.append("Email :")
                if not self.get_email_addresses().keys():
                    strings.append("    home : ")
                    strings.append("    work : ")
                else:
                    longest_key = max(self.get_email_addresses().keys(),
                                      key=len)
                    for type, email_list in sorted(
                            self.get_email_addresses().items(),
                            key=lambda k: k[0].lower()):
                        strings += helpers.convert_to_yaml(
                            type, email_list, 4, len(longest_key)+1, True)

            elif line.lower().startswith("address"):
                strings.append("Address :")
                if not self.get_post_addresses().keys():
                    strings.append("    home :")
                    strings.append("        Box      : ")
                    strings.append("        Extended : ")
                    strings.append("        Street   : ")
                    strings.append("        Code     : ")
                    strings.append("        City     : ")
                    strings.append("        Region   : ")
                    strings.append("        Country  : ")
                else:
                    for type, post_adr_list in sorted(
                            self.get_post_addresses().items(),
                            key=lambda k: k[0].lower()):
                        strings.append("    %s:" % type)
                        for post_adr in post_adr_list:
                            indentation = 8
                            if len(post_adr_list) > 1:
                                indentation += 4
                                strings.append("        -")
                            strings += helpers.convert_to_yaml(
                                "Box", post_adr.get("box"), indentation, 9,
                                True)
                            strings += helpers.convert_to_yaml(
                                "Extended", post_adr.get("extended"),
                                indentation, 9, True)
                            strings += helpers.convert_to_yaml(
                                "Street", post_adr.get("street"), indentation,
                                9, True)
                            strings += helpers.convert_to_yaml(
                                "Code", post_adr.get("code"), indentation, 9,
                                True)
                            strings += helpers.convert_to_yaml(
                                "City", post_adr.get("city"), indentation, 9,
                                True)
                            strings += helpers.convert_to_yaml(
                                "Region", post_adr.get("region"), indentation,
                                9, True)
                            strings += helpers.convert_to_yaml(
                                "Country", post_adr.get("country"),
                                indentation, 9, True)

            elif line.lower().startswith("private"):
                strings.append("Private :")
                if self.supported_private_objects:
                    longest_key = max(self.supported_private_objects, key=len)
                    for object in self.supported_private_objects:
                        strings += helpers.convert_to_yaml(
                            object, self._get_private_objects().get(object, ""),
                            4, len(longest_key)+1, True)

            elif line.lower().startswith("anniversary"):
                anniversary = self.get_anniversary()
                if anniversary:
                    if isinstance(anniversary, str):
                        strings.append("Anniversary : text= %s" % anniversary)
                    elif anniversary.year == 1900 and anniversary.month != 0 and \
                            anniversary.day != 0 and anniversary.hour == 0 and \
                            anniversary.minute == 0 and anniversary.second == 0 and \
                            self.get_version() == "4.0":
                        strings.append("Anniversary : --%.2d-%.2d"
                                       % (anniversary.month, anniversary.day))
                    elif (anniversary.tzname() and anniversary.tzname()[3:]) or \
                            (anniversary.hour != 0 or anniversary.minute != 0
                             or anniversary.second != 0):
                        strings.append("Anniversary : %s" % anniversary.isoformat())
                    else:
                        strings.append("Anniversary : %.4d-%.2d-%.2d" % (
                            anniversary.year, anniversary.month, anniversary.day))
                else:
                    strings.append("Anniversary : ")
            elif line.lower().startswith("birthday"):
                birthday = self.get_birthday()
                if birthday:
                    if isinstance(birthday, str):
                        strings.append("Birthday : text= %s" % birthday)
                    elif birthday.year == 1900 and birthday.month != 0 and \
                            birthday.day != 0 and birthday.hour == 0 and \
                            birthday.minute == 0 and birthday.second == 0 and \
                            self.get_version() == "4.0":
                        strings.append("Birthday : --%.2d-%.2d"
                                       % (birthday.month, birthday.day))
                    elif (birthday.tzname() and birthday.tzname()[3:]) or \
                            (birthday.hour != 0 or birthday.minute != 0
                             or birthday.second != 0):
                        strings.append("Birthday : %s" % birthday.isoformat())
                    else:
                        strings.append("Birthday : %.4d-%.2d-%.2d" % (
                            birthday.year, birthday.month, birthday.day))
                else:
                    strings.append("Birthday : ")
            elif line.lower().startswith("categories"):
                strings += helpers.convert_to_yaml(
                    "Categories", self._get_categories(), 0, 11, True)
            elif line.lower().startswith("note"):
                strings += helpers.convert_to_yaml(
                    "Note", self._get_notes(), 0, 5, True)
            elif line.lower().startswith("webpage"):
                strings += helpers.convert_to_yaml(
                    "Webpage", self._get_webpages(), 0, 8, True)
        return '\n'.join(strings) + "\n"   # posix standard: eof char must be \n

    def print_vcard(self, show_address_book=True, show_uid=True):
        strings = []

        # name
        if self._get_first_names() or self._get_last_names():
            names = []
            if self._get_name_prefixes():
                names += self._get_name_prefixes()
            if self._get_first_names():
                names += self._get_first_names()
            if self._get_additional_names():
                names += self._get_additional_names()
            if self._get_last_names():
                names += self._get_last_names()
            if self._get_name_suffixes():
                names += self._get_name_suffixes()
            strings.append("Name: %s" % helpers.list_to_string(names, " "))
        # organisation
        if self._get_organisations():
            strings += helpers.convert_to_yaml(
                "Organisation", self._get_organisations(), 0, -1, False)
        # fn as fallback
        if not strings:
            strings.append("Name: %s" % self.get_full_name())

        # address book name
        if show_address_book:
            strings.append("Address book: %s" % self.address_book.name)

        # person related information
        if self.get_birthday() is not None or self.get_anniversary() is not None \
                or self.get_nicknames() or self._get_roles() or self._get_titles():
            strings.append("General:")
            if self.get_anniversary():
                strings.append("    Anniversary: %s"
                               % self.get_formatted_anniversary())
            if self.get_birthday():
                strings.append(
                    "    Birthday: {}".format(self.get_formatted_birthday()))
            if self.get_nicknames():
                strings += helpers.convert_to_yaml(
                    "Nickname", self.get_nicknames(), 4, -1, False)
            if self._get_roles():
                strings += helpers.convert_to_yaml(
                    "Role", self._get_roles(), 4, -1, False)
            if self._get_titles():
                strings += helpers.convert_to_yaml(
                    "Title", self._get_titles(), 4, -1, False)

        # phone numbers
        if self.get_phone_numbers().keys():
            strings.append("Phone")
            for type, number_list in sorted(
                    self.get_phone_numbers().items(),
                    key=lambda k: k[0].lower()):
                strings += helpers.convert_to_yaml(
                    type, number_list, 4, -1, False)

        # email addresses
        if self.get_email_addresses().keys():
            strings.append("E-Mail")
            for type, email_list in sorted(
                    self.get_email_addresses().items(),
                    key=lambda k: k[0].lower()):
                strings += helpers.convert_to_yaml(
                    type, email_list, 4, -1, False)

        # post addresses
        if self.get_post_addresses().keys():
            strings.append("Address")
            for type, post_adr_list in sorted(
                    self.get_formatted_post_addresses().items(),
                    key=lambda k: k[0].lower()):
                strings += helpers.convert_to_yaml(
                    type, post_adr_list, 4, -1, False)

        # private objects
        if self._get_private_objects().keys():
            strings.append("Private:")
            for object in self.supported_private_objects:
                if object in self._get_private_objects():
                    strings += helpers.convert_to_yaml(
                        object, self._get_private_objects().get(object), 4, -1,
                        False)

        # misc stuff
        if self._get_categories() or (show_uid and self.get_uid() != "") \
                or self._get_webpages() or self._get_notes():
            strings.append("Miscellaneous")
            if show_uid and self.get_uid():
                strings.append("    UID: {}".format(self.get_uid()))
            if self._get_categories():
                strings += helpers.convert_to_yaml(
                    "Categories", self._get_categories(), 4, -1, False)
            if self._get_webpages():
                strings += helpers.convert_to_yaml(
                    "Webpage", self._get_webpages(), 4, -1, False)
            if self._get_notes():
                strings += helpers.convert_to_yaml(
                    "Note", self._get_notes(), 4, -1, False)
        return '\n'.join(strings)

    def write_to_file(self, overwrite=False):
        # make sure, that every contact contains a uid
        if not self.get_uid():
            self.add_uid(helpers.get_random_uid())
        try:
            with atomic_write(self.filename, overwrite=overwrite) as f:
                f.write(self.vcard.serialize())
        except vobject.base.ValidateError as err:
            print("Error: Vcard is not valid.\n{}".format(err))
            sys.exit(4)
        except IOError as err:
            print("Error: Can't write\n{}".format(err))
            sys.exit(4)
        except OSError as err:
            print("Error: vcard with the file name {} already exists\n"
                  "{}".format(os.path.basename(self.filename), err))
            sys.exit(4)

    def delete_vcard_object(self, object_name):
        # first collect all vcard items, which should be removed
        to_be_removed = []
        for child in self.vcard.getChildren():
            if child.name == object_name:
                if child.group:
                    for label in self.vcard.getChildren():
                        if label.name == "X-ABLABEL" and \
                                label.group == child.group:
                            to_be_removed.append(label)
                to_be_removed.append(child)
        # then delete them one by one
        for item in to_be_removed:
            self.vcard.remove(item)

    def delete_vcard_file(self):
        if os.path.exists(self.filename):
            os.remove(self.filename)
        else:
            print("Error: Vcard file {} does not exist.".format(self.filename))
            sys.exit(4)

    #######################
    # static helper methods
    #######################

    def _get_types_for_vcard_object(self, object, default_type):
        """
        get list of types for phone number, email or post address
        :param object: vcard class object
        :type object: vobject.vCard
        :param default_type: use if the object contains no type
        :type default_type: str
        :returns: list of type labels
        :rtype: list(str)
        """
        type_list = []
        # try to find label group for custom value type
        if object.group:
            for label in self.vcard.getChildren():
                if label.name == "X-ABLABEL" and label.group == object.group:
                    custom_type = label.value.strip()
                    if custom_type:
                        type_list.append(custom_type)
        # then load type from params dict
        standard_types = object.params.get("TYPE")
        if standard_types is not None:
            if not isinstance(standard_types, list):
                standard_types = [standard_types]
            for type in standard_types:
                type = type.strip()
                if type and type.lower() != "pref":
                    if not type.lower().startswith("x-"):
                        type_list.append(type)
                    elif type[2:].lower() not in [x.lower() for x in type_list]:
                        # add x-custom type in case it's not already added by
                        # custom label for loop above but strip x- before
                        type_list.append(type[2:])
        # try to get pref parameter from vcard version 4.0
        try:
            type_list.append("pref=%d" % int(object.params.get("PREF")[0]))
        except (IndexError, TypeError, ValueError):
            # else try to determine, if type params contain pref attribute
            try:
                for x in object.params.get("TYPE"):
                    if x.lower() == "pref" and "pref" not in type_list:
                        type_list.append("pref")
            except TypeError:
                pass
        # return type_list or default type
        if type_list:
            return type_list
        return [default_type]

    @staticmethod
    def _parse_type_value(types, value, supported_types):
        """Parse type value of phone numbers, email and post addresses.

        :param types: list of type values
        :type types: list(str)
        :param value: the corresponding label, required for more verbose
            exceptions
        :type value: str
        :param supported_types: all allowed standard types
        :type supported_types: list(str)
        :returns: tuple of standard and custom types and pref integer
        :rtype: tuple(list(str), list(str), int)

        """
        custom_types = []
        standard_types = []
        pref = 0
        for type in types:
            type = type.strip()
            if type:
                if type.lower() in supported_types:
                    standard_types.append(type)
                elif type.lower() == "pref":
                    pref += 1
                elif re.match(r"^pref=\d{1,2}$", type.lower()):
                    pref += int(type.split("=")[1])
                else:
                    if type.lower().startswith("x-"):
                        custom_types.append(type[2:])
                        standard_types.append(type)
                    else:
                        custom_types.append(type)
                        standard_types.append("X-{}".format(type))
        return (standard_types, custom_types, pref)


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

import os
import random
import string
from datetime import datetime
from textwrap import dedent

from .object_type import ObjectType

def pretty_print(table, justify="L"):
    # support for multiline columns
    line_break_table = []
    for row in table:
        # get line break count
        most_line_breaks_in_row = 0
        for col in row:
            if str(col).count("\n") > most_line_breaks_in_row:
                most_line_breaks_in_row = col.count("\n")
        # fill table rows
        for index in range(0, most_line_breaks_in_row+1):
            line_break_row = []
            for col in row:
                try:
                    line_break_row.append(str(col).split("\n")[index])
                except IndexError:
                    line_break_row.append("")
            line_break_table.append(line_break_row)
    else:
        # replace table variable
        table = line_break_table
    # get width for every column
    column_widths = [0] * table[0].__len__()
    offset = 3
    for row in table:
        for index, col in enumerate(row):
            width = len(str(col))
            if width > column_widths[index]:
                column_widths[index] = width
    table_row_list = []
    for row in table:
        single_row_list = []
        for col_index, col in enumerate(row):
            if justify == "R":  # justify right
                formated_column = str(col).rjust(column_widths[col_index] +
                                                 offset)
            elif justify == "L":  # justify left
                formated_column = str(col).ljust(column_widths[col_index] +
                                                 offset)
            elif justify == "C":  # justify center
                formated_column = str(col).center(column_widths[col_index] +
                                                  offset)
            single_row_list.append(formated_column)
        table_row_list.append(' '.join(single_row_list))
    return '\n'.join(table_row_list)

def list_to_string(input, delimiter):
    """converts list to string recursively so that nested lists are supported

    :param input: a list of strings and lists of strings (and so on recursive)
    :type input: list
    :param delimiter: the deimiter to use when joining the items
    :type delimiter: str
    :returns: the recursively joined list
    :rtype: str
    """
    if isinstance(input, list):
        return delimiter.join(
            list_to_string(item, delimiter) for item in input)
    return input

def string_to_list(input, delimiter):
    if isinstance(input, list):
        return input
    return [x.strip() for x in input.split(delimiter)]

def string_to_date(input):
    """Convert string to date object.

    :param input: the date string to parse
    :type input: str
    :returns: the parsed datetime object
    :rtype: datetime.datetime
    """
    # try date formats --mmdd, --mm-dd, yyyymmdd, yyyy-mm-dd and datetime
    # formats yyyymmddThhmmss, yyyy-mm-ddThh:mm:ss, yyyymmddThhmmssZ,
    # yyyy-mm-ddThh:mm:ssZ.
    for format_string in ("--%m%d", "--%m-%d", "%Y%m%d", "%Y-%m-%d",
                          "%Y%m%dT%H%M%S", "%Y-%m-%dT%H:%M:%S",
                          "%Y%m%dT%H%M%SZ", "%Y-%m-%dT%H:%M:%SZ"):
        try:
            return datetime.strptime(input, format_string)
        except ValueError:
            pass
    # try datetime formats yyyymmddThhmmsstz and yyyy-mm-ddThh:mm:sstz where tz
    # may look like -06:00.
    for format_string in ("%Y%m%dT%H%M%S%z", "%Y-%m-%dT%H:%M:%S%z"):
        try:
            return datetime.strptime(''.join(input.rsplit(":", 1)),
                                     format_string)
        except ValueError:
            pass
    raise ValueError

def get_random_uid():
    return ''.join([random.choice(string.ascii_lowercase + string.digits)
                    for _ in range(36)])

def file_modification_date(filename):
    t = os.path.getmtime(filename)
    return datetime.fromtimestamp(t)

def convert_to_yaml(
        name, value, indentation, indexOfColon, show_multi_line_character):
    """converts a value list into yaml syntax
    :param name: name of object (example: phone)
    :type name: str
    :param value: object contents
    :type value: str, list(str), list(list(str))
    :param indentation: indent all by number of spaces
    :type indentation: int
    :param indexOfColon: use to position : at the name string (-1 for no space)
    :type indexOfColon: int
    :param show_multi_line_character: option to hide "|"
    :type show_multi_line_character: boolean
    :returns: yaml formatted string array of name, value pair
    :rtype: list(str)
    """
    strings = []
    if isinstance(value, list):
        # special case for single item lists:
        if len(value) == 1 \
                and isinstance(value[0], str):
            # value = ["string"] should not be converted to
            # name:
            #   - string
            # but to "name: string" instead
            value = value[0]
        elif len(value) == 1 \
                and isinstance(value[0], list) \
                and len(value[0]) == 1 \
                and isinstance(value[0][0], str):
            # same applies to value = [["string"]]
            value = value[0][0]
    if isinstance(value, str):
        strings.append("%s%s%s: %s" % (
            ' ' * indentation, name, ' ' * (indexOfColon-len(name)),
            indent_multiline_string(value, indentation+4,
                                    show_multi_line_character)))
    elif isinstance(value, list):
        strings.append("%s%s%s: " % (
            ' ' * indentation, name, ' ' * (indexOfColon-len(name))))
        for outer in value:
            # special case for single item sublists
            if isinstance(outer, list) \
                    and len(outer) == 1 \
                    and isinstance(outer[0], str):
                # outer = ["string"] should not be converted to
                # -
                #   - string
                # but to "- string" instead
                outer = outer[0]
            if isinstance(outer, str):
                strings.append("%s- %s" % (
                    ' ' * (indentation+4), indent_multiline_string(
                        outer, indentation+8, show_multi_line_character)))
            elif isinstance(outer, list):
                strings.append("%s- " % (' ' * (indentation+4)))
                for inner in outer:
                    if isinstance(inner, str):
                        strings.append("%s- %s" % (
                            ' ' * (indentation+8), indent_multiline_string(
                                inner, indentation+12,
                                show_multi_line_character)))
    return strings

def convert_to_vcard(name, value, allowed_object_type):
    """converts user input into vcard compatible data structures
    :param name: object name, only required for error messages
    :type name: str
    :param value: user input
    :type value: str or list(str)
    :param allowed_object_type: set the accepted return type for vcard
        attribute
    :type allowed_object_type: enum of type ObjectType
    :returns: cleaned user input, ready for vcard or a ValueError
    :rtype: str or list(str)
    """
    if isinstance(value, str):
        if allowed_object_type == ObjectType.list_with_strings:
            raise ValueError(
                "Error: " + name + " must not contain a single string.")
        else:
            return value.strip()
    elif isinstance(value, list):
        if allowed_object_type == ObjectType.string:
            raise ValueError(
                "Error: " + name + " must not contain a list.")
        else:
            for entry in value:
                if not isinstance(entry, str):
                    raise ValueError(
                        "Error: " + name + " must not contain a nested list")
            # filter out empty list items and strip leading and trailing space
            return [x.strip() for x in value if x]
    else:
        if allowed_object_type == ObjectType.string:
            raise ValueError(
                "Error: " + name + " must be a string.")
        elif allowed_object_type == ObjectType.list_with_strings:
            raise ValueError(
                "Error: " + name + " must be a list with strings.")
        else:
            raise ValueError(
                "Error: " + name + " must be a string or a list with strings.")

def indent_multiline_string(input, indentation, show_multi_line_character):
    # if input is a list, convert to string first
    if isinstance(input, list):
        input = list_to_string(input, "")
    # format multiline string
    if "\n" in input:
        lines = ["|"] if show_multi_line_character else [""]
        for line in input.split("\n"):
            lines.append("%s%s" % (' ' * indentation, line.strip()))
        return '\n'.join(lines)
    return input.strip()

def get_new_contact_template(supported_private_objects=[]):
    formatted_private_objects = []
    if supported_private_objects:
        formatted_private_objects.append("")
        longest_key = max(supported_private_objects, key=len)
        for object in supported_private_objects:
            formatted_private_objects += convert_to_yaml(
                object, "", 12, len(longest_key)+1, True)

    # create template
    return dedent("""
        # name components
        # every entry may contain a string or a list of strings
        # format:
        #   First name : name1
        #   Additional :
        #       - name2
        #       - name3
        #   Last name  : name4
        Prefix     :
        First name :
        Additional :
        Last name  :
        Suffix     :

        # nickname
        # may contain a string or a list of strings
        Nickname :

        # important dates
        # Formats:
        #   vcard 3.0 and 4.0: yyyy-mm-dd or yyyy-mm-ddTHH:MM:SS
        #   vcard 4.0 only: --mm-dd or text= string value
        # anniversary
        Anniversary :
        # birthday
        Birthday :

        # organisation
        # format:
        #   Organisation : company
        # or
        #   Organisation :
        #       - company1
        #       - company2
        # or
        #   Organisation :
        #       -
        #           - company
        #           - unit
        Organisation :

        # organisation title and role
        # every entry may contain a string or a list of strings
        #
        # title at organisation
        # example usage: research scientist
        Title :
        # role at organisation
        # example usage: project leader
        Role  :

        # phone numbers
        # format:
        #   Phone:
        #       type1, type2: number
        #       type3:
        #           - number1
        #           - number2
        #       custom: number
        # allowed types:
        #   vcard 3.0: At least one of bbs, car, cell, fax, home, isdn, msg, modem,
        #                              pager, pcs, pref, video, voice, work
        #   vcard 4.0: At least one of home, work, pref, text, voice, fax, cell, video,
        #                              pager, textphone
        #   Alternatively you may use a single custom label (only letters).
        #   But beware, that not all address book clients will support custom labels.
        Phone :
            cell :
            home :

        # email addresses
        # format like phone numbers above
        # allowed types:
        #   vcard 3.0: At least one of home, internet, pref, work, x400
        #   vcard 4.0: At least one of home, internet, pref, work
        #   Alternatively you may use a single custom label (only letters).
        Email :
            home :
            work :

        # post addresses
        # allowed types:
        #   vcard 3.0: At least one of dom, intl, home, parcel, postal, pref, work
        #   vcard 4.0: At least one of home, pref, work
        #   Alternatively you may use a single custom label (only letters).
        Address :
            home :
                Box      :
                Extended :
                Street   :
                Code     :
                City     :
                Region   :
                Country  :

        # categories or tags
        # format:
        #   Categories : single category
        # or
        #   Categories :
        #       - category1
        #       - category2
        Categories :

        # web pages
        # may contain a string or a list of strings
        Webpage :

        # private objects
        # define your own private objects in the vcard section of your khard config file
        # example:
        #   [vcard]
        #   private_objects = Jabber, Skype, Twitter
        # these objects are stored with a leading "X-" before the object name in the
        # vcard files.
        # every entry may contain a string or a list of strings
        Private :%s

        # notes
        # may contain a string or a list of strings
        # for multi-line notes use:
        #   Note : |
        #       line one
        #       line two
        Note :
        """ % '\n'.join(formatted_private_objects))


# -*- coding: utf-8 -*-
"""A simple class to load and manage the vcard files from disk."""

import abc
import glob
import logging
import os
import re
import sys

import vobject.base

from .carddav_object import CarddavObject

class AddressBookParseError(Exception):
    """Indicate an error while parsing data from an address book backend."""

    def __init__(self, filename, *args, **kwargs):
        """Store the filename that caused the error."""
        super().__init__(*args, **kwargs)
        self.filename = filename

class AddressBook(metaclass=abc.ABCMeta):
    """The base class of all address book implementations."""

    def __init__(self, name, private_objects=tuple(), localize_dates=True,
                 skip=False):
        """
        :param name: the name to identify the address book
        :type name: str
        :param private_objects: the names of private vCard extension fields to
            load
        :type private_objects: iterable(str)
        :param localize_dates: wheater to display dates in the local format
        :type localize_dates: bool
        :param skip: skip unparsable vCard files
        :type skip: bool
        """
        self._loaded = False
        self.contacts = {}
        self._short_uids = None
        self.name = name
        self._private_objects = private_objects
        self._localize_dates = localize_dates
        self._skip = skip

    def __str__(self):
        return self.name

    def __eq__(self, other):
        return isinstance(other, type(self)) and self.name == other.name

    def __ne__(self, other):
        return not self == other

    @staticmethod
    def _compare_uids(uid1, uid2):
        """Calculate the minimum length of initial substrings of uid1 and uid2
        for them to be different.

        :param uid1: first uid to compare
        :type uid1: str
        :param uid2: second uid to compare
        :type uid2: str
        :returns: the length of the shortes unequal initial substrings
        :rtype: int
        """
        sum = 0
        for char1, char2 in zip(uid1, uid2):
            if char1 == char2:
                sum += 1
            else:
                break
        return sum

    def _search_all(self, query):
        """Search in all fields for contacts matching query.

        :param query: the query to search for
        :type query: str
        :yields: all found contacts
        :rtype: generator(carddav_object.CarddavObject)

        """
        regexp = re.compile(query, re.IGNORECASE | re.DOTALL)
        for contact in self.contacts.values():
            # search in all contact fields
            contact_details = contact.print_vcard()
            if regexp.search(contact_details) is not None:
                yield contact
            else:
                # find phone numbers with special chars like /
                clean_contact_details = re.sub("[^a-zA-Z0-9\n]", "",
                                               contact_details)
                if regexp.search(clean_contact_details) is not None \
                        and len(re.sub("\D", "", query)) >= 3:
                    yield contact

    def _search_names(self, query):
        """Search in the name filed for contacts matching query.

        :param query: the query to search for
        :type query: str
        :yields: all found contacts
        :rtype: generator(carddav_object.CarddavObject)

        """
        regexp = re.compile(query, re.IGNORECASE | re.DOTALL)
        for contact in self.contacts.values():
            # only search in contact name
            if regexp.search(contact.get_full_name()) is not None:
                yield contact

    def _search_uid(self, query):
        """Search for contacts with a matching uid.

        :param query: the query to search for
        :type query: str
        :yields: all found contacts
        :rtype: generator(carddav_object.CarddavObject)

        """
        try:
            # First we treat the argument as a full UID and try to match it
            # exactly.
            yield self.contacts[query]
        except KeyError:
            # If that failed we look for all contacts whos UID start with the
            # given query.
            for uid in self.contacts:
                if uid.startswith(query):
                    yield self.contacts[uid]

    def search(self, query, method="all"):
        """Search this address book for contacts matching the query.

        The method can be one of "all", "name" and "uid".  The backend for this
        address book migth be load()ed if needed.

        :param query: the query to search for
        :type query: str
        :param method: the type of fileds to use when seaching
        :type method: str
        :returns: all found contacts
        :rtype: list(carddav_object.CarddavObject)

        """
        logging.debug('address book %s, searching with %s', self.name, query)
        if not self._loaded:
            self.load(query)
        if method == "all":
            search_function = self._search_all
        elif method == "name":
            search_function = self._search_names
        elif method == "uid":
            search_function = self._search_uid
        else:
            raise ValueError('Only the search methods "all", "name" and "uid" '
                             'are supported.')
        return list(search_function(query))

    def get_short_uid_dict(self, query=None):
        """Create a dictionary of shortend UIDs for all contacts.

        All arguments are only used if the address book is not yet initialized
        and will just be handed to self.load().

        :param query: see self.load()
        :type query: str
        :returns: the contacts mapped by the shortes unique prefix of their UID
        :rtype: dict(str: CarddavObject)
        """
        if self._short_uids is None:
            if not self._loaded:
                self.load(query)
            if not self.contacts:
                self._short_uids = {}
            elif len(self.contacts) == 1:
                self._short_uids = {uid[0:1]: contact
                                    for uid, contact in self.contacts.items()}
            else:
                self._short_uids = {}
                sorted_uids = sorted(self.contacts)
                # Prepare for the loop; the first and last items are handled
                # seperatly.
                item0, item1 = sorted_uids[:2]
                same1 = self._compare_uids(item0, item1)
                self._short_uids[item0[:same1 + 1]] = self.contacts[item0]
                for item_new in sorted_uids[2:]:
                    # shift the items and the common prefix lenght one further
                    item0, item1 = item1, item_new
                    same0, same1 = same1, self._compare_uids(item0, item1)
                    # compute the final prefix length for item1
                    same = max(same0, same1)
                    self._short_uids[item0[:same + 1]] = self.contacts[item0]
                # Save the last item.
                self._short_uids[item1[:same1 + 1]] = self.contacts[item1]
        return self._short_uids

    def get_short_uid(self, uid):
        """Get the shortend UID for the given UID.

        :param uid: the full UID to shorten
        :type uid: str
        :returns: the shortend uid or the empty string
        :rtype: str
        """
        if uid:
            short_uids = self.get_short_uid_dict()
            for length_of_uid in range(len(uid), 0, -1):
                if short_uids.get(uid[:length_of_uid]) is not None:
                    return uid[:length_of_uid]
        return ""

    @abc.abstractmethod
    def load(self, query=None):
        """Load the vCards from the backing store.

        If a query is given loading is limited to entries which match the
        query.  If the query is None all entries will be loaded.

        :param query: the query to limit loading to matching entries
        :type query: str
        :returns: the number of loaded contacts and the number of errors
        :rtype: (int, int)

        """
        pass

class VdirAddressBook(AddressBook):
    """An AddressBook implementation based on a vdir.

    This address book can load contacts from vcard files that reside in one
    direcotry on disk.
    """

    def __init__(self, name, path, **kwargs):
        """
        :param name: the name to identify the address book
        :type name: str
        :param path: the path to the backing structure on disk
        :type path: str
        :param **kwargs: further arguments for the parent constructor
        """
        self.path = os.path.expanduser(path)
        if not os.path.isdir(self.path):
            raise FileNotFoundError("[Errno 2] The path {} to the address book"
                                    " {} does not exist.".format(path, name))
        super().__init__(name, **kwargs)

    def _find_vcard_files(self, search=None, search_in_source_files=False):
        """Find all vcard files inside this address book.

        If a search string is given only files which contents match that will
        be returned.

        :param search: a regular expression to limit the results
        :type search: str
        :param search_in_source_files: apply search regexp directly on the .vcf files to speed up parsing (less accurate)
        :type search_in_source_files: bool
        :returns: the paths of the vcard files
        :rtype: generator

        """
        files = glob.glob(os.path.join(self.path, "*.vcf"))
        if search and search_in_source_files:
            for filename in files:
                with open(filename, "r") as filehandle:
                    if re.search(search, filehandle.read(),
                                 re.IGNORECASE | re.DOTALL):
                        yield filename
        else:
            yield from files

    def load(self, query=None, search_in_source_files=False):
        """Load all vcard files in this address book from disk.

        If a search string is given only files which contents match that will
        be loaded.

        :param query: a regular expression to limit the results
        :type query: str
        :param search_in_source_files: apply search regexp directly on the .vcf files to speed up parsing (less accurate)
        :type search_in_source_files: bool
        :returns: the number of successfully loaded cards and the number of
            errors
        :rtype: int, int
        :throws: AddressBookParseError
        """
        if self._loaded:
            return
        logging.debug('Loading Vdir %s with query %s', self.name, query)
        errors = 0
        for filename in self._find_vcard_files(
                search=query, search_in_source_files=search_in_source_files):
            try:
                card = CarddavObject.from_file(self, filename,
                                               self._private_objects,
                                               self._localize_dates)
            except (IOError, vobject.base.ParseError) as err:
                verb = "open" if isinstance(err, IOError) else "parse"
                logging.debug("Error: Could not %s file %s\n%s", verb,
                              filename, err)
                if self._skip:
                    errors += 1
                else:
                    # FIXME: This should throw an apropriate exception and the
                    # sys.exit should be called somewhere closer to the command
                    # line parsing.
                    logging.error(
                        "The vcard file %s of address book %s could not be "
                        "parsed\nUse --debug for more information or "
                        "--skip-unparsable to proceed", filename, self.name)
                    sys.exit(2)
            else:
                uid = card.get_uid()
                if not uid:
                    logging.warning("Card %s from address book %s has no UID "
                                    "and will not be availbale.", card,
                                    self.name)
                elif uid in self.contacts:
                    logging.warning(
                        "Card %s and %s from address book %s have the same "
                        "UID. The former will not be availbale.", card,
                        self.contacts[uid], self.name)
                else:
                    self.contacts[uid] = card
        self._loaded = True
        if errors:
            logging.warning(
                "%d of %d vCard files of address book %s could not be parsed.",
                errors, len(self.contacts) + errors, self)
        logging.debug('Loded %s contacts from address book %s.',
                      len(self.contacts), self.name)

class AddressBookCollection(AddressBook):
    """A collection of several address books.

    This represents a temporary merege of the contact collections provided by
    the underlying adress books.  On load all contacts from all subadressbooks
    are copied into a dict in this address book.  This allow this class to use
    all other methods from the parent AddressBook class.
    """

    def __init__(self, name, abooks, **kwargs):
        """
        :param name: the name to identify the address book
        :type name: str
        :param abooks: a list of address books to combine in this collection
        :type abooks: list(AddressBook)
        :param **kwargs: further arguments for the parent constructor
        """
        super().__init__(name, **kwargs)
        self._abooks = abooks

    def load(self, query=None):
        if self._loaded:
            return
        logging.debug('Loading collection %s with query %s', self.name, query)
        for abook in self._abooks:
            abook.load(query)
            for uid in abook.contacts:
                if uid in self.contacts:
                    logging.warning(
                        "Card %s from address book %s will not be availbale "
                        "because there is already another card with the same "
                        "UID: %s", abook.contacts[uid], abook, uid)
                else:
                    self.contacts[uid] = abook.contacts[uid]
        self._loaded = True
        logging.debug('Loded %s contacts from address book %s.',
                      len(self.contacts), self.name)

    def get_abook(self, name):
        """Get one of the backing abdress books by its name,

        :param name: the name of the address book to get
        :type name: str
        :returns: the matching address book or None
        :rtype: AddressBook or NoneType

        """
        for abook in self._abooks:
            if abook.name == name:
                return abook