#! /usr/bin/env python3

"""\
Run Rivet analyses on HepMC events read from a file or Unix pipe

Examples:

  %(prog)s [options] <hepmcfile> [<hepmcfile2> ...]

  or

  mkfifo fifo.hepmc
  my_generator -o fifo.hepmc &
  %(prog)s [options] fifo.hepmc


Environment variables:

 * RIVET_ANALYSIS_PATH: list of paths to be searched for plugin analysis libraries at runtime
 * RIVET_DATA_PATH: list of paths to be searched for data files (defaults to use analysis path)
 * RIVET_REF_PATH: list of paths to be searched for reference data files (defaults to use data path)
 * RIVET_INFO_PATH: list of paths to be searched for analysis info files (defaults to use data path)
 * RIVET_PLOT_PATH: list of paths to be searched for plot-style files (defaults to use data path)
 * RIVET_ANALYSIS_PLUGINS: list of paths to be searched for data files (defaults to use data path)
 * RIVET_RANDOM_SEED: force the random seed used by the detector smearing system
 * RIVET_STRIP_HEPMC: reduce the complexity of HepMC events before analysis (experimental)
"""

import os, sys

## Load the rivet module
try:
    import rivet
except Exception as e:
    sys.stderr.write("The rivet Python module could not be loaded: are your PYTHONPATH and (DY)LD_LIBRARY_PATH set correctly?\n")
    sys.stderr.write("Try running 'python -c \"import rivet\"' at the command line (or \"import rivet\" in a Python REPL)\n")
    sys.stderr.write("Full error message:\n{}\n".format(e))
    sys.exit(5)

rivet.util.set_process_name("rivet")

import time, datetime, logging, signal

## Parse command line options
import argparse
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("ARGS", nargs="*")
parser.add_argument("--version", dest="SHOW_VERSION", action="store_true", default=False, help="show Rivet version")

anagroup = parser.add_argument_group("Analysis handling")
anagroup.add_argument("-a", "--analysis", "--analyses", dest="ANALYSES", action="append",
                      default=[], metavar="ANA",
                      help="add an analysis (or comma-separated list of analyses) to the processing list.")
anagroup.add_argument("--list-analyses", "--list", dest="LIST_ANALYSES", action="store_true",
                      default=False, help="show the list of available analyses' names. With -v, it shows the descriptions, too")
anagroup.add_argument("--list-keywords", "--keywords", dest="LIST_KEYWORDS", action="store_true",
                      default=False, help="show the list of available keywords.")
anagroup.add_argument("--list-used-analyses", action="store_true", dest="LIST_USED_ANALYSES",
                      default=False, help="list the analyses used by this command (after subtraction of inappropriate ones)")
anagroup.add_argument("--show-analysis", "--show-analyses", "--show", dest="SHOW_ANALYSES", action="append",
                      default=[], help="show the details of an analysis")
anagroup.add_argument("--show-bibtex", dest="SHOW_BIBTEX", action="store_true",
                      default=False, help="show BibTeX entries for all used analyses")
anagroup.add_argument("--analysis-path", dest="ANALYSIS_PATH", metavar="PATH", default=None,
                      help="specify the analysis search path (cf. $RIVET_ANALYSIS_PATH).")
# TODO: remove/deprecate the append?
anagroup.add_argument("--analysis-path-append", dest="ANALYSIS_PATH_APPEND", metavar="PATH", default=None,
                      help="append to the analysis search path (cf. $RIVET_ANALYSIS_PATH).")
anagroup.add_argument("--pwd", dest="ANALYSIS_PATH_PWD", action="store_true", default=False,
                      help="append the current directory (pwd) to the analysis/data search paths (cf. $RIVET_ANALYSIS_PATH).")
anagroup.add_argument("--print-projection-tree", dest="DEBUG_PROJ_TREE", action="store_true", default=False,
                      help="Plot the projection tree for this set of analyses (in addition to running rivet normally")
