###
# Copyright (c) 2003-2005, James Vega
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions, and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions, and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the author of this software nor the name of
#     contributors to this software may be used to endorse or promote products
#     derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###

import re
import rssparser

from BeautifulSoup import BeautifulSoup

import supybot.conf as conf
import supybot.utils as utils
from supybot.commands import *
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks

class TrackerError(Exception):
    pass

class Sourceforge(callbacks.PluginRegexp):
    """
    Module for Sourceforge stuff. Currently contains commands to query a
    project's most recent bugs and rfes.
    """
    threaded = True
    callBefore = ['Web']
    regexps = ['sfSnarfer']

    _reopts = re.I | re.S
    _infoRe = re.compile(r'href="(?:/track[^"]+aid=(\d+)[^"]+)">\s+([^<]+)\s+</a>',
                         _reopts)
    _hrefOpts = '&set=custom&_assigned_to=0&_status=%s&_category=100' \
                '&_group=100&order=artifact_id&sort=DESC'
    _statusOpt = {'any':100, 'open':1, 'closed':2, 'deleted':3, 'pending':4}
    _optDict = {'any':'', 'open':'', 'closed':'', 'deleted':'', 'pending':''}

    _projectURL = 'http://sourceforge.net/projects/'
    _trackerURL = 'http://sourceforge.net/support/tracker.php?aid='
    def __init__(self, irc):
        self.__parent = super(Sourceforge, self)
        self.__parent.__init__(irc)

    def isCommand(self, name):
        if name in ('bug', 'rfe', 'patch'):
            return self.registryValue('enableSpecificTrackerCommands')
        else:
            return self.__parent.isCommand(name)

    def _formatResp(self, text):
        """
        Parses the Sourceforge query to return a list of tuples that
        contain the tracker information.
        """
        for item in filter(None, self._infoRe.findall(text)):
            if self.registryValue('bold'):
                yield (ircutils.bold(item[0]),
                       utils.web.htmlToText(item[1]))
            else:
                yield (item[0], utils.web.htmlToText(item[1]))

    def _getTrackerURL(self, project, Type, status):
        """
        Searches the project's Summary page to find the proper tracker link.
        """
        try:
            text = utils.web.getUrl('%s%s' % (self._projectURL, project))
        except utils.web.Error, e:
            raise callbacks.Error, str(e)
        soup = BeautifulSoup(text)
        linkRe = re.compile(r'tracker.*;atid')
        typeRe = re.compile(r'^%s$' % Type.strip(), re.I)
        trackers = soup('div', 'topnav')[0].ul('a', {'href': linkRe})
        opts = self._hrefOpts % self._statusOpt[status]
        url = 'http://sourceforge.net%%s%s' % opts
        for tracker in trackers:
            if typeRe.search(tracker.string):
                return url % utils.web.htmlToText(tracker['href'])
        else:
            raise TrackerError, 'Invalid Tracker page'

    def _getTrackerList(self, url, type):
        """
        Searches the tracker list page and returns a list of the trackers.
        """
        try:
            text = utils.web.getUrl(url)
        except utils.web.Error, e:
            raise callbacks.Error, str(e)
        if "No matches found." in text:
            return 'No %s were found.' % type
        head = '#%i: %s'
        resp = [format(head, *entry) for entry in self._formatResp(text)]
        if resp:
            if len(resp) > 10:
                resp = map(lambda s: utils.str.ellipsisify(s, 50), resp)
            return format('%L', resp)
        raise callbacks.Error, 'No %s were found.  (%s)' % \
              (type, conf.supybot.replies.possibleBug())

    _sfTitle = re.compile(r'Detail:\s*(\d+)\s*-\s*(\w.*)', re.I)
    _linkHref = re.compile(r'atid')
    def _getTrackerInfo(self, url):
        """
        Parses the specific tracker page, returning useful information.
        """
        try:
            s = utils.web.getUrl(url)
        except utils.web.Error, e:
            raise TrackerError, str(e)
        soup = BeautifulSoup(s)
        bold = self.registryValue('bold')
        resp = []
        head = ''
        sfTitle = self._sfTitle.search(soup.title.string)
        ul = soup('ul', {'id': 'breadcrumb'})[0]
        linkType = ul.first('a', {'href': self._linkHref}).string
        if sfTitle and linkType:
            linkType = utils.str.depluralize(linkType)
            (num, desc) = sfTitle.groups()
            if bold:
                head = format('%s #%i: %s',
                              ircutils.bold(linkType), num, desc)
            else:
                head = format('%s #%i: %s', linkType, num, desc)
            resp.append(head)
        else:
            return None
        table = soup.first('table')
        props = {}
        linkRe = re.compile(r'help_window')
        for td in table('td'):
            if td.b.string:
                props[td.b.string.rstrip(': ')] = td.br.next.strip('\t\n -')
            elif td('a', {'href': linkRe}):
                props[td.b.next.rstrip(': ')] = td.br.next.strip('\t\n -')
        for prop in ('Resolution', 'Date Submitted', 'Submitted By',
                     'Assigned To', 'Priority', 'Status'):
            try:
                if bold:
                    resp.append('%s: %s' % (ircutils.bold(prop), props[prop]))
                else:
                    resp.append('%s: %s' % (prop, props[prop]))
            except KeyError:
                pass
        return '; '.join(resp)

    def bug(self, irc, msg, args, id):
        """<id>

        Returns a description of the bug with id <id>.  Really, this is
        just a wrapper for the tracker command; it won't even complain if the
        <id> you give isn't a bug.
        """
        self._tracker(irc, id)
    bug = wrap(bug, [('id', 'bug')])

    def patch(self, irc, msg, args, id):
        """<id>

        Returns a description of the patch with id <id>.  Really, this is
        just a wrapper for the tracker command; it won't even complain if the
        <id> you give isn't a patch.
        """
        self._tracker(irc, id)
    patch = wrap(patch, [('id', 'patch')])

    def rfe(self, irc, msg, args, id):
        """<id>

        Returns a description of the rfe with id <id>.  Really, this is
        just a wrapper for the tracker command; it won't even complain if the
        <id> you give isn't an rfe.
        """
        self._tracker(irc, id)
    rfe = wrap(rfe, [('id', 'rfe')])

    def tracker(self, irc, msg, args, id):
        """<id>

        Returns a description of the tracker with id <id> and the corresponding
        url.
        """
        self._tracker(irc, id)
    tracker = wrap(tracker, [('id', 'tracker')])

    def _tracker(self, irc, id):
        try:
            url = '%s%s' % (self._trackerURL, id)
            resp = self._getTrackerInfo(url)
            if resp is None:
                irc.error('Invalid Tracker page snarfed: %s' % url)
            else:
                irc.reply('%s <%s>' % (resp, url))
        except TrackerError, e:
            irc.error(str(e))

    def _trackers(self, irc, args, msg, optlist, project, tracker):
        status = 'open'
        for (option, _) in optlist:
            if option in self._statusOpt:
                status = option
        try:
            int(project)
            s = 'Use the tracker command to get information about specific '\
                '%s.' % tracker
            irc.error(s)
            return
        except ValueError:
            pass
        if not project:
            project = self.registryValue('defaultProject', msg.args[0])
            if not project:
                raise callbacks.ArgumentError
        try:
            url = self._getTrackerURL(project, tracker, status)
        except TrackerError, e:
            irc.error('%s.  I can\'t find the %s link.' %
                      (e, tracker.capitalize()))
            return
        irc.reply(self._getTrackerList(url, tracker))

    def bugs(self, irc, msg, args, optlist, project):
        """[--{any,open,closed,deleted,pending}] [<project>]

        Returns a list of the most recent bugs filed against <project>.
        <project> is not needed if there is a default project set.  Search
        defaults to open bugs.
        """
        self._trackers(irc, args, msg, optlist, project, 'bugs')
    bugs = wrap(bugs, [getopts(_optDict), additional('something', '')])

    def rfes(self, irc, msg, args, optlist, project):
        """[--{any,open,closed,deleted,pending}] [<project>]

        Returns a list of the most recent rfes filed against <project>.
        <project> is not needed if there is a default project set.  Search
        defaults to open rfes.
        """
        self._trackers(irc, args, msg, optlist, project, 'rfe')
    rfes = wrap(rfes, [getopts(_optDict), additional('something', '')])

    def patches(self, irc, msg, args, optlist, project):
        """[--{any,open,closed,deleted,pending}] [<project>]

        Returns a list of the most recent patches filed against <project>.
        <project> is not needed if there is a default project set.  Search
        defaults to open patches.
        """
        self._trackers(irc, args, msg, optlist, project, 'patches')
    patches = wrap(patches, [getopts(_optDict), additional('something', '')])

    _intRe = re.compile(r'(\d+)')
    _percentRe = re.compile(r'([\d.]+%)')
    def stats(self, irc, msg, args, project):
        """[<project>]

        Returns the current statistics for <project>.  <project> is not needed
        if there is a default project set.
        """
        url = 'http://sourceforge.net/' \
              'export/rss2_projsummary.php?project=' + project
        results = rssparser.parse(url)
        if not results['items']:
            irc.errorInvalid('SourceForge project name', project)
        class x:
            pass
        def get(r, s):
            m = r.search(s)
            if m is not None:
                return m.group(0)
            else:
                irc.error('Sourceforge gave me a bad RSS feed.', Raise=True)
        def gets(r, s):
            L = []
            for m in r.finditer(s):
                L.append(m.group(1))
            return L
        def afterColon(s):
            return s.split(': ', 1)[-1]
        try:
            for item in results['items']:
                title = item['title']
                description = item['description']
                if 'Project name' in title:
                    x.project = afterColon(title)
                elif 'Developers on project' in title:
                    x.devs = get(self._intRe, title)
                elif 'Activity percentile' in title:
                    x.activity = get(self._percentRe, title)
                    x.ranking = get(self._intRe, afterColon(description))
                elif 'Downloadable files' in title:
                    x.downloads = get(self._intRe, title)
                    x.downloadsToday = afterColon(description)
                elif 'Tracker: Bugs' in title:
                    (x.bugsOpen, x.bugsTotal) = gets(self._intRe, title)
                elif 'Tracker: Patches' in title:
                    (x.patchesOpen, x.patchesTotal) = gets(self._intRe, title)
                elif 'Tracker: Feature' in title:
                    (x.rfesOpen, x.rfesTotal) = gets(self._intRe, title)
        except AttributeError:
            irc.error('Unable to parse stats RSS.', Raise=True)
        irc.reply(
            format('%s has %n, '
                   'is %s active (ranked %i), '
                   'has had %n (%s today), '
                   'has %n (out of %i), '
                   'has %n (out of %i), '
                   'and has %n (out of %i).',
                   x.project, (int(x.devs), 'developer'),
                   x.activity, x.ranking,
                   (int(x.downloads), 'download'), x.downloadsToday,
                   (int(x.bugsOpen), 'open', 'bug'), x.bugsTotal,
                   (int(x.rfesOpen), 'open', 'rfe'), x.rfesTotal,
                   (int(x.patchesOpen), 'open', 'patch'), x.patchesTotal))
    stats = wrap(stats, ['lowered'])

    _totbugs = re.compile(r'Bugs</a>\s+?\( <b>([^<]+)</b>', re.S | re.I)
    def _getNumBugs(self, project):
        try:
            text = utils.web.getUrl('%s%s' % (self._projectURL, project))
        except utils.web.Error, e:
            raise callbacks.Error, str(e)
        m = self._totbugs.search(text)
        if m:
            return m.group(1)
        else:
            return ''

    _totrfes = re.compile(r'Feature Requests</a>\s+?\( <b>([^<]+)</b>',
                          re.S | re.I)
    def _getNumRfes(self, project):
        try:
            text = utils.web.getUrl('%s%s' % (self._projectURL, project))
        except utils.web.Error, e:
            raise callbacks.Error, str(e)
        m = self._totrfes.search(text)
        if m:
            return m.group(1)
        else:
            return ''

    def total(self, irc, msg, args, type, project):
        """{bugs,rfes} [<project>]

        Returns the total count of open bugs or rfes.  <project> is only
        necessary if a default project is not set.
        """
        if type == 'bugs':
            self._totalbugs(irc, msg, project)
        elif type == 'rfes':
            self._totalrfes(irc, msg, project)
    total = wrap(total, [('literal',('bugs', 'rfes')),additional('something')])

    def _totalbugs(self, irc, msg, project):
        project = project or self.registryValue('defaultProject', msg.args[0])
        total = self._getNumBugs(project)
        if total:
            irc.reply(total)
        else:
            irc.error('Could not find bug statistics for %s.' % project)

    def _totalrfes(self, irc, msg, project):
        project = project or self.registryValue('defaultProject', msg.args[0])
        total = self._getNumRfes(project)
        if total:
            irc.reply(total)
        else:
            irc.error('Could not find RFE statistics for %s.' % project)

    def fight(self, irc, msg, args, optlist, projects):
        """[--{bugs,rfes}] [--{open,closed}] <project name> <project name> \
        [<project name> ...]

        Returns the projects, in order, from greatest number of bugs to least.
        Defaults to bugs and open.
        """
        search = self._getNumBugs
        type = 0
        for (option, _) in optlist:
            if option == 'bugs':
                search = self._getNumBugs
            if option == 'rfes':
                search = self._getNumRfes
            if option == 'open':
                type = 0
            if option == 'closed':
                type = 1
        results = []
        for proj in projects:
            num = search(proj)
            if num:
                results.append((int(num.split('/')[type].split()[0]), proj))
        results.sort()
        results.reverse()
        s = ', '.join([format('\'%s\': %i', s, i) for (i, s) in results])
        irc.reply(s)
    fight = wrap(fight, [getopts({'bugs':'','rfes':'','open':'','closed':''}),
                         many('something')])

    def sfSnarfer(self, irc, msg, match):
        r"https?://(?:www\.)?(?:sourceforge|sf)\.net/tracker/" \
        r".*\?(?:&?func=detail|&?aid=\d+|&?group_id=\d+|&?atid=\d+){4}"
        if not self.registryValue('trackerSnarfer', msg.args[0]):
            return
        try:
            url = match.group(0)
            resp = self._getTrackerInfo(url)
            if resp is None:
                self.log.info('Invalid Tracker page snarfed: %s', url)
            else:
                irc.reply(resp, prefixNick=False)
        except TrackerError, e:
            self.log.info(str(e))
    sfSnarfer = urlSnarfer(sfSnarfer)


Class = Sourceforge


# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:


syntax highlighted by Code2HTML, v. 0.9.1