''' Miscellaneous Arsenal routines

This contains general purpose Arsenal code

Copyright (C) 2008 Canonical Lt.
Author: Bryce Harrington <bryce.harrington@ubuntu.com>

This program 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 2 of the License, or (at your
option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
the full text of the license.
'''

import re, os, sys
import time, datetime
from urllib2 import URLError, HTTPError
import httplib2

# TODO:  Provide mechanism for selecting the testing service rather than edge
from launchpadlib.launchpad import Launchpad, EDGE_SERVICE_ROOT
from launchpadlib.errors import HTTPError
from launchpadlib.credentials import Credentials

service_root = EDGE_SERVICE_ROOT
#service_root = "https://api.launchpad.net"

# TODO:  Merge code ideas from other lpl users
# TODO:  Remove use of first bugtask for some items
# TODO:  Add ''' ''' code docs for all functions
# TODO:  Separate the Xorg-specific stuff into a separate library file

class Arsenal:
    ''' High level interface to launchpad '''

    def __init__(self):
        ''' Construct Arsenal object, including obtaining launchpad credentials '''

        self.project_name = ""

        home = os.path.expanduser('~')
        self.name = 'arsenal'

        ''' Config dir '''
        self.confdir = os.path.join(home, '.config', self.name)
        if not os.path.exists(self.confdir):
            # TODO:  Create a dbgmsg type routine for controlling verbosity
            print "Creating .config dir"
            os.makedirs(self.confdir)

        ''' Cache dir '''
        self.cachedir = os.path.join(home, '.cache', self.name)
        if not os.path.exists(self.cachedir):
            print "Creating cache dir"
            os.makedirs(self.cachedir,0700)

        return self.get_creds()

    def get_creds(self):
        ''' Credentials '''
        retrieve_credentials = False
        # TODO:  Should lp-credentials.txt be $script.cred ?
        self.credentials_file = os.path.join(self.confdir, "lp-credentials.txt")
        if not os.path.exists(self.credentials_file):
            print "No existing credentials - need to create new ones"
            retrieve_credentials = True
        else:
            print "Atttempting to reuse existing credentials"
            try:
                credentials = Credentials()
                credentials.load(open(self.credentials_file))
                self.launchpad = Launchpad(credentials, service_root, self.cachedir)
            except:
                print "Error:  Launchpad failed to give credentials"
                retrieve_credentials = True

        if retrieve_credentials:
            print "Retrieving credentials from launchpad for",self.name,"to",self.cachedir
            self.launchpad = Launchpad.get_token_and_login(self.name, service_root, self.cachedir)
            credfd = open(self.credentials_file, "w")
            os.chmod(self.credentials_file, 0600)
            self.launchpad.credentials.save(credfd)
            credfd.close()

        return

    def reset(self):
        self.get_creds()

        if self.project_name:
            self.load_project(self.project_name)

    def Debug(self, level):
        httplib2.debuglevel = level

    def load_project(self, project):
        tries = 3
        while tries > 0:
            tries -= 1
            try:
                self.project = self.launchpad.load("https://api.edge.launchpad.net/beta/" + project)
                tries = 0
            except:
                print "*** Error:  Exception cast loading project",project,"***"

        self.project_name = project
        return self.project

    # TODO:  Support positional parameters
    def new_bug(self, package, title, description):
        # TODO:  Allow setting status, etc.
        target = self.launchpad.load(self.project.self_link + "/+source/" + package);
        bug_url = self.launchpad.bugs.createBug(
            target = target,
            title = title,
            description = description)
        return ArsenalBug(bug_url, self.launchpad)

    def get_bug(self, bug_number):
        return self.launchpad.bugs[bug_number]



class ArsenalBug:
    ''' Wrapper around launchpadlibs bug object '''

    # TODO:  Really want to wrapper a bug_task?
    def __init__(self, bug, launchpad):
        self.bug = bug
        self.bug_tasks = bug.bug_tasks
        self.id  = bug.id
        self.title = bug.title.encode('utf-8')
        self.attachments = bug.attachments
        self.description = bug.description.encode('utf-8')
        self.owner = bug.owner
        self.owner_firstname = self.owner.name.split(' ')[0]
        self.launchpad = launchpad
        return

    def has_attachment(self, filename):
        # TODO:  Implement
        # TODO:  Maybe use regex for detection?
        return False

    def has_tag(self, tag):
        return tag in self.bug.tags

    def append_tag(self, tag):
        id = self.id
        if not tag in self.bug.tags:
            #self.bug.tags.append(tag)
            # Workaround bug #254901
            self.bug = self.launchpad.load("https://api.edge.launchpad.net/beta/bugs/%d" % (id))
            tag_list = self.bug.tags
            tag_list.append(tag)
            self.bug.tags = tag_list

            print " ---> Tagged ",tag
            # Workaround bug #336866
            self.bug.lp_save()
            # Reload bug
            self.bug = self.launchpad.load("https://api.edge.launchpad.net/beta/bugs/%d" % (id))

            return True
        return False

    def remove_tag(self, tag):
        if tag in self.bug.tags:
            # Workaround bug #254901
            tag_list = self.bug.tags
            tag_list.append(tag)
            self.bug.tags = tag_list

            # Workaround bug #336866
            id = self.bug.id
            self.bug.lp_save()
            # Reload bug
            self.bug = self.launchpad.load("https://api.edge.launchpad.net/beta/bugs/%d" % (id))

            print " ---> Removed tag ",tag
            return True
        return False

    def append_description(self, text):
        self.bug.description = self.bug.description + "\n" + text

    def append_comment(self, message):
        # First doublecheck that we've not posted this comment before, so
        # automated calls to this routine don't end up spamming the reporter
        for m in self.bug.messages:
            if m.content == message:
                print " ---> Seems to already have this message"
                return
        self.bug.newMessage(subject = "Re: "+self.title, content = message)
        return True