anagroup.add_argument("--no-detex", dest="DO_DETEX", action="store_false", default=True,
                      help="do not perform replacement of LaTeX symbols with Unicode when printing to terminal")

extragroup = parser.add_argument_group("Extra run settings")
extragroup.add_argument("-o", "-H", "--histo-file", dest="HISTOFILE",
                        default="Rivet.yoda", help="specify the output histo file path (default = %(default)s)")
extragroup.add_argument("-p", "--preload-file", "--preload-files", dest="PRELOADFILES", action="append", default=[],
                        help="specify a pre-existing YODA file (or comma-separated list of files)"
                        "to read in, for initializing the histograms (default = none)")
extragroup.add_argument("--no-histo-file", dest="WRITE_DATA", action="store_false", default=True,
                        help="don't write out any histogram file at the end of the run (default = write)")
extragroup.add_argument("-x", "--cross-section", dest="CROSS_SECTION",
                        default=None, metavar="XS",
                        help="specify the signal process cross-section in pb")
extragroup.add_argument("-n", "--nevts", dest="MAXEVTNUM", type=int,
                        default=None, metavar="NUM",
                        help="restrict the max number of events to process")
extragroup.add_argument("--nskip", dest="EVTSKIPNUM", type=int,
                        default=0, metavar="NUM",
                        help="skip NUM events read from input before beginning processing")
extragroup.add_argument("--nlo-smearing", dest="NLO_SMEARING", type=float, default=0.0,
                        help="If larger than zero, smear histogram fillings to avoid that "
                        + "NLO counter events end up on the other side of a bin-boundary "
                        + "as compared to the corresponding real event in a fill. The fills "
                        + "will be smeared over the given fraction of the bin width.")
extragroup.add_argument("--ignore-beams", dest="IGNORE_BEAMS", action="store_true", default=False,
                        help="ignore input event beams when checking analysis compatibility. "
                        "WARNING: analyses may not work correctly, or at all, with inappropriate beams")
extragroup.add_argument("-d", "--dump", "--histo-interval", dest="DUMP_PERIOD", type=int,
                        default=1000, metavar="NUM",
                        help="specify the number of events between histogram file updates, "
                        "default = %(default)s. Set to 0 to only write out at the end of the run. "
                        "Note that intermediate histograms will be those from the analyze step "
                        "only, except for analyses explicitly declared Reentrant for which the "
                        "finalize function is executed first.")
extragroup.add_argument("--bootstrapfile", dest="BOOTSTRAP_FILE", default=None,
                        help="set the file name for a boostrap file. Note: This file grows in size quickly!")

weightsgroup = parser.add_argument_group("Weight-stream settings")
weightsgroup.add_argument("--weight-skip", "--skip-weights", "--skip-multiweights", dest="SKIP_WEIGHTS", action="store_true",
                          default=False, help="only run on the nominal weight")
weightsgroup.add_argument("--weight-match", "--match-weights", dest="MATCH_WEIGHTS", default="",
                          help="select weight variations matching this comma-separated list of weight names "
                          "(supports regular expressions). Nominal weight cannot be skipped.")
weightsgroup.add_argument("--weight-unmatch", "--unmatch-weights", dest="UNMATCH_WEIGHTS", default="",
                          help="deselect weight variations matching this comma-separated list of weight names "
                          "(supports regular expressions). Nominal weight cannot be skipped.")
weightsgroup.add_argument("--weight-nominal", "--weight-default", "--nominal-weight", "--default-weight", dest="NOMINAL_WEIGHT", default="",
                          help="specify the nominal weight name from the variations ")
weightsgroup.add_argument("--weight-cap", dest="WEIGHT_CAP", type=float,
                          default=None, help="set maximum abs(weight) to be used for histogram fills")

timinggroup = parser.add_argument_group("Timeouts and periodic operations")
timinggroup.add_argument("--event-timeout", dest="EVENT_TIMEOUT", type=int,
                         default=21600, metavar="NSECS",
                         help="max time in whole seconds to wait for an event to be generated from the specified source (default = %(default)s)")
