#!/usr/bin/env python

import os
import re
from optparse import OptionParser
from sys import stderr
from subprocess import Popen, PIPE
from warnings import warn

if "SAGE_ROOT" in os.environ:
    sage = os.environ["SAGE_ROOT"]+"/sage"
else:
    sage = "sage"

##############################################################################
# Command line option handling

usage = r"""usage: %prog [options] command
list of commands:
 config     show current configuration (sage command, path, version, ...)
 install    install the sage-combinat patches
 update     update to the latest sage-combinat patches
 upgrade    upgrade sage and update to the latest sage-combinat patches
 status     show changed files in the working directory and in the patch queue
 qselect    choose appropriate guards for the current version of sage"""
parser = OptionParser(usage=usage)
parser.add_option("-b", "--branch", dest="branch",
                  help="Use sage-BRANCH instead of sage-combinat",
                  metavar="BRANCH", default = "combinat", type="string")

def set_sage_command(option, opt, value, parser):
    global sage
    if sage == value:
        return
    setattr(parser.values, option.dest, value)

parser.add_option("--sage", action = "callback", callback = set_sage_command,
                  help="Specify and store the sage command line",
                  metavar="/opt/bin/sage", default = sage, type="string")
parser.add_option("-f", "--force", action="store_true", dest="force",
                  help="Force proceeding")
parser.set_defaults(verbose=True)
parser.add_option("-v", action="store_true", dest="verbose",
                  help="Print status messages to stdout")
parser.add_option("-q", "--quiet",
                  action="store_false", dest="verbose",
                  help="Don't print status messages to stdout")
parser.add_option("-s", "--server", dest="server",
                  help="Set the URL for the sage-combinat server",
                  metavar="http://combinat.sagemath.org/patches/",
                  default="http://combinat.sagemath.org/patches/", type="string")

parser.add_option("-n", dest="n", action="store_true",
                  help="After qselect: disable all previous non version guards")

(options, args) = parser.parse_args()

##############################################################################
# Utilities

def info(mesg):
    global options
    if options.verbose:
        print >> stderr, mesg

def error(s):
    print >> stderr, "Error: "+s
    parser.print_help()
    exit(0)

def system(command):
    info("  "+command)
    ret = os.system(command)
    if not ret == 0:
        print >> stderr, "Abort"
        exit(ret)

def get_sage_root():
    r"""
    Returns the root directory of the sage installation (e.g. /opt/sage)
    """
    global sage
    if os.environ.has_key("SAGE_ROOT"):
        return os.environ["SAGE_ROOT"]
    # Query the environment variable via sage -sh
    try:
        sage_root = Popen(["echo 'echo ROOT${SAGE_ROOT}ROOT' | "+sage+" -sh"], stdout=PIPE, shell=True).communicate()[0]
    except OSError, e:
        error("Could not start sage"+str(e))
    return re.split("ROOT", sage_root)[1]

"""
A regexp matching Sage version strings

EXAMPLES::

    sage: re.search(version_regexp, "Sage Version 5.0").group()
    '5.0'
    sage: re.search(version_regexp, "Sage Version 5.4.2").group()
    '5.4.2'
    sage: re.search(version_regexp, "Sage Version 5.3.1.alpha3").group()
    '5.3.1.alpha3'
    sage: re.search(version_regexp, "Sage Version 5.3.1.alpha3 blah blah").group()
    '5.3.1.alpha3'
    sage: re.search(version_regexp, "Sage Version 5.3.beta2 blah blah").group()
    '5.3.beta2'
    sage: re.search(version_regexp, "Sage Version 5.rc0 blah blah").group()
    '5.rc0'

    sage: re.match(version_regexp, "Sage Version 5.rc0 blah blah")
    sage: re.match(version_regexp, "5.rc0 blah").group()
    '5.rc0'
    sage: re.match(version_regexp+"$", "4_3_3:")
    sage: re.match(version_regexp, "4_3_3:")
    '4_3_3'

"""
version_regexp = '(\d+(\.|_))*(\d+)((\.|_)(alpha|beta|rc)(\d+))?'

def get_sage_version():
    global sage
    # Looking up for sage and sage root directory
    try:
        sage_version = Popen([sage+" --version"], stdout=PIPE, shell=True).communicate()[0]
    except OSError, e:
        error("Could not start sage"+str(e))

    match = re.search(version_regexp, sage_version)
    if match is None:
        error("Cannot determine Sage version number from"+sage_version)
    else:
        version = match.group()
    return version

