##
## trac_utils.py
##
## Various utilities for viewing, applying, and testing patches from
## the sage trac server
##

## python imports
import os, sys, shutil, urllib

## sage imports
import sage.misc.hg as hg

##
## various globals
##
slash = os.path.sep

SAGE_TRAC = 'http://trac.sagemath.org'
TRAC_TICKET_PATH = '/sage_trac/ticket/'

repository_ls = [ hg.hg_sage, hg.hg_scripts, hg.hg_extcode, hg.hg_examples ]
repositories = { 'sage': hg.hg_sage,
                 'main': hg.hg_sage,
                 'scripts': hg.hg_scripts,
                 'bin': hg.hg_scripts,
                 'extcode': hg.hg_extcode,
                 'examples': hg.hg_examples }

def all_patches(n):
    """
    Given a ticket number (either as a string or integer), return the list
    of all patches stored on the Sage trac server on that ticket.
    """
    ticket_url = SAGE_TRAC + TRAC_TICKET_PATH + str(n)
    print "Fetching %s ..."%ticket_url
    try:
        f = urllib.urlopen(ticket_url)
    except IOError:
        raise IOError, "could not fetch %s"%ticket_url

    attachment_lines = [ x for x in f.readlines() if '/attachment/' in x ]
    f.close()

    attachment_names = []
    for x in attachment_lines:
        name = SAGE_TRAC + x[x.find('/sage_trac/'):x.find('.patch')+6]
        name = name.replace('/attachment/', '/raw-attachment/', 1)
        if name not in attachment_names:
            attachment_names.append(name)

    return attachment_names

def get_all_patches(patch_ls, directory=None, overwrite=False):
    """
    Given a list of patches, create a temporary directory and put all
    the patches there. if a directory is given, use that instead.
    """
    if directory is None:
        import sage.misc.misc as misc
        directory = misc.tmp_dir('release')
    else:
        directory = os.path.abspath(directory)
        if not os.path.exists(directory):
            os.makedirs(directory)

    os.chdir(directory)
    print "Fetching patches and storing in %s:"%directory

    patch_files = []
    for patch_url in patch_ls:
        patch_filename = os.path.split(patch_url)[1]
        if os.path.exists(patch_filename):
            if overwrite:
                os.remove(patch_filename)
            else:
                raise OSError, "patch file %s already exists"%(directory + slash + patch_filename)
        print "Fetching %s ..."%patch_url
        try:
            urllib.urlretrieve(patch_url, filename=patch_filename)
        except IOError:
            os.chdir('..')
            shutil.rmtree(directory)
            raise IOError, "error fetching %s"%patch_url
        patch_files.append(patch_filename)
            
    return patch_files, directory

def apply_patches(patch_ls, patch_directory, order=None, dry_run=True):
    """
    Applies a list of patches to sage.
    """
    repos_used = []
    if order is None:
        order = [ (x, repositories['sage']) for x in patch_ls ]
        repos_used.append(repositories['sage'])
    else:
        for i in range(len(order)):
            item = order[i]
            if isinstance(item, tuple):
                if len(item) != 2 or (not isinstance(item[0], str)) or \
                   (item[1] not in repository_ls):
                    raise ValueError, "unknown patch %s"%item
                repository = item[1]
                item = item[0]
            else:
                repository = repositories['sage']

            if isinstance(item, str):
                if not item in patch_ls:
                    raise ValueError, "unknown patch %s"%item
                order[i] = (item, repository)
            else:
                try:
                    ind = int(item)
                except ValueError:
                    raise ValueError, "unknown patch %s"%item
                if (0 > ind) or (ind > len(patch_ls)):
                    raise IndexError, "no patch of index %s"%ind
                order[i] = (patch_ls[ind], repository)
            if not repository in repos_used:
                repos_used.append(repository)

    if dry_run:
        for patch, repository in order:
            print "applying patch %s/%s to repository %s"%(patch_directory, patch, repository.dir())
        return

    # make sure we have queues enabled in all the repos
    for repo in repos_used:
        res, error = repo('qinit', interactive=False)
        if 'unknown command' in error:
            raise NotImplementedError, "please enable Mercurial queues"
        series, err = repo('qser', interactive=False)
        if series != '':
            # TODO: use qsave/qrestore to make this possible. not hard,
            # but I need to make sure I understand what happens to the live
            # changes when I do this. Or maybe just qtop?
            #
            # here's a way to get the appropriate revision number to save
            # *after* we repo('qsave'):
            # repo('tip', interactive=False)[0].split()[1].split(':')[0]
            raise NotImplementedError, "cannot merge patches with nonempty queue series"

    # start applying some patches
    applied_patches = []
    for patch, repo in order:
        if not os.path.exists(patch_directory + slash + patch):
            raise OSError, "could not find patch file" + patch_directory + slash + patch

        patch_filename = patch_directory + slash + patch
        msg, err = repo('qimport %s'%patch_filename, interactive=False)
        if 'abort' in err:
            # TODO: kill all the patches we've applied
            raise ValueError, "error applying patch %s"%patch_filename

        msg, err = repo('qpush', interactive=False)
        if 'abort' in err:
            # TODO: kill all the patches we've applied
            raise ValueError, "error applying patch %s"%patch_filename

        applied_patches.append((patch, repo))

    return applied_patches, repos_used, order

def commit_queue(repos_used):
    """
    Given a list of repositories, commit the queue in each into the
    repository. (That is, call qcommit on each.)
    """
    for repo in repos_used:
        msg, err = repo('qfinish -a', interactive=False)
        if 'abort' in err:
            raise OSError, "error committing to repository %s: %s"%(repo.dir(), err)

def clear_queue(applied_patches, repos):
    """
    Given a list patch_ls, each entry of which is of the form (patch,
    repo), qpop and qdelete each of them from the corresponding
    repository.
    """
    for repo in repos:
        msg, err = repo('qpop -a', interactive=False)
        if 'abort' in err:
            raise ValueError, "error popping patches from repository %s: %s"%(repo.dir(), err)
        
    for patch, repo in applied_patches:
        msg, err = repo('qdelete %s'%patch, interactive=False)
        if 'abort' in err:
            raise ValueError, "error deleting patches from repository %s: %s"%(repo.dir(), err)

        