timinggroup.add_argument("--run-timeout", dest="RUN_TIMEOUT", type=int,
                         default=None, metavar="NSECS",
                         help="max time in whole seconds to wait for the run to finish. This can be useful on batch systems such "
                         "as the LCG Grid where tokens expire on a fixed wall-clock and can render long Rivet runs unable to write "
                         "out the final histogram file (default = unlimited)")

verbgroup = parser.add_argument_group("Verbosity control")
parser.add_argument("-l", dest="NATIVE_LOG_STRS", action="append",
                    default=[], help="set a log level in the Rivet library")
verbgroup.add_argument("-v", "--verbose", action="store_const", const=logging.DEBUG, dest="LOGLEVEL",
                       default=logging.INFO, help="print debug (very verbose) messages")
verbgroup.add_argument("-q", "--quiet", action="store_const", const=logging.WARNING, dest="LOGLEVEL",
                       default=logging.INFO, help="be very quiet")
verbgroup.add_argument("--silent", action="store_const", const=logging.CRITICAL, dest="LOGLEVEL",
                       default=logging.INFO, help="minimal verbosity (critical messages only)")

args = parser.parse_args()


## Print the version and exit
if args.SHOW_VERSION:
    print("rivet v%s" % rivet.version())
    sys.exit(0)

## Override/modify analysis search path
if args.ANALYSIS_PATH:
    rivet.setAnalysisLibPaths(args.ANALYSIS_PATH.split(":"))
    rivet.setAnalysisDataPaths(args.ANALYSIS_PATH.split(":"))
if args.ANALYSIS_PATH_APPEND:
    for ap in args.ANALYSIS_PATH_APPEND.split(":"):
        rivet.addAnalysisLibPath(ap)
        rivet.addAnalysisDataPath(ap)
if args.ANALYSIS_PATH_PWD:
    rivet.addAnalysisLibPath(os.path.abspath("."))
    rivet.addAnalysisDataPath(os.path.abspath("."))


## Configure logging
logging.basicConfig(level=args.LOGLEVEL, format="%(message)s")
if not args.NATIVE_LOG_STRS:
    # find the key in rivet.LEVELS corresponding to the value of args.LOGLEVEL
    l = list(rivet.LEVELS.keys())[list(rivet.LEVELS.values()).index(args.LOGLEVEL)]
    args.NATIVE_LOG_STRS.append("Rivet=%s" % l)
for l in args.NATIVE_LOG_STRS:
    name, level = None, None
    try:
        name, level = l.split("=")
    except:
        name = "Rivet"
        level = l
    ## Fix name
    if name != "Rivet" and not name.startswith("Rivet."):
        name = "Rivet." + name
    try:
        ## Get right error type
        level = rivet.LEVELS.get(level.upper(), None)
        logging.debug("Setting log level: %s %d" % (name, level))
        rivet.setLogLevel(name, level)
    except:
        logging.warning("Couldn't process logging string '%s'" % l)



############################
## Listing available analyses/keywords


def getAnalysesByKeyword(alist, kstring):
    add, veto, ret = [], [], []
    bits = [i for i in kstring.replace("^@", "@^").split("@") if len(i) > 0]
    for b in bits:
        if b.startswith("^"):
            veto.append(b.strip("^"))
        else:
            add.append(b)

    add = set(add)
    veto = set(veto)

    for a in alist:
        kwds = set([i.lower() for i in rivet.AnalysisLoader.getAnalysis(a).keywords()])
        if kwds.isdisjoint(veto) and add.issubset(kwds):
            ret.append(a)
    return ret


