# -*- coding: utf-8 -*-
"""
    werkzeug.contrib.atom
    ~~~~~~~~~~~~~~~~~~~~~

    This module provides a class called :class:`AtomFeed` which can be
    used to generate feeds in the Atom syndication format (see :rfc:`4287`).

    Example::

        def atom_feed(request):
            feed = AtomFeed("My Blog", feed_url=request.url,
                            url=request.host_url,
                            subtitle="My example blog for a feed test.")
            for post in Post.query.limit(10).all():
                feed.add(post.title, post.body, content_type='html',
                         author=post.author, url=post.url, id=post.uid,
                         updated=post.last_update, published=post.pub_date)
            return feed.get_response()

    :copyright: 2007 Pallets
    :license: BSD-3-Clause
"""
import warnings
from datetime import datetime

from .._compat import implements_to_string
from .._compat import string_types
from ..utils import escape
from ..wrappers import BaseResponse

warnings.warn(
    "'werkzeug.contrib.atom' is deprecated as of version 0.15 and will"
    " be removed in version 1.0.",
    DeprecationWarning,
    stacklevel=2,
)

XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"


def _make_text_block(name, content, content_type=None):
    """Helper function for the builder that creates an XML text block."""
    if content_type == "xhtml":
        return u'<%s type="xhtml"><div xmlns="%s">%s</div></%s>\n' % (
            name,
            XHTML_NAMESPACE,
            content,
            name,
        )
    if not content_type:
        return u"<%s>%s</%s>\n" % (name, escape(content), name)
    return u'<%s type="%s">%s</%s>\n' % (name, content_type, escape(content), name)


def format_iso8601(obj):
    """Format a datetime object for iso8601"""
    iso8601 = obj.isoformat()
    if obj.tzinfo:
        return iso8601
    return iso8601 + "Z"


