# Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.

# This program 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 Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Handling of Subversion properties."""

__author__ = "Jelmer Vernooij <jelmer@samba.org>"
__docformat__ = "restructuredText"

import bisect, calendar, time, urlparse


class InvalidExternalsDescription(Exception):
    _fmt = """Unable to parse externals description."""


def is_valid_property_name(prop):
    """Check the validity of a property name.

    :param prop: Property name
    :return: Whether prop is a valid property name
    """
    if not prop[0].isalnum() and not prop[0] in ":_":
        return False
    for c in prop[1:]:
        if not c.isalnum() and not c in "-:._":
            return False
    return True


def time_to_cstring(timestamp):
    """Determine string representation of a time.

    :param timestamp: Timestamp
    :return: string with date
    """
    tm_usec = timestamp % 1000000
    (tm_year, tm_mon, tm_mday, tm_hour, tm_min, 
            tm_sec, tm_wday, tm_yday, tm_isdst) = time.gmtime(timestamp / 1000000)
    return "%04d-%02d-%02dT%02d:%02d:%02d.%06dZ" % (tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, tm_usec)


def time_from_cstring(text):
    """Parse a time from a cstring.
    
    :param text: Parse text
    :return: timestamp
    """
    (basestr, usecstr) = text.split(".", 1)
    assert usecstr[-1] == "Z"
    tm_usec = int(usecstr[:-1])
    tm = time.strptime(basestr, "%Y-%m-%dT%H:%M:%S")
    return (long(calendar.timegm(tm)) * 1000000 + tm_usec)


def parse_externals_description(base_url, val):
    """Parse an svn:externals property value.

    :param base_url: URL on which the property is set. Used for 
        relative externals.

    :returns: dictionary with local names as keys, (revnum, url)
              as value. revnum is the revision number and is 
              set to None if not applicable.
    """
    def is_url(u):
        return ("://" in u)
    ret = {}
    for l in val.splitlines():
        if l == "" or l[0] == "#":
            continue
        pts = l.rsplit(None, 3) 
        if len(pts) == 4:
            if pts[0] == "-r": # -r X URL DIR
                revno = int(pts[1])
                path = pts[3]
                relurl = pts[2]
            elif pts[1] == "-r": # DIR -r X URL
                revno = int(pts[2])
                path = pts[0]
                relurl = pts[3]
            else:
                raise InvalidExternalsDescription()
        elif len(pts) == 3:
            if pts[1].startswith("-r"): # DIR -rX URL
                revno = int(pts[1][2:])
                path = pts[0]
                relurl = pts[2]
            elif pts[0].startswith("-r"): # -rX URL DIR
                revno = int(pts[0][2:])
                path = pts[2]
                relurl = pts[1]
            else:
                raise InvalidExternalsDescription()
        elif len(pts) == 2:
            if not is_url(pts[0]):
                relurl = pts[1]
                path = pts[0]
            else:
                relurl = pts[0]
                path = pts[1]
            revno = None
        else:
            raise InvalidExternalsDescription()
        if relurl.startswith("//"):
            raise NotImplementedError("Relative to the scheme externals not yet supported")
        if relurl.startswith("^/"):
            raise NotImplementedError("Relative to the repository root externals not yet supported")
        ret[path] = (revno, urlparse.urljoin(base_url+"/", relurl))
    return ret


def parse_mergeinfo_property(text):
    """Parse a mergeinfo property.

    :param text: Property contents
    """
    ret = {}
    for l in text.splitlines():
        (path, ranges) = l.rsplit(":", 1)
        assert path.startswith("/")
        ret[path] = []
        for range in ranges.split(","):
            if range[-1] == "*":
                inheritable = False
                range = range[:-1]
            else:
                inheritable = True
            try:
                (start, end) = range.split("-", 1)
                ret[path].append((int(start), int(end), inheritable))
            except ValueError:
                ret[path].append((int(range), int(range), inheritable))

    return ret


def generate_mergeinfo_property(merges):
    """Generate the contents of the svn:mergeinfo property

    :param merges: dictionary mapping paths to lists of ranges
    :return: Property contents
    """
    def formatrange((start, end, inheritable)):
        suffix = ""
        if not inheritable:
            suffix = "*"
        if start == end:
            return "%d%s" % (start, suffix)
        else:
            return "%d-%d%s" % (start, end, suffix)
    text = ""
    for (path, ranges) in merges.iteritems():
        assert path.startswith("/")
        text += "%s:%s\n" % (path, ",".join(map(formatrange, ranges)))
    return text