## List of analyses
all_analyses = rivet.AnalysisLoader.analysisNames() #< excludes aliases
if args.LIST_ANALYSES:
    ## Treat args as case-insensitive regexes if present
    regexes = None
    if args.ARGS:
        import re
        regexes = [re.compile(arg, re.I) for arg in args.ARGS]
    # import tempfile, subprocess
    # tf, tfpath = tempfile.mkstemp(prefix="rivet-list.")
    names = []
    msg = []
    for aname in all_analyses:
        if not regexes:
            toshow = True
        else:
            toshow = False
            for regex in regexes:
                if regex.search(aname):
                    toshow = True
                    break
        if toshow:
            names.append(aname)
            msg.append('')
            if args.LOGLEVEL <= logging.INFO:
                a = rivet.AnalysisLoader.getAnalysis(aname)
                st = "" if "VALIDATED" in a.status() else "[UNVALIDATED] " #("[%s] " % a.status())
                # detex() introduces non-ASCII chars from Greek names in analysis titles.
                msg[-1] = r"%s%s" % (st, a.summary())
                if args.LOGLEVEL < logging.INFO:
                    if a.keywords():
                        msg[-1] += r"  [%s]" % " ".join(a.keywords())
                    if a.luminosityfb():
                        msg[-1] += r"  [ \\int L = %s fb^{-1} ]" % a.luminosityfb()
    if args.DO_DETEX:
        msg = rivet.util.detex(msg)
    retlist = '\n'.join([ r"%-25s   %s" % (a,m) for a,m in zip(names,msg) ])
    print(retlist)
    sys.exit(0)


def getKeywords(alist):
    all_keywords = []
    for a in alist:
        all_keywords.extend(rivet.AnalysisLoader.getAnalysis(a).keywords())
    all_keywords = [i.lower() for i in all_keywords]
    return sorted(list(set(all_keywords)))


## List keywords
if args.LIST_KEYWORDS:
    # a = rivet.AnalysisLoader.getAnalysis(aname)
    for k in getKeywords(all_analyses):
        print(k)
    sys.exit(0)