@implements_to_string
class AtomFeed(object):

    """A helper class that creates Atom feeds.

    :param title: the title of the feed. Required.
    :param title_type: the type attribute for the title element.  One of
                       ``'html'``, ``'text'`` or ``'xhtml'``.
    :param url: the url for the feed (not the url *of* the feed)
    :param id: a globally unique id for the feed.  Must be an URI.  If
               not present the `feed_url` is used, but one of both is
               required.
    :param updated: the time the feed was modified the last time.  Must
                    be a :class:`datetime.datetime` object.  If not
                    present the latest entry's `updated` is used.
                    Treated as UTC if naive datetime.
    :param feed_url: the URL to the feed.  Should be the URL that was
                     requested.
    :param author: the author of the feed.  Must be either a string (the
                   name) or a dict with name (required) and uri or
                   email (both optional).  Can be a list of (may be
                   mixed, too) strings and dicts, too, if there are
                   multiple authors. Required if not every entry has an
                   author element.
    :param icon: an icon for the feed.
    :param logo: a logo for the feed.
    :param rights: copyright information for the feed.
    :param rights_type: the type attribute for the rights element.  One of
                        ``'html'``, ``'text'`` or ``'xhtml'``.  Default is
                        ``'text'``.
    :param subtitle: a short description of the feed.
    :param subtitle_type: the type attribute for the subtitle element.
                          One of ``'text'``, ``'html'``, ``'text'``
                          or ``'xhtml'``.  Default is ``'text'``.
    :param links: additional links.  Must be a list of dictionaries with
                  href (required) and rel, type, hreflang, title, length
                  (all optional)
    :param generator: the software that generated this feed.  This must be
                      a tuple in the form ``(name, url, version)``.  If
                      you don't want to specify one of them, set the item
                      to `None`.
    :param entries: a list with the entries for the feed. Entries can also
                    be added later with :meth:`add`.

    For more information on the elements see
    http://www.atomenabled.org/developers/syndication/

    Everywhere where a list is demanded, any iterable can be used.
    """

    default_generator = ("Werkzeug", None, None)

    def __init__(self, title=None, entries=None, **kwargs):
        self.title = title
        self.title_type = kwargs.get("title_type", "text")
        self.url = kwargs.get("url")
        self.feed_url = kwargs.get("feed_url", self.url)
        self.id = kwargs.get("id", self.feed_url)
        self.updated = kwargs.get("updated")
        self.author = kwargs.get("author", ())
        self.icon = kwargs.get("icon")
        self.logo = kwargs.get("logo")
        self.rights = kwargs.get("rights")
        self.rights_type = kwargs.get("rights_type")
        self.subtitle = kwargs.get("subtitle")
        self.subtitle_type = kwargs.get("subtitle_type", "text")
        self.generator = kwargs.get("generator")
        if self.generator is None:
            self.generator = self.default_generator
        self.links = kwargs.get("links", [])
        self.entries = list(entries) if entries else []

        if not hasattr(self.author, "__iter__") or isinstance(
            self.author, string_types + (dict,)
        ):
            self.author = [self.author]
        for i, author in enumerate(self.author):
            if not isinstance(author, dict):
                self.author[i] = {"name": author}

        if not self.title:
            raise ValueError("title is required")
        if not self.id:
            raise ValueError("id is required")
        for author in self.author:
            if "name" not in author:
                raise TypeError("author must contain at least a name")

    def add(self, *args, **kwargs):
        """Add a new entry to the feed.  This function can either be called
        with a :class:`FeedEntry` or some keyword and positional arguments
        that are forwarded to the :class:`FeedEntry` constructor.
        """
        if len(args) == 1 and not kwargs and isinstance(args[0], FeedEntry):
            self.entries.append(args[0])
        else:
            kwargs["feed_url"] = self.feed_url
            self.entries.append(FeedEntry(*args, **kwargs))

    def __repr__(self):
        return "<%s %r (%d entries)>" % (
            self.__class__.__name__,
            self.title,
            len(self.entries),
        )

    def generate(self):
        """Return a generator that yields pieces of XML."""
        # atom demands either an author element in every entry or a global one
        if not self.author:
            if any(not e.author for e in self.entries):
                self.author = ({"name": "Unknown author"},)

        if not self.updated:
            dates = sorted([entry.updated for entry in self.entries])
            self.updated = dates[-1] if dates else datetime.utcnow()

        yield u'<?xml version="1.0" encoding="utf-8"?>\n'
        yield u'<feed xmlns="http://www.w3.org/2005/Atom">\n'
        yield "  " + _make_text_block("title", self.title, self.title_type)
        yield u"  <id>%s</id>\n" % escape(self.id)
        yield u"  <updated>%s</updated>\n" % format_iso8601(self.updated)
        if self.url:
            yield u'  <link href="%s" />\n' % escape(self.url)
        if self.feed_url:
            yield u'  <link href="%s" rel="self" />\n' % escape(self.feed_url)
        for link in self.links:
            yield u"  <link %s/>\n" % "".join(
                '%s="%s" ' % (k, escape(link[k])) for k in link
            )
        for author in self.author:
            yield u"  <author>\n"
            yield u"    <name>%s</name>\n" % escape(author["name"])
            if "uri" in author:
                yield u"    <uri>%s</uri>\n" % escape(author["uri"])
            if "email" in author:
                yield "    <email>%s</email>\n" % escape(author["email"])
            yield "  </author>\n"
        if self.subtitle:
            yield "  " + _make_text_block("subtitle", self.subtitle, self.subtitle_type)
        if self.icon:
            yield u"  <icon>%s</icon>\n" % escape(self.icon)
        if self.logo:
            yield u"  <logo>%s</logo>\n" % escape(self.logo)
        if self.rights:
            yield "  " + _make_text_block("rights", self.rights, self.rights_type)
        generator_name, generator_url, generator_version = self.generator
        if generator_name or generator_url or generator_version:
            tmp = [u"  <generator"]
            if generator_url:
                tmp.append(u' uri="%s"' % escape(generator_url))
            if generator_version:
                tmp.append(u' version="%s"' % escape(generator_version))
            tmp.append(u">%s</generator>\n" % escape(generator_name))
            yield u"".join(tmp)
        for entry in self.entries:
            for line in entry.generate():
                yield u"  " + line
        yield u"</feed>\n"

    def to_string(self):
        """Convert the feed into a string."""
        return u"".join(self.generate())

    def get_response(self):
        """Return a response object for the feed."""
        return BaseResponse(self.to_string(), mimetype="application/atom+xml")

    def __call__(self, environ, start_response):
        """Use the class as WSGI response object."""
        return self.get_response()(environ, start_response)

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