def range_includes_revnum(ranges, revnum):
    """Check if the specified range contains the mentioned revision number.

    :param ranges: list of ranges
    :param revnum: revision number
    :return: Whether or not the revision number is included
    """
    i = bisect.bisect(ranges, (revnum, revnum, True))
    if i == 0:
        return False
    (start, end, inheritable) = ranges[i-1]
    return (start <= revnum <= end)


def range_add_revnum(ranges, revnum, inheritable=True):
    """Add revision number to a list of ranges

    :param ranges: List of ranges
    :param revnum: Revision number to add
    :param inheritable: TODO
    :return: New list of ranges
    """
    # TODO: Deal with inheritable
    item = (revnum, revnum, inheritable)
    if len(ranges) == 0:
        ranges.append(item)
        return ranges
    i = bisect.bisect(ranges, item)
    if i > 0:
        (start, end, inh) = ranges[i-1]
        if (start <= revnum <= end):
            # already there
            return ranges
        if end == revnum-1:
            # Extend previous range
            ranges[i-1] = (start, end+1, inh)
            return ranges
    if i < len(ranges):
        (start, end, inh) = ranges[i]
        if start-1 == revnum:
            # Extend next range
            ranges[i] = (start-1, end, inh)
            return ranges
    ranges.insert(i, item)
    return ranges


def mergeinfo_includes_revision(merges, path, revnum):
    """Check if the specified mergeinfo contains a path in revnum.

    :param merges: Dictionary with merges
    :param path: Merged path
    :param revnum: Revision number
    :return: Whether the revision is included
    """
    assert path.startswith("/")
    try:
        ranges = merges[path]
    except KeyError:
        return False

    return range_includes_revnum(ranges, revnum)


def mergeinfo_add_revision(mergeinfo, path, revnum):
    """Add a revision to a mergeinfo dictionary

    :param mergeinfo: Merginfo dictionary
    :param path: Merged path to add
    :param revnum: Merged revision to add
    :return: Updated dictionary
    """
    assert path.startswith("/")
    mergeinfo[path] = range_add_revnum(mergeinfo.get(path, []), revnum)
    return mergeinfo


PROP_EXECUTABLE = 'svn:executable'
PROP_EXECUTABLE_VALUE = '*'
PROP_EXTERNALS = 'svn:externals'
PROP_IGNORE = 'svn:ignore'
PROP_KEYWORDS = 'svn:keywords'
PROP_MIME_TYPE = 'svn:mime-type'
PROP_MERGEINFO = 'svn:mergeinfo'
PROP_NEEDS_LOCK = 'svn:needs-lock'
PROP_NEEDS_LOCK_VALUE = '*'
PROP_PREFIX = 'svn:'
PROP_SPECIAL = 'svn:special'
PROP_SPECIAL_VALUE = '*'
PROP_WC_PREFIX = 'svn:wc:'
PROP_ENTRY_PREFIX = 'svn:entry'
PROP_ENTRY_COMMITTED_DATE = 'svn:entry:committed-date'
PROP_ENTRY_COMMITTED_REV = 'svn:entry:committed-rev'
PROP_ENTRY_LAST_AUTHOR = 'svn:entry:last-author'
PROP_ENTRY_LOCK_TOKEN = 'svn:entry:lock-token'
PROP_ENTRY_UUID = 'svn:entry:uuid'

PROP_REVISION_LOG = "svn:log"
PROP_REVISION_AUTHOR = "svn:author"
PROP_REVISION_DATE = "svn:date"
PROP_REVISION_ORIGINAL_DATE = "svn:original-date"

def diff(current, previous):
    """Find the differences between two property dictionaries.

    :param current: Dictionary with current (new) properties
    :param previous: Dictionary with previous (old) properties
    :return: Dictionary that contains an entry for 
             each property that was changed. Value is a tuple 
             with the old and the new property value.
    """
    ret = {}
    for key, newval in current.iteritems():
        oldval = previous.get(key)
        if oldval != newval:
            ret[key] = (oldval, newval)
    return ret