## Show analyses' details
if len(args.SHOW_ANALYSES) > 0:
    toshow = []
    for i, a in enumerate(args.SHOW_ANALYSES):
        a_up = a.upper()
        if a_up in all_analyses and a_up not in toshow:
            toshow.append(a_up)
        else:
            ## Treat as a case-insensitive regex
            import re
            regex = re.compile(a, re.I)
            for ana in all_analyses:
                if regex.search(ana) and a_up not in toshow:
                    toshow.append(ana)

    msgs = []
    for i, name in enumerate(sorted(toshow)):
        import textwrap
        ana = rivet.AnalysisLoader.getAnalysis(name)

        msg = ""
        msg += name + "\n"
        msg += (len(name) * "=") + "\n\n"
        summ = ana.summary()
        msg += (rivet.util.detex(summ) if args.DO_DETEX else summ) + "\n\n"
        msg += "Status: " + ana.status() + "\n"
        msg += "URL: https://rivet.hepforge.org/analyses/{}.html".format(name) + "\n"
        msg += "\n"

        # TODO: reduce to only show Inspire in v3
        if ana.inspireID():
            msg += "Inspire ID: " + ana.inspireID() + "\n"
            msg += "Inspire URL: https://inspirehep.net/literature/" + ana.inspireID() + "\n"
            msg += "HepData URL: https://hepdata.net/record/ins" + ana.inspireID() + "\n"
        elif ana.spiresID():
            msg += "Spires ID: " + ana.spiresID() + "\n"
            msg += "Inspire URL: https://inspirehep.net/literature?q=" + ana.spiresID() + "\n"
            msg += "HepData URL: https://hepdata.cedar.ac.uk/view/irn" + ana.spiresID() + "\n"


        if ana.year():
            msg += "Year of publication: " + ana.year() + "\n"
        if ana.bibKey():
            msg += "BibTeX key: " + ana.bibKey() + "\n"

        msg += "Authors:\n"
        for a in ana.authors():
            msg += "  " + a + "\n"
        msg += "\n"

        msg += "Description:\n"
        twrap = textwrap.TextWrapper(width=75, initial_indent=2*" ", subsequent_indent=2*" ")
        desc = ana.description()
        msg += twrap.fill(rivet.util.detex(desc) if args.DO_DETEX else desc) + "\n\n"

        if ana.experiment():
            msg += "Experiment: " + ana.experiment()
            if ana.collider():
                msg += "(%s)" % ana.collider()
            msg += "\n"
        # TODO: move this formatting into Analysis or a helper function?
        if ana.requiredBeamIDs():
            def pid_to_str(pid):
                if pid == 11:
                    return "e-"
                elif pid == -11:
                    return "e+"
                elif pid == 2212:
                    return "p+"
                elif pid == -2212:
                    return "p-"
                elif pid == 1000010020:
                    return "d"
                elif pid == 1000130270:
                    return "Al"
                elif pid == 1000290630:
                    return "Cu"
                elif pid == 1000541290:
                    return "Xe"
                elif pid == 1000791970:
                    return "Au"
                elif pid == 1000822080:
                    return "Pb"
                elif pid == 1000922380:
                    return "U"
                elif pid == 10000:
                    return "*"
                else:
                    return str(pid)
            beamstrs = []
            for bp in ana.requiredBeamIDs():
                beamstrs.append(pid_to_str(bp[0]) + " " + pid_to_str(bp[1]))
            msg += "Beams: " + ", ".join(beamstrs) + "\n"

        if ana.requiredBeamEnergies():
            msg += "Beam energies: " + "; ".join(["(%0.1f, %0.1f) GeV\n" % (epair[0], epair[1]) for epair in ana.requiredBeamEnergies()])
        else:
            msg += "Beam energies: ANY\n"

        if ana.runInfo():
            msg += "Run details:\n"
            twrap = textwrap.TextWrapper(width=75, initial_indent=2*" ", subsequent_indent=4*" ")
            for l in ana.runInfo().split("\n"):
                msg += twrap.fill(l) + "\n"

        if ana.luminosityfb():
            msg += "\nIntegrated data luminosity = %s inverse fb.\n"%ana.luminosityfb()

        if ana.keywords():
            msg += "\nAnalysis keywords:"
            for k in ana.keywords():
                msg += " %s"%k
            msg += "\n\n"

        if ana.references():
            msg += "\n" + "References:\n"
            for r in ana.references():
                url = None
                if r.startswith("arXiv:"):
                    code = r.split()[0].replace("arXiv:", "")
                    url = "http://arxiv.org/abs/" + code
                elif r.startswith("doi:"):
                    code = r.replace("doi:", "")
                    url = "http://dx.doi.org/" + code
                if url is not None:
                    r += " - " + url
                msg += "  " + r + "\n"

        ## Add to the output
        msgs.append(msg)

    ## Write the combined messages to a temporary file and page it
    if msgs:
        try:
            import tempfile, subprocess
            tffd, tfpath = tempfile.mkstemp(prefix="rivet-show.")
            msgsum = r"\n\n".join(msgs)
            msgsum = msgsum.encode('utf-8')
            os.write(tffd, msgsum)
            if sys.stdout.isatty():
                pager = subprocess.Popen(["less", "-FX", tfpath]) #, stdin=subprocess.PIPE)
                pager.communicate()
            else:
                f = open(tfpath)
                print(f.read())
                f.close()
        finally:
            os.unlink(tfpath) #< always clean up
    sys.exit(0)



############################
## Actual analysis runs



## We allow comma-separated lists of analysis names -- normalise the list here
newanas = []
for a in args.ANALYSES:
    if "," in a:
        newanas += a.split(",")
    elif "@" in a: #< NB. this bans combination of ana lists and keywords in a single arg
        temp = getAnalysesByKeyword(all_analyses, a)
        for i in temp:
            newanas.append(i)
    else:
        newanas.append(a)
args.ANALYSES = newanas