def encode_sage_version_for_comparison(version):
    """
    Encodes a version of Sage as a list for comparison purposes

    INPUT:

    - ``version`` -- a string

        sage: encode_sage_version_for_comparison("4.2")
        [4, 2, 0]
        sage: encode_sage_version_for_comparison("5.0.beta3")
        [5, 0, -3, 3, 0]
        sage: encode_sage_version_for_comparison("4.2.1.rc0")
        [4, 2, 1, -1, 0, 0]
        sage: encode_sage_version_for_comparison("4.0.alpha3")
        [4, 0, -3, 3, 0]

    The only purpose of this encoding is that Python's standard
    (lexicographic) comparison of lists implements the comparison of
    Sage versions.

    EXAMPLES::

        sage: e = encode_sage_version_for_comparison
        sage: e("4.2") < e("4.2.1")
        True
        sage: e("4.2.1") < e("4.2.2")
        True
        sage: e("4.2.rc0") < e("4.2")
        True
        sage: e("4.7.2.rc0") < e("4.7.2.rc2")
        True
        sage: e("4.7.2.beta2") < e("4.7.2")
        True
        sage: e("4.7.2.beta2") < e("4.7.2.beta3")
        True
        sage: e("4.7.2.beta10") < e("4.7.2.rc0")
        True
    """
    version = version.replace("alpha", "-3.")
    version = version.replace("beta", "-2.")
    version = version.replace("rc", "-1.")
    return [int(s) for s in re.split("\.|_", version)]+[0]

def cmp_sage_versions(version1, version2):
    """
    Compares two Sage versions given as strings

    EXAMPLES::

        sage: cmp_sage_versions("4.2", "4.2.1")
        -1
        sage: cmp_sage_versions("4.2.1", "4.2.2")
        -1
        sage: cmp_sage_versions("4.2.rc0", "4.2")
        -1
        sage: cmp_sage_versions("4.7.2.rc0", "4.7.2.rc2")
        -1
        sage: cmp_sage_versions("4.7.2.beta2", "4.7.2")
        -1
        sage: cmp_sage_versions("4.7.2.beta2", "4.7.2.beta3")
        -1
        sage: cmp_sage_versions("4.7.2.beta10", "4.7.2.rc0")
        -1
        sage: cmp_sage_versions("4.7.2.beta10", "4.7.2.beta10")
        0

    """
    return cmp(encode_sage_version_for_comparison(version1), encode_sage_version_for_comparison(version2))

def cd_to_combinat():
    global sage_combinat_root
    info("Switching to sage combinat root directory: %s"%sage_combinat_root)
    os.chdir(sage_combinat_root)

# TODO: organize this as in sage.misc.sg

def hg_query(command, dir):
    r"""
    Runs hg in the directory dir, and returns its output as a string
    """
    cd_to_combinat() # make sure we are in the sage combinat directory
    return Popen(["cd %s && %s %s"%(dir,hg,command)], stdout=PIPE, shell=True).communicate()[0].rstrip()

def hg_status():
    global sage_combinat_root
    return hg_query("status", sage_combinat_root)

def hg_qstatus():
    global sage_combinat_patch_queue
    return hg_query("status", sage_combinat_patch_queue)

def hg_qtop():
    global sage_combinat_root
    return hg_query("qtop", sage_combinat_root)

def hg_series():
    global sage_combinat_root
    return hg_query("qseries", sage_combinat_root)

def hg_all_patches():
    return re.split("\r\n|\r|\n",hg_series())

def hg_qnext():
    global sage_combinat_root
    return hg_query("qnext", sage_combinat_root)

def hg_all_guards():
    return re.split("\r\n|\r|\n",
                    hg_query("qselect -s", sage_combinat_root))

def hg_active_guards():
    # hg qselect -v returns all the active guards, one per line
    # the first line is always "active guards" or "no active guards"
    return re.split("\r\n|\r|\n",
                    hg_query("qselect -v", sage_combinat_root))[1:]

def hg_are_all_patch_applied():
    return hg_qnext() == "All patches applied"

def hg_are_no_patch_applied():
    return hg_qtop() == "No patches applied"

def check_for_no_diff(check_patch_queue=True):
    if hg_status() != "":
        info("There are local changes; aborting")
        info("Please qrefresh or discard your changes before proceeding")
        info(hg_status())
        exit(1)
    if check_patch_queue and hg_qstatus() != "":
        info("There are uncommited patches:")
        info(hg_qstatus())
        if not options.force:
            info("Use option --force to proceed anyway")
            exit(1)

def qselect_backward_compatibility_patches(guards = []):
    global sage_version, options
    r"""
    Selects the appropriate guards for this version of sage
    e.g. if we are running sage 3.0.2, then we want to apply all
    the patches which are guarded by 3_0_3, 3_0_4, ...
    """

    # FIXME: how to change the guards on a one by one basis

    non_version_guards = filter(lambda guard: re.match(version_regexp, guard) is None, hg_active_guards())
    info("Current non version guards: %s"%" ".join(non_version_guards))

    if options.n:
        non_version_guards = []

    non_version_guards = non_version_guards + guards

    info("Updated non version guards: %s"%" ".join(non_version_guards))

    def is_newversion_guard(guard):
        """
        Returns true if this is a guard for version X (like 4_3_3 or 5_0_beta4) with X > sage_version
        """
        if not re.match(version_regexp+"$", guard):
            if  re.match(version_regexp, guard):
                # Catch erroneous guards like 4_3_3:
                message = "Invalid version guard in the mercurial queue: %s"%guard
                if options.force:
                    warn(message)
                else:
                    error(message)
            return False
        if cmp_sage_versions(guard, sage_version) > 0:
            info("Keep backward compatibility patches for sage "+re.sub("_",".",guard))
            return True
        else:
            info("Skip backward compatibility patches for sage "+re.sub("_",".",guard))
            return False

    version_guards = filter(is_newversion_guard,
                            map(lambda guard: guard[1:], # get rid of leading "+"
                                hg_all_guards()))

    info("Updating guards")
    system(hg+" qselect -q -n")
    system(hg+" qselect "+
           " ".join(non_version_guards + version_guards))

