# ----------------------------------------------------------------
# Defines some helper types and functions for batchSVN and batchSVNData
# 
# by Curt Clifton, September 2009
# ----------------------------------------------------------------

import os
from string import Template
from subprocess import Popen, PIPE, STDOUT

class RepositorySet:
    """Represents a collection of SVN repositories.
    
    Examples include all the individual student repositories for the course, or 
    the set of team repositories for a particular project."""
    def __init__(self, name, reposPrefix, reposSuffixes):
        self.name = name
        self.prefix = reposPrefix
        self.reposSuffixes = reposSuffixes
    def repos(self):
        for s in self.reposSuffixes:
            yield Repository(self.prefix + s, s)
    def __repr__(self):
        return "RepositorySet(%s,%s,%s)" % (self.name, self.prefix, self.reposSuffixes)

class Repository:
    """Represents a single repository."""
    def __init__(self, longName, shortName):
        self.longName = longName
        self.shortName = shortName
    def __repr__(self):
        return "Repository(%s,%s)" % (self.longName, self.shortName)

class Project:
    """Represents a project and associated JUnit test case classes."""
    def __init__(self, name, *testClasses):
        """Constructs a Project instance.
        
        Optional arguments are strings giving fully qualified names of JUnit
        test classes."""
        self.name = name
        self.testClasses = testClasses
    def __repr__(self):
        args = list(self.testClasses)
        args.insert(0, self.name)
        args = ','.join(args)
        return "Project(" + args + ")"

class MenuItem:
    """Represents a menu item to be added to the main menu of the script."""
    def __init__(self, itemText, itemFunction):
        """Constructs a new menu item.
        
        itemFunction is a one-argument function that will be executed
        when a user selects the menu item.  It will be passed a DataBlob
        object whose fields are the relevant "global" variables from the
        main application.  See batchSVN.py for details on the included
        values.  The function should return True if the program should
        exit after executing the function, or False otherwise.
        """
        self.text = itemText
        self.function = itemFunction
    def display(self, nextItemNumber):
        '''Displays this menu item with the given item number.  Returns the
        appropriate number for the item after this one.'''
        print "%2d - %s" % (nextItemNumber, self.text)
        return nextItemNumber + 1

class MenuSeparator:
    """Represents a separator between items in a menu."""
    def __init__(self, separatorText = '----'):
        self.text = separatorText
    def display(self, nextItemNumber):
        '''Displays this menu separator without an item number. Returns the
        appropriate number for the item after this one, that is, nextItemNumber'''
        print self.text
        return nextItemNumber

class DataBlob:
    """Contains fields representing the relevant "global" variables from the
    main application.
    
    Used to pass data into menu item functions.
    """
    pass

# ----------------------------------------------------------------
def truncateMiddle(d, length):
    """Returns a truncated version of d by omitting characters in the middle
    if d is longer then length."""
    splitLen = length / 2
    if len(d) < length:
        return d
    else:
        return d[:splitLen] + '...' + d[-(length-3-splitLen):]

# ----------------------------------------------------------------
def displayMenu(prompt, items, allowOthers = False):
    """
    Displays a numbered menu consisting of the given list menu items.
    Returns a tuple consisting of the chosen item number (or None if
    other text is entered) and the text of the choice.  items may be a
    list of strings or a list of MenuItem and MenuSeparator objects.

    Presents the given prompt.  Continues presenting the prompt until
    the user picks an item from the list.  If allowOthers is True, then
    also lets user enter a non-numeric string which is the text returned
    to the caller.  Otherwise the element of 'items' corresponding to the
    entered number is returned.
    """
    if isinstance(items[0], str):    
        for i,t in enumerate(items):
            print "%2d - %s" % (i, t)
    else:
        num = 0
        for mi in items:
            num = mi.display(num)
        items = filter(lambda x: isinstance(x, MenuItem), items)
    print
        
    
    choice = ''
    while choice == '':
        inputValue = raw_input(prompt).strip()
        try:
            inputValue = int(inputValue)
            choice = items[inputValue]
            
        except ValueError:
            if allowOthers:
                choice = inputValue
                inputValue = None
            else:
                print '\a*** you must choose a number from the list above ***'
                choice = ''
        except IndexError:
            print '\a*** only numbers in the list above are valid ***'

    return inputValue, choice