## Parse supplied cross-section
if args.CROSS_SECTION is not None:
    xsstr = args.CROSS_SECTION
    try:
        args.CROSS_SECTION = float(xsstr)
    except:
        import re
        suffmatch = re.search(r"[^\d.]", xsstr)
        if not suffmatch:
            raise ValueError("Bad cross-section string: %s" % xsstr)
        factor = base = None
        suffstart = suffmatch.start()
        if suffstart != -1:
            base = xsstr[:suffstart]
            suffix = xsstr[suffstart:].lower()
            if suffix == "mb":
                factor = 1e+9
            elif suffix == "mub":
                factor = 1e+6
            elif suffix == "nb":
                factor = 1e+3
            elif suffix == "pb":
                factor = 1
            elif suffix == "fb":
                factor = 1e-3
            elif suffix == "ab":
                factor = 1e-6
        if factor is None or base is None:
            raise ValueError("Bad cross-section string: %s" % xsstr)
        xs = float(base) * factor
        args.CROSS_SECTION = xs


## Print the available CLI options!
#if args.LIST_OPTIONS:
#    for o in parser.option_list:
#        print(o.get_opt_string())
#    sys.exit(0)


## Set up signal handling
RECVD_KILL_SIGNAL = None
def handleKillSignal(signum, frame):
    "Declare us as having been signalled, and return to default handling behaviour"
    global RECVD_KILL_SIGNAL
    logging.critical("Signal handler called with signal " + str(signum))
    RECVD_KILL_SIGNAL = signum
    signal.signal(signum, signal.SIG_DFL)
## Signals to handle
signal.signal(signal.SIGTERM, handleKillSignal);
signal.signal(signal.SIGHUP,  handleKillSignal);
signal.signal(signal.SIGINT,  handleKillSignal);
signal.signal(signal.SIGUSR1, handleKillSignal);
signal.signal(signal.SIGUSR2, handleKillSignal);
try:
    signal.signal(signal.SIGXCPU, handleKillSignal);
except:
    pass


## Identify HepMC files/streams
## TODO: check readability, deal with stdin
if len(args.ARGS) > 0:
    HEPMCFILES = args.ARGS
else:
    HEPMCFILES = ["-"]


## Event number logging
def logNEvt(n, starttime, maxevtnum):
    if n % 10000 == 0:
        nevtloglevel = logging.CRITICAL
    elif n % 1000 == 0:
        nevtloglevel = logging.WARNING
    elif n % 100 == 0:
        nevtloglevel = logging.INFO
    else:
        nevtloglevel = logging.DEBUG

    if logging.getLogger().getEffectiveLevel() > nevtloglevel:
        return

    currenttime = datetime.datetime.now().replace(microsecond=0)
    elapsedtime = currenttime - starttime
    msg = "Event {:d} ({} elapsed)".format(n, str(elapsedtime))

    # if maxevtnum is None:
    #     logging.log(nevtloglevel, "Event %d (%s elapsed)" % (n, str(elapsedtime)))
    # else:
    #     remainingtime = (maxevtnum-n) * elapsedtime.total_seconds() / float(n)
    #     eta = time.strftime("%a %b %d %H:%M", datetime.localtime(currenttime + remainingtime))
    #     logging.log(nevtloglevel, "Event %d (%d s elapsed / %d s left) -> ETA: %s" %
    #                 (n, elapsedtime, remainingtime, eta))

    if sys.stdout.isatty() and nevtloglevel <= logging.INFO:
        print(msg, end="\r")
        sys.stdout.flush()
    else:
        logging.log(nevtloglevel, msg)


## Do some checks on output histo file, before we start the event loop
histo_parentdir = os.path.dirname(os.path.abspath(args.HISTOFILE))
if not os.path.exists(histo_parentdir):
  logging.error('Parent path of output histogram file does not exist: %s\nExiting.' % histo_parentdir)
  sys.exit(4)
if not os.access(histo_parentdir,os.W_OK):
  logging.error('Insufficient permissions to write output histogram file to directory %s\nExiting.' % histo_parentdir)
  sys.exit(4)


