# -*- coding: iso-8859-1 -*-
#
# Copyright (C) 2005 Edgewall Software
# Copyright (C) 2005,2006 Lele Gaifax <lele@metapensiero.it>
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.com/license.html.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://projects.edgewall.com/trac/.
#
# Author: Lele Gaifax <lele@metapensiero.it>

from time import mktime, timezone

from xml.sax import parseString, SAXException
from xml.sax.handler import ContentHandler

from trac.util import TracError
from trac.versioncontrol import Changeset
from trac.versioncontrol.cache import CachedChangeset

from tracdarcs.node import Node, DarcsNode
from tracdarcs.repos import reversed

class DarcsChangeset(Changeset):
    """
    Represents a set of changes of a repository.
    """

    def __init__(self, rev, patchname, message, author, date, changes, hash):
        if message:
            log = patchname + '\n' + message
        else:
            log = patchname
        Changeset.__init__(self, rev, log, author, date)
        self.patchname = patchname
        self.changes = changes
        self.hash = hash

    def get_changes(self):
        """
        Generator that produces a (path, kind, change, base_path, base_rev)
        """

        moves = {}
        for c in self.changes:
            yield (c.path, c.kind, c.change, c.ancestor_path, c.ancestor_rev)

    def get_node(self, path, maybedir=False):
        """
        Find and return the node relative to given path.
        """

        dpath = path + '/'
        for c in self.changes:
            if c.path == path or c.ancestor_path == path:
                return c
            if maybedir and not path.endswith('/'):
                if c.path == dpath or c.ancestor_path == dpath:
                    return c

    def _maybeRenamedFrom(self, name):
        """
        Go thru all changes and determine the original name of a file.

        This is different from the given name only when a parent
        directory is renamed by this changeset.
        """

        for c in self.changes:
            if (c.change == Changeset.MOVE
                and c.kind == Node.DIRECTORY
                and name.startswith(c.path)):
                name = c.ancestor_path + name[len(c.path):]
        return name

def changesets_from_darcschanges(changes, repository, start_revision,
                                 force_encoding=None):
    """
    Parse XML output of ``darcs changes``.

    Return a list of ``Changeset`` instances.
    """

    class DarcsXMLChangesHandler(ContentHandler):
        def __init__(self):
            self.changesets = []
            self.index = start_revision-1
            self.current = None
            self.current_field = []

        def startElement(self, name, attributes):
            if name == 'patch':
                self.current = {}
                self.current['author'] = attributes['author']
                date = attributes['date']
                # 20040619130027
                y = int(date[:4])
                m = int(date[4:6])
                d = int(date[6:8])
                hh = int(date[8:10])
                mm = int(date[10:12])
                ss = int(date[12:14])
                unixtime = int(mktime((y, m, d, hh, mm, ss, 0, 0, 0)))-timezone
                self.current['date'] = unixtime
                self.current['comment'] = ''
                self.current['hash'] = attributes['hash']
                self.current['entries'] = []
            elif name in ['name', 'comment', 'add_file', 'add_directory',
                          'remove_directory', 'modify_file', 'remove_file']:
                self.current_field = []
            elif name == 'move':
                self.old_name = attributes['from']
                self.new_name = attributes['to']

        def getAncestor(self, name, maybedir=False):
            prevnode = None
            for cs in reversed(self.changesets):
                prevnode = cs.get_node(name, maybedir=maybedir)
                if prevnode:
                    break
                else:
                    # If the changeset doesn't know about it, maybe
                    # it renamed one of its parent directories.
                    name = cs._maybeRenamedFrom(name)
            if prevnode is None:
                prevnode = repository.get_node(name)
            return prevnode

        def endElement(self, name):
            if name == 'patch':
                # Sort the entries: this is needed because darcs does not
                # always print them in the right order, and sometime a
                # "MOVE something" preceeds an "ADD something" in the same
                # patch :-| This is the reason we have to collect all the
                # entries of a patch first before building concrete nodes.
                self.current['entries'].sort()
                self.index += 1

                # Build the concrete nodes, retrieving also the ancestor of
                # each one.
                entries = []
                for order, change, kind, path, old in self.current['entries']:
                    if change <> Changeset.ADD:
                        ancestor = None
                        if change == Changeset.MOVE:
                            p = old
                        else:
                            p = path
                        # First look in the current patch changes
                        for a in entries:
                            if a.path == p:
                                ancestor = a
                                break
                            elif kind <> Node.FILE and a.path == p+'/':
                                ancestor = a
                                break
                        if ancestor is None:
                            ancestor = self.getAncestor(p, True)
                        apath, arev, akind = ancestor.path, ancestor.rev, \
                                             ancestor.kind
                        if kind is None:
                            kind = akind
                            if kind is None:
                                kind = Node.FILE
                            elif kind == Node.DIRECTORY:
                                path += '/'
                    else:
                        apath = None
                        arev = -1
                    entries.append(DarcsNode(path, self.index, kind, change,
                                             repository, apath, arev))

                cset = DarcsChangeset(self.index,
                                      self.current['name'],
                                      self.current['comment'],
                                      self.current['author'],
                                      self.current['date'],
                                      entries,
                                      self.current['hash'])
                self.changesets.append(cset)
                self.current = None
            elif name in ['name', 'comment']:
                self.current[name] = ''.join(self.current_field)
            elif name == 'move':
                self.current['entries'].append((1, Changeset.MOVE, None,
                                                self.new_name, self.old_name))
            elif name in ['add_file', 'add_directory', 'modify_file',
                          'remove_file', 'remove_directory']:
                path = ''.join(self.current_field).strip()
                change = { 'add_file': Changeset.ADD,
                           'add_directory': Changeset.ADD,
                           'modify_file': Changeset.EDIT,
                           'remove_file': Changeset.DELETE,
                           'remove_directory': Changeset.DELETE
                         }[name]
                isdir = name in ('add_directory', 'remove_directory')
                kind = isdir and Node.DIRECTORY or Node.FILE
                # Eventually add one final '/' to identify directories.
                # This is because Trac brings around simple tuples at times,
                # that cannot carry that flag with them.
                if isdir:
                    path += '/'
                if name == 'modify_file':
                    order = 2
                else:
                    order = 0
                self.current['entries'].append((order, change, kind, path, None))

        def characters(self, data):
            self.current_field.append(data)

    if force_encoding:
        changes = changes.decode(force_encoding, 'replace').encode('utf-8')
        changes = '<?xml version="1.0" encoding="utf-8"?>\n' + changes

    handler = DarcsXMLChangesHandler()
    try:
        parseString(changes, handler)
    except SAXException, le:
        raise TracError('Unable to parse "darcs changes" output: ' + str(le))

    return handler.changesets