def update(update_from_sage_main = False):
    r"""
    High level operation to update the sage-combinat patches:
     - pop all patches
     - update the patch queue
     - push back the patches
     - rebuild
    """
    check_for_no_diff()

    info("Storing top applied patch")
    if hg_are_all_patch_applied() or hg_are_no_patch_applied():
        qtop = "-a"
    else:
        qtop = hg_qtop()

    info("Unapplying all the patches")
    system(hg+" qpop -a")

    if update_from_sage_main:
        info("Pulling the new version of Sage from the local main repository")
        system(hg+" pull -u ../sage-main")

    info("Pulling the new version of the patches from the patch server")
    system("(cd .hg/patches ; "+hg+" pull -u %s)"%options.server)
    qselect_backward_compatibility_patches()

    # Revert to the formerly applied top patch.
    # If this was the topmost patch, or if this patch does not
    # exist anymore, then apply all patches
    if qtop in hg_all_patches():
        # Revert up to the formerly applied top patch.
        system(hg+" qpush %s"%qtop)
        info("Reapplying the patches up to %s"%qtop)
    else:
        if qtop != "-a":
            info("Warning: the former top patch %s does not exist anymore"%qtop)
        info("Applying all patches")
        system(hg+" qpush -a")

    info("Rebuilding")
    system(sage+" -b %s"%branch)

##############################################################################

sage         = options.sage
hg           = """hg --config 'extensions.hgext.mq=' --config 'ui.username="sage-combinat script"'"""
sage_root    = get_sage_root()
sage_version = get_sage_version()

##############################################################################


if len(args) == 0:
    error("command required")

branch = options.branch

sage_combinat_root = os.path.join(sage_root, "devel", "sage-"+branch)
sage_combinat_hg = os.path.join(sage_combinat_root, ".hg")
sage_combinat_patch_queue = os.path.join(sage_combinat_hg, "patches")

if args[0] == "config":
    info("sage command:                "+sage)
    info("sage version:                "+sage_version)
    info("sage-combinat branch:        "+branch)
    info("sage-combinat server:        "+options.server)
    info("")
    info("sage root:                   "+sage_root)
    info("sage-combinat root:          "+sage_combinat_root)
    info("sage-combinat hg repository: "+sage_combinat_hg)
    info("sage-combinat patch queue:   "+sage_combinat_patch_queue)

elif args[0] == "install":
    if os.path.exists(sage_combinat_root):
        if os.path.exists(sage_combinat_patch_queue):
            error("sage-combinat apparently already installed in %s"%sage_combinat_hg)
        else:
            info("Detected a partial (broken?) installation of sage-combinat in %s"%sage_combinat_root)
            if not options.force:
                info("Use option --force proceed anyway and try to fix it")
                exit(1)
    else:
        info("Creating sage-%s branch:"%branch)
        system(sage+" -b main") # This makes sure we are cloning the main branch!!!
        system(sage+" -clone %s"%branch)
        info("Done")
    assert(os.path.exists(sage_combinat_hg))
    assert(not os.path.exists(sage_combinat_patch_queue))
    check_for_no_diff(check_patch_queue=False)
    info("Uploading sage-combinat patches into .hg/patches:")
    system("cd .hg/; "+hg+" clone %s patches"%options.server)
    qselect_backward_compatibility_patches()
    info("Applying all the patches")
    system(hg+" qpush -a")
    info("Switching to %s branch and rebuilding"%branch)
    system(sage+" -b %s"%branch)
    info("Finished installation of Sage-combinat patches!")

elif args[0] == "update":
    update()

elif args[0] == "upgrade":
    check_for_no_diff()
    info("Upgrading sage")
    system(sage+" -upgrade")
    # Update Sage's version to apply the appropriate patches!
    # (we assume sage's root has not changed)
    sage_version = get_sage_version()
    update(update_from_sage_main = True)

elif args[0] == "status":
    info("Top patch applied: %s"%hg_qtop())
    info("Are all patches applied: %s "%str(hg_are_all_patch_applied()))
    info("Changed files in the sage-combinat directory:")
    info(hg_status())
    info("Changed files in the sage-combinat patch directory:")
    info(hg_qstatus())

elif args[0] == "qselect":
    qselect_backward_compatibility_patches(args[1:])

else:
    error("unknown command "+args[0])