## Set up analysis handler
ah = rivet.AnalysisHandler()
ah.setIgnoreBeams(args.IGNORE_BEAMS)
ah.skipMultiWeights(args.SKIP_WEIGHTS)
ah.matchWeightNames(args.MATCH_WEIGHTS)
ah.unmatchWeightNames(args.UNMATCH_WEIGHTS)
ah.setNominalWeightName(args.NOMINAL_WEIGHT)
if args.WEIGHT_CAP is not None:
    ah.setWeightCap(args.WEIGHT_CAP)

ah.setNLOSmearing(args.NLO_SMEARING)

for a in args.ANALYSES:
    ## Print warning message and exit if not a valid analysis name
    if not rivet.stripOptions(a) in rivet.AnalysisLoader.allAnalysisNames():
        logging.warning("'%s' is not a known Rivet analysis! Do you need to set RIVET_ANALYSIS_PATH or use the --pwd switch?\n" % a)
        # TODO: lay out more neatly, or even try for a "did you mean XXXX?" heuristic?
        logging.warning("There are %d currently available analyses:\n" % len(all_analyses) + ", ".join(all_analyses))
        sys.exit(1)
    logging.debug("Adding analysis '%s'" % a)
    ah.addAnalysis(a)

for plfarg in args.PRELOADFILES:
    for plf in plfarg.split(","):
        ah.readData(plf)

if args.DUMP_PERIOD:
    ah.setFinalizePeriod(args.HISTOFILE, args.DUMP_PERIOD)

if args.BOOTSTRAP_FILE:
    ah.setBootstrapFilename(args.BOOTSTRAP_FILE)

if args.SHOW_BIBTEX:
    bibs = []
    for aname in sorted(ah.analysisNames()):
        ana = rivet.AnalysisLoader.getAnalysis(aname)
        bibs.append("% " + aname + "\n" + ana.bibTeX())
    if bibs:
        print("\nBibTeX for used Rivet analyses:\n")
        print("% --------------------------\n")
        print("\n\n".join(bibs) + "\n")
        print("% --------------------------\n")


## Read and process events
run = rivet.Run(ah)
if args.CROSS_SECTION is not None:
    logging.info("User-supplied cross-section = %e pb" % args.CROSS_SECTION)
    run.setCrossSection(args.CROSS_SECTION)
if args.LIST_USED_ANALYSES is not None:
    run.setListAnalyses(args.LIST_USED_ANALYSES)

## Print platform type
import platform
starttime = datetime.datetime.now().replace(microsecond=0)
logging.info("Rivet %s running on machine %s (%s) at %s" % \
             (rivet.version(), platform.node(), platform.machine(), str(starttime)))


def min_nonnull(a, b):
    "A version of min which considers None to always be greater than a real number"
    if a is None: return b
    if b is None: return a
    return min(a, b)

## Set up an event timeout handler
class TimeoutException(Exception):
    pass
if args.EVENT_TIMEOUT or args.RUN_TIMEOUT:
    def evttimeouthandler(signum, frame):
        logging.warn("It has taken more than %d secs to get an event! Is the input event stream working?" %
                     min_nonnull(args.EVENT_TIMEOUT, args.RUN_TIMEOUT))
        raise TimeoutException("Event timeout")
    signal.signal(signal.SIGALRM, evttimeouthandler)


## Init run based on one event
hepmcfile = HEPMCFILES[0]
## Apply a file-level weight derived from the filename
hepmcfileweight = 1.0
if ":" in hepmcfile:
    hepmcfile, hepmcfileweight = hepmcfile.rsplit(":", 1)
    hepmcfileweight = float(hepmcfileweight)
try:
    if args.EVENT_TIMEOUT or args.RUN_TIMEOUT:
        signal.alarm(min_nonnull(args.EVENT_TIMEOUT, args.RUN_TIMEOUT))
        init_ok = run.init(hepmcfile, hepmcfileweight)
    signal.alarm(0)
    if not init_ok:
        logging.error("Failed to initialise using event file '%s'... exiting" % hepmcfile)
        sys.exit(2)
