# -*- encoding: utf-8 -*-
#   Copyright 2008 Agile42 GmbH, Berlin (Germany)
#   Copyright 2007 Andrea Tomasini <andrea.tomasini__at__agile42.com>
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#   Authors: 
#        - Andrea Tomasini <andrea.tomasini__at__agile42.com>

import sys
import re

from trac.env import Environment
from trac.util.text import to_unicode

from agilo.ticket.model import AgiloTicket, AgiloTicketModelManager
from agilo.utils import Key, Status
from agilo.utils.errors import *
#TODO: Import and use to invalidate cache from the SVN hook, only works when 
# there will be a .lock file as invalidation.
# from agilo.charts.chart_generator import ChartGenerator

__all__ = ['AgiloSVNPreCommit', 'AgiloSVNPostCommit']

# Commands definition and regular expressions
CLOSE_COMMANDS = ['close', 'closed', 'closes', 'fix', 'fixed', 'fixes']
REFER_COMMANDS = ['addresses', 're', 'references', 'refs', 'see']
REMAIN_COMMANDS = ['remaining', 'still', 'rem_time', 'time', 'rem']
ALL_COMMANDS = CLOSE_COMMANDS + REFER_COMMANDS + REMAIN_COMMANDS
# Regular Expression to match valid commands only
TIME_PATTERN = '(?:[0-9]+)(?:\.[0-9]+)?(?:h|d)'
commandRe = r'(?P<action>' + r'|'.join(ALL_COMMANDS) + r').?(?P<ticket>#[0-9]+(\:' + TIME_PATTERN + '){0,1}(?:(?:[, &]*|[ ]?and[ ]?)#[0-9]+(\:' + TIME_PATTERN + '){0,1})*)'
commandPattern = re.compile(commandRe, re.DOTALL|re.IGNORECASE)
ticketPattern = re.compile(r'#(?P<ticket_id>[0-9]+)(?P<rem_time>\:' + TIME_PATTERN + '){0,1}')


class AgiloSVNPreCommit(object):
    """
    An Agilo SVN hook to process the SVN comment before the commit.
    It checks whether there are valid tickets number into the comment, 
    and in case there are valid tickets, it checks that if there is a
    remaining_time specified than the ticket is of type task and it is 
    not closed.
    """
    def __init__(self, project, log, env=None):
        """Initialize the class with the project path and the revision"""
        try:
            self.env = env or Environment(project)
            self.tm = AgiloTicketModelManager(self.env)
            self.log = log
        except Exception, e:
            #print_exc()
            print >> sys.stderr, "An Error occured while opening Trac project: '%s' => %s" % \
                                 (project, to_unicode(e))
            print >> sys.stderr, "AgiloSVNPreCommit initialized with: '%s' %s, '%s' %s" % \
                                 (project, type(project), log, type(log))
            sys.exit(1)
        
    def execute(self):
        """Execute the hook"""
        # Now get all the commands and tickets out of the log and
        # check whether are valid or not
        errors = list()
        at_least_one = False
        #print "Log: %s" % self.log
        for valid in commandPattern.finditer(self.log):
            #print "Expression matching: <%s> <%s>" % (valid.group('action'), valid.group('ticket'))
            for ticket_id, rem_time in ticketPattern.findall(valid.group('ticket')):
                # Check that the ticket is existing first
                try:
                    t_id = int(ticket_id.replace('#', ''))
                    ticket = self.tm.get(tkt_id=t_id)
                    at_least_one = True
                    if rem_time and not ticket.is_readable_field(Key.REMAINING_TIME):
                        errors.append("Remaining time is not an allowed property for this ticket #%s" \
                                      " of type: '%s', with remaining time: %s" % \
                                      (ticket_id, ticket.get_type(), rem_time[1:]))
                        #print "Error: %s" % errors[-1:]
                except Exception, e:
                    errors.append("Unable to verify ticket #%s, reason: '%s'" % (ticket_id, to_unicode(e)))
        if len(errors) > 0:
            #print >> sys.stderr, "\n".join(errors)
            raise InvalidAttributeError("\n".join(errors))
        elif not at_least_one and self.log.find('#') != -1:
            raise ParsingError("No valid command found in: '%s'\n" \
                               "Supported commands include:\n" \
                               "Close Tickets: %s\n" \
                               "Refer Tickets: %s\n" \
                               "Remaining Time: %s\n" % \
                               (self.log, CLOSE_COMMANDS, REFER_COMMANDS, REMAIN_COMMANDS))
        elif len(self.log) == 0:
            raise Exception("Please provide a comment")
        return True
        