# These affect only bug_task, not bug
#    def status(self, status):
#        self.bug_task.transitionToStatus(status = status)
#        return self.bug_task.status

#    def importance(self, importance):
#        self.bug_task.transitionToImportance(importance = importance)
#        return self.bug_task.importance

    def subscribe(self, subscriber):
        person = self.launchpad.load('https://api.edge.launchpad.net/beta/~' + subscriber)
        self.bug.subscribe(person = person)
        return True

    def dupe(self, dupe):
        self.bug.duplicate_of = dupe
        self.bug.lp_save()
#        # Reload bug
#        self.bug = self.launchpad.load("https://api.edge.launchpad.net/beta/bugs/%d" % (id))
        return True

    def subscribe(self, lp_id):
        print "bug " + str(bug.id) + ": subscribing " + lp_id.display_name
        self.bug.subscribe(person=lp_id)

    def unsubscribe(self, lp_id):
        # this currently does not work due to
        # https://bugs.launchpad.net/malone/+bug/281028
        print "bug " + str(self.bug.id) + ": unsubscribing " + lp_id.display_name
        self.bug.unsubscribe(person=lp_id)

    def age(self):
        ''' Age of bug in days '''
#        now = datetime.datetime.now()
#        dlm_str = self.bug.date_created.split('.')[0]
#        dlm = datetime.datetime(*(time.strptime(dlm_str, "%Y-%m-%dT%H:%M:%S")[0:6]))
        dlm = self.bug.date_created
        now = dlm.now(dlm.tzinfo)
        return (now - dlm).days

    def age_last_message(self):
        ''' Age of last comment to bug in days '''
#        now = datetime.datetime(now(), timezone=True)
#        dlm_str = self.bug.date_last_message.split('.')[0]
#        dlm = datetime.datetime(*(time.strptime(dlm_str, "%Y-%m-%dT%H:%M:%S")[0:6]))
        dlm = self.bug.date_last_message
        now = dlm.now(dlm.tzinfo)
        return (now - dlm).days

    def age_last_updated(self):
        ''' Age of last update to bug in days '''
#        now = datetime.datetime.now()
#        dlm_str = self.bug.date_last_updated.split('.')[0]
#        dlm = datetime.datetime(*(time.strptime(dlm_str, "%Y-%m-%dT%H:%M:%S")[0:6]))
        dlm = self.bug.date_last_updated
        now = dlm.now(dlm.tzinfo)
        return (now - dlm).days

# TODO:  Need a class specifically for backtraces
# TODO:  Review and adapt ideas from apport

def has_multiline_backtrace(text):
    '''
    Detects if there is a backtrace at least 3 levels deep
    '''
    regex_0 = re.compile('^#0 \w+')
    regex_1 = re.compile('^#1 0x\d+')
    regex_2 = re.compile('^#2 0x\d+')

    return regex_0.search(text) and \
           regex_1.search(text) and \
           regex_2.search(text)

def has_full_backtrace(text):
    '''
    Detects if backtrace contains parameter values
    '''
    regex = re.compile('^#\d+ 0x\d+ in \w+ \(\s+.*\)')
    regex_param = re.compile('^\s+\w+ = .+')

    return regex.search(text) and \
           regex_param.search(text)

def has_truncated_backtrace(text):
    '''
    Detects if the text has at least one line with a function name only
    '''
    regex = re.compile('^#\d+ 0x\d+ in \w+ \(\)$')
    return regex.search(text)

def has_xorg_backtrace(text):
    '''
    Matches the typical Xorg.0.log backtrace, even if no symbols are installed
    '''
    regex_symbolless = re.compile('^#\d+: [\w\/\.]+ \[0x[0-9a-f]+\]')
    regex_symbolled  = re.compile('^#\d+: [\w\/\.]+\(.+\) \[0x[0-9a-f]+\]')
    return regex_symbolless.search(text) or \
           regex_symbolled.search(text)

def has_backtrace(text):
    '''
    General purpose check for presence of a backtrace of any format
    '''
    return has_xorg_backtrace(text) or \
           has_multiline_backtrace(text)

# TODO:
#    * Routine for comparing two backtraces and judging if they're equivalent
#      -> Then a script could identify them as potential dupes
#    * Parser to break a whole stack trace down into constituent pieces
#    * Of a set of routines, identify the first one in actual X code
#    * Identify crashes that seem to be kernel vs. driver vs. mesa vs. X
#    * Extract code snippets for each function in the stacktrace
#    * Spot null pointers or other irregular looking pointers
#    * Tool to delete CoreDump.gz and un-privatize crash bugs with valid backtraces
#    * Script to extract backtrace text and put into description
#    * Add code to process-* scripts to identify crash bugs that lack
#      full backtraces, and guide the user to collect them

def dump_launchpad_object(i):
    print repr(i)
    print " attr:  ", sorted(i.lp_attributes)
    print " ops:   ", sorted(i.lp_operations)
    print " coll:  ", sorted(i.lp_collections)
    print " entr:  ", sorted(i.lp_entries)
    print