except TimeoutException as te:
    logging.error("Timeout in initialisation from event file '%s'... exiting" % hepmcfile)
    sys.exit(3)
except Exception as ex:
    logging.warning("Could not read from '%s' (error=%s)" % (hepmcfile, str(ex)))
    sys.exit(3)

if args.DEBUG_PROJ_TREE:
    ptg=rivet.ProjectionTreeGenerator()
    ptg.setPath("RivetProjectionTree.gv")
    ptg.getProjTreeFromAnalysisHandler(ah)
    ptg.writeProjTree()

## Event loop
old_evtnum = 0
for fileidx, hepmcfile in enumerate(HEPMCFILES):
    ## Apply a file-level weight derived from the filename
    hepmcfileweight = 1.0
    if ":" in hepmcfile:
        hepmcfile, hepmcfileweight = hepmcfile.rsplit(":", 1)
        hepmcfileweight = float(hepmcfileweight)

    ## Open next HepMC file (NB. this doesn't apply to the first file: it was already used for the run init)
    if fileidx > 0:
        try:
            run.openFile(hepmcfile, hepmcfileweight)
        except Exception as ex:
            logging.warning("Could not read from '%s' (error=%s)" % (hepmcfile, ex))
            continue
        if not run.readEvent():
            logging.warning("Could not read events from '%s'" % hepmcfile)
            continue

    ## Announce new file
    msg = "\nReading events from '%s'" % hepmcfile
    if hepmcfileweight != 1.0:
        msg += " (file weight = %e)" % hepmcfileweight
    logging.info(msg)

    ## The event loop
    while args.MAXEVTNUM is None or old_evtnum-args.EVTSKIPNUM < args.MAXEVTNUM:
        evtnum = run.numEvents() # only updates when event number changes

        ## Optional event skipping
        if evtnum <= args.EVTSKIPNUM:
            if evtnum != old_evtnum:
              old_evtnum = evtnum
              if evtnum % 1000 == 0:
                  logging.info("Skipping event #%i" % evtnum)
            logging.debug("Skipping event #%i" % evtnum)
            run.readEvent();
            continue

        ## Only log the event number once we're actually processing
        if evtnum != old_evtnum:
          old_evtnum = evtnum
          logNEvt(evtnum, starttime, args.MAXEVTNUM)

        ## Process this event
        processed_ok = run.processEvent()
        if not processed_ok:
            logging.warn("Event processing failed for evt #%i!" % evtnum)
            break

        ## Set flag to exit event loop if run timeout exceeded
        if args.RUN_TIMEOUT and (time.time() - starttime) > args.RUN_TIMEOUT:
            logging.warning("Run timeout of %d secs exceeded... exiting gracefully" % args.RUN_TIMEOUT)
            RECVD_KILL_SIGNAL = True

        ## Exit the loop if signalled
        if RECVD_KILL_SIGNAL is not None:
            break

        ## Read next event (with timeout handling if requested)
        try:
            if args.EVENT_TIMEOUT:
                signal.alarm(args.EVENT_TIMEOUT)
            read_ok = run.readEvent()
            signal.alarm(0)
            if not read_ok:
                break
        except TimeoutException as te:
            logging.error("Timeout in reading event from '%s'... exiting" % hepmcfile)
            sys.exit(3)

## Print end-of-loop messages
loopendtime = datetime.datetime.now().replace(microsecond=0)
logging.info("\nFinished event loop at %s" % str(loopendtime))
logging.info("Cross-section = %e pb" % ah.nominalCrossSection())

## Finalize and write out data file
run.finalize()
if args.WRITE_DATA:
    ah.writeData(args.HISTOFILE)

## Print end-of-run messages
endtime = datetime.datetime.now().replace(microsecond=0)
logging.info("\nRivet run completed at %s, time elapsed = %s" % (str(endtime), str(endtime-starttime)))
logging.info("Histograms written to %s" % os.path.abspath(args.HISTOFILE))