@implements_to_string
class FeedEntry(object):

    """Represents a single entry in a feed.

    :param title: the title of the entry. Required.
    :param title_type: the type attribute for the title element.  One of
                       ``'html'``, ``'text'`` or ``'xhtml'``.
    :param content: the content of the entry.
    :param content_type: the type attribute for the content element.  One
                         of ``'html'``, ``'text'`` or ``'xhtml'``.
    :param summary: a summary of the entry's content.
    :param summary_type: the type attribute for the summary element.  One
                         of ``'html'``, ``'text'`` or ``'xhtml'``.
    :param url: the url for the entry.
    :param id: a globally unique id for the entry.  Must be an URI.  If
               not present the URL is used, but one of both is required.
    :param updated: the time the entry was modified the last time.  Must
                    be a :class:`datetime.datetime` object.  Treated as
                    UTC if naive datetime. Required.
    :param author: the author of the entry.  Must be either a string (the
                   name) or a dict with name (required) and uri or
                   email (both optional).  Can be a list of (may be
                   mixed, too) strings and dicts, too, if there are
                   multiple authors. Required if the feed does not have an
                   author element.
    :param published: the time the entry was initially published.  Must
                      be a :class:`datetime.datetime` object.  Treated as
                      UTC if naive datetime.
    :param rights: copyright information for the entry.
    :param rights_type: the type attribute for the rights element.  One of
                        ``'html'``, ``'text'`` or ``'xhtml'``.  Default is
                        ``'text'``.
    :param links: additional links.  Must be a list of dictionaries with
                  href (required) and rel, type, hreflang, title, length
                  (all optional)
    :param categories: categories for the entry. Must be a list of dictionaries
                       with term (required), scheme and label (all optional)
    :param xml_base: The xml base (url) for this feed item.  If not provided
                     it will default to the item url.

    For more information on the elements see
    http://www.atomenabled.org/developers/syndication/

    Everywhere where a list is demanded, any iterable can be used.
    """

    def __init__(self, title=None, content=None, feed_url=None, **kwargs):
        self.title = title
        self.title_type = kwargs.get("title_type", "text")
        self.content = content
        self.content_type = kwargs.get("content_type", "html")
        self.url = kwargs.get("url")
        self.id = kwargs.get("id", self.url)
        self.updated = kwargs.get("updated")
        self.summary = kwargs.get("summary")
        self.summary_type = kwargs.get("summary_type", "html")
        self.author = kwargs.get("author", ())
        self.published = kwargs.get("published")
        self.rights = kwargs.get("rights")
        self.links = kwargs.get("links", [])
        self.categories = kwargs.get("categories", [])
        self.xml_base = kwargs.get("xml_base", feed_url)

        if not hasattr(self.author, "__iter__") or isinstance(
            self.author, string_types + (dict,)
        ):
            self.author = [self.author]
        for i, author in enumerate(self.author):
            if not isinstance(author, dict):
                self.author[i] = {"name": author}

        if not self.title:
            raise ValueError("title is required")
        if not self.id:
            raise ValueError("id is required")
        if not self.updated:
            raise ValueError("updated is required")

    def __repr__(self):
        return "<%s %r>" % (self.__class__.__name__, self.title)

    def generate(self):
        """Yields pieces of ATOM XML."""
        base = ""
        if self.xml_base:
            base = ' xml:base="%s"' % escape(self.xml_base)
        yield u"<entry%s>\n" % base
        yield u"  " + _make_text_block("title", self.title, self.title_type)
        yield u"  <id>%s</id>\n" % escape(self.id)
        yield u"  <updated>%s</updated>\n" % format_iso8601(self.updated)
        if self.published:
            yield u"  <published>%s</published>\n" % format_iso8601(self.published)
        if self.url:
            yield u'  <link href="%s" />\n' % escape(self.url)
        for author in self.author:
            yield u"  <author>\n"
            yield u"    <name>%s</name>\n" % escape(author["name"])
            if "uri" in author:
                yield u"    <uri>%s</uri>\n" % escape(author["uri"])
            if "email" in author:
                yield u"    <email>%s</email>\n" % escape(author["email"])
            yield u"  </author>\n"
        for link in self.links:
            yield u"  <link %s/>\n" % "".join(
                '%s="%s" ' % (k, escape(link[k])) for k in link
            )
        for category in self.categories:
            yield u"  <category %s/>\n" % "".join(
                '%s="%s" ' % (k, escape(category[k])) for k in category
            )
        if self.summary:
            yield u"  " + _make_text_block("summary", self.summary, self.summary_type)
        if self.content:
            yield u"  " + _make_text_block("content", self.content, self.content_type)
        yield u"</entry>\n"

    def to_string(self):
        """Convert the feed item into a unicode object."""
        return u"".join(self.generate())

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