class AgiloSVNPostCommit(object):
    """
    An Agilo SVN hook to process the SVN comment after the commit.
    To use it just call it as AgiloSVNPostCommit() from a script called
    post-commit into the <repository>/hooks/ folder of your SVN server.
    Tries to keep compatibility with the trac-post-commit-hook.py written
    by Stephen Hansen, Copyright (c) 2004 and distributed. 
    """
    def __init__(self, project, rev, env=None):
        """Initialize the class with the project path and the revision"""
        try:
            self.env = env or Environment(project)
            self.tm = AgiloTicketModelManager(self.env)
            repos = self.env.get_repository()
            repos.sync()
        except Exception, e:
            print >> sys.stderr, "An Error occurred while opening Trac project: %s => %s" % (project, to_unicode(e))
            sys.exit(1)
        # Now let's read the last committed revision data
        try:
            changeset = repos.get_changeset(int(rev))
        except Exception, e:
            print >> sys.stderr, "Impossible to open revision: %s, due to the following error: %s" % (rev, to_unicode(e))
            sys.exit(1)
        self.author = changeset.author
        self.rev = rev
        self.message = "(In [%s]) %s" % (rev, changeset.message)
        # Now parse the command in the ticket, and react accordingly
        self.commands = dict()
        for valid in commandPattern.finditer(changeset.message):
            for ticket_id, rem_time in ticketPattern.findall(valid.group('ticket')):
                #print "Ticket #%s, Remaining: %s" % (ticket_id, rem_time)
                self.commands.setdefault(int(ticket_id), list()).append(
                                        self.findCommand(valid.group('action'), 
                                                         remaining=rem_time[1:-1]))
    
    def execute(self):
        """Execute the parsed commands"""
        #print "Commands: %s" % self.commands
        # Sort the ticket in reverse order by id, it will be most likely
        # that a task is existing after a User Story has been created, 
        # in which case it will be possible to execute multiple command in
        # a hierarchy. TODO: Check hierarchy, but very expensive
        keys = self.commands.keys()
        keys.sort(reverse=True)
        for t_id, cmds in [(key, self.commands[key]) for key in keys]:
            ticket = self.tm.get(tkt_id=t_id)
            for cmd in cmds:
                cmd(ticket)
            self.tm.save(ticket, author=self.author, comment=self.message)
        # We need to invalidate the chart cache here because some tickets may
        # have been closed through commit comments. Unfortunately there is 
        # no way to access the shared memory right now, see #565
        return True
    
    def findCommand(self, cmd, **kwargs):
        """
        Returns the command corresponding to the given pattern, and
        the given parameter, so that the method can be called later
        """
        def closeCommand():
            """
            Closes a ticket and applies all the defined rules
            """
            def execute(ticket):
                if isinstance(ticket, AgiloTicket):
                    if ticket.is_writeable_field(Key.REMAINING_TIME):
                        # Check if the task as been assigned
                        if ticket[Key.STATUS] in (Status.ACCEPTED, Status.ASSIGNED):
                            # If the author is the owner of the ticket close it
                            owner = ticket[Key.OWNER]
                            if owner == self.author:
                                ticket[Key.STATUS] = Status.CLOSED
                                ticket[Key.RESOLUTION] = Status.RES_FIXED
                                ticket[Key.REMAINING_TIME] = '0'
                            else:
                                raise NotOwnerError("You (%s) are not the owner of this task (%s)," \
                                                    " can't close it!" % (self.author, owner))
                        else:
                            raise NotAssignedError("The task(#%d) is not assigned (%s), you have to accept" \
                                                   " it before closing it!" % \
                                                   (ticket.get_id(), ticket[Key.STATUS]))
                    else:
                        # Check if all the linked items are closed than close it
                        close = True
                        for linked in ticket.get_outgoing():
                            if linked[Key.STATUS] != Status.CLOSED:
                                close = False
                                break
                        if close:
                            ticket[Key.STATUS] = Status.CLOSED
                            ticket[Key.RESOLUTION] = Status.RES_FIXED
                        else:
                            raise DependenciesError("The ticket(#%d) of type: '%s' has still "\
                                                    "some open dependencies... can't close it!" % \
                                                    (ticket.get_id(), ticket.get_type()))
            # Return the method
            return execute
                        
        def remainCommand(**kwargs):
            """
            Sets the remaining time for the given linked end point
            """
            rem_time = 0
            #print "Arguments: %s" % kwargs
            if kwargs and len(kwargs) > 0:
                if kwargs.has_key('remaining'):
                    # If conversion fails it will raise an exception
                    rem_time = kwargs['remaining']
                    #print "Remaining time detected: %s" % rem_time
            def execute(ticket):
                if isinstance(ticket, AgiloTicket) and ticket.is_writeable_field(Key.REMAINING_TIME):
                    # Check if the task is assigned and the current author is the owner
                    owner = ticket[Key.OWNER]
                    if owner == self.author:
                        ticket[Key.REMAINING_TIME] = rem_time
                        if ticket[Key.STATUS] not in (Status.ASSIGNED, Status.ACCEPTED):
                            # If the ticket is not already accepted, set it to assigned
                            ticket[Key.STATUS] = Status.ACCEPTED
                    else:
                        raise NotOwnerError("You (%s) are not the owner (%s) of the task(#%d)"\
                                            " changing the remaining time is not allowed!" % \
                                            (self.author, owner, ticket.get_id()))
                else:
                    raise InvalidAttributeError("The ticket(#%d) type %s, doesn't allow the attribute: %s" % \
                                                (ticket.get_id(), ticket.get_type(), Key.REMAINING_TIME))
            # Return the method
            return execute
            
        def voidCommand(**kwargs):
            """Does intentionally nothing"""
            def execute(ticket):
                pass
            # Return the doing nothing method ;-)
            return execute
            
        # Find the right command
        cmd = cmd.lower()
        #print "Command received: %s" % cmd
        if cmd in CLOSE_COMMANDS:
            return closeCommand()
        elif cmd in REFER_COMMANDS:
            return voidCommand()
        elif cmd in REMAIN_COMMANDS:
            return remainCommand(**kwargs)
        else:
            return voidCommand()
        
