#! /usr/bin/python
# -*- coding: utf-8 -*-
#
# bin/news-to-specfile-changelog
# Part of ComixCursors, a desktop cursor theme.
#
# Copyright © 2010–2013 Ben Finney <ben+opendesktop@benfinney.id.au>
#
# This work is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this work. If not, see <http://www.gnu.org/licenses/>.

""" Render the NEWS file to a specfile changelog field value.

    The project NEWS file is a reStructuredText document, with each
    section describing a version of the project. The document is
    intended to be readable as-is by end users.

    This program transforms the document to a compact list of changes,
    suitable for the “changelog” field of an RPM specfile.

    Requires:

    * Docutils <http://docutils.sourceforge.net/>

    """

import datetime
import textwrap

from docutils.core import publish_cmdline, default_description
import docutils.nodes
import docutils.writers


class SpecChangelogWriter(docutils.writers.Writer):
    """ Docutils writer to produce a changelog field for RPM spec files.
        """

    supported = ('spec_changelog')
    """ Formats this writer supports. """

    def __init__(self):
        docutils.writers.Writer.__init__(self)
        self.translator_class = SpecChangelogTranslator

    def translate(self):
        visitor = self.translator_class(self.document)
        self.document.walkabout(visitor)
        self.output = visitor.astext()


class NewsEntry(object):
    """ An individual entry from the NEWS document. """

    def __init__(
            self, released=None, version=None, maintainer=None, body=None):
        self.released = released
        self.version = version
        self.maintainer = maintainer
        self.body = body

    def as_specfile_changelog_entry(self):
        """ Format the news entry as an RPM specfile changelog entry. """
        # Reference: <URL:http://fedoraproject.org/wiki/PackagingGuidelines>

        released_timestamp_text = self.released.strftime("%a %b %d %Y")
        header = " ".join([
            "*", released_timestamp_text, self.maintainer, "-", self.version])
        text = "\n".join([header, self.body])
        return text



def get_name_for_field_body(node):
    """ Return the text of the field name of a field body node. """
    field_node = node.parent
    field_name_node_index = field_node.first_child_matching_class(
        docutils.nodes.field_name)
    field_name_node = field_node.children[field_name_node_index]
    field_name = unicode(field_name_node.children[0])
    return field_name


class InvalidFormatError(ValueError):
    """ Raised when the document is not a valid NEWS document. """


def news_timestamp_to_datetime(text):
    """ Return a datetime value from the news entry timestamp. """
    if text == "FUTURE":
        timestamp = datetime.datetime.max
    else:
        timestamp = datetime.datetime.strptime(text, "%Y-%m-%d")
    return timestamp


class SpecChangelogTranslator(docutils.nodes.SparseNodeVisitor):
    """ Translator from document nodes to a changelog for RPM spec files. """

    wrap_width = 78

    field_convert_funcs = {
        'released': news_timestamp_to_datetime,
        'maintainer': str,
        }

    def __init__(self, document):
        docutils.nodes.NodeVisitor.__init__(self, document)
        self.settings = document.settings
        self.current_field_name = None
        self.body = u""
        self.indent_width = 0
        self.initial_indent = u""
        self.subsequent_indent = u""
        self.current_entry = None

    def astext(self):
        """ Return the translated document as text. """
        return self.body

    def append_to_current_entry(self, text):
        if self.current_entry is not None:
            if self.current_entry.body is not None:
                self.current_entry.body += text

    def visit_Text(self, node):
        raw_text = node.astext()
        text = textwrap.fill(
            raw_text,
            width=self.wrap_width,
            initial_indent=self.initial_indent,
            subsequent_indent=self.subsequent_indent)
        self.append_to_current_entry(text)

    def depart_Text(self, node):
        pass

    def visit_comment(self, node):
        raise docutils.nodes.SkipNode

    def depart_comment(self, node):
        pass

    def visit_field_body(self, node):
        convert_func = self.field_convert_funcs[self.current_field_name]
        attr_name = self.current_field_name
        attr_value = convert_func(node.astext())
        setattr(self.current_entry, attr_name, attr_value)
        raise docutils.nodes.SkipNode

    def depart_field_body(self, node):
        pass

    def visit_field_list(self, node):
        section_node = node.parent
        if not isinstance(section_node, docutils.nodes.section):
            raise InvalidFormatError(
                "Unexpected field list within " + repr(section_node))

    def depart_field_list(self, node):
        self.current_field_name = None
        if self.current_entry is not None:
            self.current_entry.body = u""

    def visit_field_name(self, node):
        field_name = node.astext()
        if field_name.lower() not in ("released", "maintainer"):
            raise InvalidFormatError(
                "Unexpected field name %(field_name)s" % vars())
        self.current_field_name = field_name.lower()
        raise docutils.nodes.SkipNode

    def depart_field_name(self, node):
        pass

    def adjust_indent_width(self, delta):
        self.indent_width += delta
        self.subsequent_indent = u" " * self.indent_width
        self.initial_indent = self.subsequent_indent

    def visit_list_item(self, node):
        self.adjust_indent_width(+2)
        self.initial_indent = self.subsequent_indent[:-2]
        self.append_to_current_entry(self.initial_indent + "- ")

    def depart_list_item(self, node):
        self.adjust_indent_width(-2)
        self.append_to_current_entry("\n")

    def visit_section(self, node):
        self.current_entry = NewsEntry()

    def depart_section(self, node):
        self.body += self.current_entry.as_specfile_changelog_entry() + "\n"
        self.current_entry = None

    def visit_title(self, node):
        title_text = node.astext()
        words = title_text.split(" ")
        self.current_entry.version = words[-1]

    def depart_title(self, node):
        pass


description = (
    u"Render the NEWS file to a specfile changelog field value."
    + u" " + default_description)
publish_cmdline(writer=SpecChangelogWriter(), description=description)