# ----------------------------------------------------------------
def forEachRepo(command, data, msg='', create=False, 
                withCapture=False, suppressOutput=False):
    """Iterates over each repository executing the given command.  The command
    is executed with the current working directory set to the local working
    directory.  The command is a template string.  Within the template string:
    
        ${rootURL} is replaced with the root URL of the repository
        ${repo} is replaced with the full repository name
        ${proj} is replaced with the project name
        ${shortName} is replaced with the short repository name, that is
                        the full repository name with the course/term prefix
                        removed
        ${msg} is replaced with the msg argument
        ${scriptDir} is replaced with the script directory
        ${cwd} is replaced with the working directory
        ${testClasses} is replaced with a space-delimited list of the fully
                          qualified names of the JUnit test classes
                      
    msg is the replacement for the ${msg} placeholder in the command
    
    data is a DataBlob containing the "global" variables from the main script
    
    create specifies whether the local project subdirectory should be created
    
    withCapture specifies whether the output (stdout and stderr) of the command
        should be captured to batchLogFileName, lines of captured data are 
        prefixed with the short repository name, a colon, and a space
    
    suppressOutput specified whether the output of the command should be 
        suppressed on the console
    """
    replacements = {}
    replacements['rootURL'] = data.rootURL
    replacements['proj'] = data.activeProject.name
    replacements['msg'] = "'" + msg + "'"
    replacements['scriptDir'] = data.scriptDir
    replacements['cwd'] = data.workingDir
    replacements['testClasses'] = ' '.join(data.activeProject.testClasses)
    commandTemplate = Template(command)
    
    if command.find('${testClasses}') >= 0 and len(data.activeProject.testClasses) == 0:
        print '\a\n*** No test classes listed for project "' + data.activeProject.name + '" ***\n'
        return

    # Creates project specific subdirectory if requested
    if create:
        if os.access(data.activeProject.name,os.F_OK):
            print '\a\n*** Local project subdirectory already exists ***\n'
            return
        else:
            os.mkdir(data.activeProject.name)
            if not os.access(data.activeProject.name,os.F_OK):
                print '\a\n*** Unable to create local project subdirectory ***\n'
                return

    # Verifies project subdirectory exists
    if not os.access(data.activeProject.name,os.F_OK):
        print '\a\n*** Unable to access local project subdirectory ***\n'
        return
        
    # Functional goodness for printing
    if suppressOutput:
        def printHelper(x):
            pass
    else:
        def printHelper(x):
            print x

    # Iterates over the repositories
    if withCapture:
        captureFile = open(data.batchLogFileName, 'w')
    for repo in data.activeRepoSet.repos():
        replacements['repo'] = repo.longName
        replacements['shortName'] = repo.shortName
        cmd = commandTemplate.substitute(replacements)

        printHelper('-*' * (data.menuWidth / 2))
        shortNameLength = len(replacements['shortName'])
        padRepeat = (data.menuWidth - shortNameLength) / 2 / 4
        printHelper('--> ' * padRepeat + replacements['shortName'].upper() + ' <--' * padRepeat)
        printHelper(truncateMiddle(cmd, data.menuWidth))

        os.chdir(data.activeProject.name)
        p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
        (cmdInput, cmdOutput) = (p.stdin, p.stdout)
        outputText = cmdOutput.readlines()
        cmdInput.close()
        cmdOutput.close()
        for line in outputText:
            printHelper(line.strip())
            if withCapture:
                captureFile.write(replacements['shortName'])
                captureFile.write(': ')
                captureFile.write(line)
        os.chdir('..')
    if withCapture:
        captureFile.close()