#!/usr/bin/env python
#
# Copyright (C) 2013,2014,2016-2020
#       Smithsonian Astrophysical Observatory
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

toolname = "combine_spectra"
__revision__ = "09 December 2022"

import sys
import os




import ciao_contrib.logger_wrapper as lw
lw.initialize_logger(toolname)
lgr = lw.get_logger(toolname)
verb0 = lgr.verbose0
verb1 = lgr.verbose1
verb2 = lgr.verbose2
verb3 = lgr.verbose3
verb5 = lgr.verbose5


import pycrates as pc

from collections import namedtuple
FileSet = namedtuple( "FileSet", ["infile","arffile","rmffile"])

import numpy as np

def is_none( infile ):
    """
    Little helper routine to check for None, "none", or blank values
    """
    if infile is None:
        return True
    if infile.strip().lower() in ["none", ""]:
        return True
    return False



class Spectrum():
    """
    Gather all things related to a spectrum

    """
    def __init__( self, infile, arffile, rmffile, exppref="pha" ):

        self.infile = infile
        self.arffile = arffile
        self.rmffile = rmffile

        self.backfile = None
        self.channels = None
        self.counts = None
        self.backscal = None
        self.exposure = None
        self.is_grating = False
        self._load_spectrum( infile, exppref )


    def _load_spectrum( self, infile, exppref ):

        try:
            tab = pc.read_pha( str(infile ))
            tab.__del__()
        except:
            verb1("File {} does not appear to be a fully compliant PHA file, will try to use anyway".format(infile) )

        tab = pc.read_file( str(infile) )
        self.channels = tab.get_column("channel").values.copy()

        if len( self.channels.shape ) != 1:
            raise IOError("File {} may be a Type:II pha file.  These need to be split into Type:I with dmtype2split before they can be combined".format(infile))

        if tab.column_exists( "counts" ):
            self.counts = tab.get_column("counts").values.copy()
        elif tab.column_exists( "rate" ):
            verb0("Combining spectra with a RATE column has not been tested/verified")
            self.counts = tab.get_column("rate").values.copy()
        else:
            raise IOError("Could not find counts nor rate column in file {}".format(infile))

        self.backscal = self._get_from_key_or_column(tab,"backscal")

        if "NONE" == self.arffile:
            self.arffile = None
        elif is_none(self.arffile):
            self.arffile = self._get_filename_if_not_none( infile,tab, "ancrfile", pc.read_file)
        else:
            #make sure can read it
            arf = pc.read_file( self.arffile )
            if not arf.column_exists("specresp"):
                raise IOError("File '{}' does not appear to be an ARF".format(self.arffile))

        if exppref == "pha" or is_none(self.arffile):
            if tab.key_exists("exposure"):
                self.exposure = tab.get_key_value("exposure")
            else:
                raise IOError("The file '{}' is missing the 'EXPOSURE' keyword.".format(infile))
            if exppref == "arf":
                verb2( "Requested exp_origin=arf, but ARF could not be located.  Will use exposure from spectrum file '{}'".format(infile))
        else:  # ARF
            self.exposure = pc.read_file( str(self.arffile)).get_key_value("exposure")


        if "NONE" == self.rmffile:
            self.rmffile = None
        elif is_none(self.rmffile):
            self.rmffile = self._get_filename_if_not_none( infile, tab,"respfile", pc.read_rmf)
        else:
            pc.read_rmf( self.rmffile ) # check readable


        if "NONE" == self.backfile:
            self.backfile = None
        elif is_none(self.backfile):
            self.backfile = self._get_filename_if_not_none(infile, tab, "backfile", pc.read_file )
        else:
            pc.read_file( self.backfile) # check readable


        if tab.column_exists("grouping"):
            verb2("Grouping information in file {} will be ignored".format( infile ))

        if tab.column_exists("stat_err"):
            verb2("Statistical errors in file {} will be recomputed using Gehrel's approximation.".format(infile))

        if tab.key_exists("grating") and tab.get_key_value("grating") in ['HETG','LETG']:
            self.is_grating = True

        tab.__del__()


    def _get_filename_if_not_none( self, infile, tab, key, loader ):
        """
        Lookup the keyword.  If key exists and is not blank or none,
        then try to open the file with the specified 'loader' function.

        If that fails, then get path from infile, and see if the file
        can be found in the same dir as the infile.  Otherwise error out.
        """
        if not tab.key_exists( key ):
            return None

        keyval = tab.get_key_value( key )
        if is_none(keyval):
            return None

        try:
            t2 = loader( str(keyval) )
            return keyval
        except Exception as e :
            try:
                # Take path from infile and try that
                infiledir = os.path.dirname( infile )
                respfile = os.path.basename( keyval )
                if infiledir:
                    t2 = loader( infiledir+os.sep+respfile )
                    return infiledir+os.sep+respfile
                else:
                    t2 = loader( respfile )
                    return respfile
            except:
                verb0("Cannot locate response file {}.  Responses will not be combined.".format(respfile))
                return None


    def _get_from_key_or_column( self, tab, keycol ):
        """
        If value is column, get it.  Else if value is header, get it.  Othewise return 1.0
        """
        if tab.column_exists( keycol):
            return tab.get_column(keycol).values.copy()
        elif tab.key_exists( keycol):
            return np.array( [tab.get_key_value( keycol )]*tab.get_nrows() )
        else:
            verb1("Warning: could not find {} value in file {}; will use 1.0".format(keycol,infile))
            return np.array( [1.0]*tab.get_nrows() )


    def get_backfile(self):
        """
        accessor
        """
        return self.backfile



class Source():

    def __init__( self, src, bkg, exppref ):
        """
        Load src and background files from nameTuple

        """
        self.source = Spectrum( src.infile, src.arffile, src.rmffile, exppref )

        # Special case for L3 files where bkg is extra extension
        bb = self.source.get_backfile()
        bbb = os.path.basename(bb if bb else "")
        ifb = os.path.basename(src.infile)
        if (bbb == ifb) or (bbb == ifb+".gz") or (bbb+".gz" == ifb):
            try:
                pc.read_file(bb+"[SPECTRUM2]")
                bb += "[SPECTRUM2]"
            except:
                raise IOError("Source and background file names are the same {}".format(src.infile))

        # bkg is a FileSet NamedTuple          
        if not is_none( bkg.infile ): 
            # If bkg.infile is set, then use it.
            self.background = Spectrum( bkg.infile, bkg.arffile, bkg.rmffile, exppref )
        elif bkg.infile is None and not is_none( bb ):
            # If bkg.infile is None, then parameter was blank, use BACKFILE keyword
            self.background = Spectrum( bb, bkg.arffile, bkg.rmffile, exppref )
        else:
            # BACKFILE is blank|'none' or bkg_spectra parameter is "none"
            self.background = None



def match_stacks( pars ):
    """
    Make sure we have all  the inputs needed to match up the src w/ its
    response files with the background and their response files.

    """

    import stk as stk

    s_pha = stk.build( pars["src_spectra"] )

    def build_or_none( parname ):
        """
        Check if blank, if so return list of None as long as src_spectra
        """

        if "none" == pars[parname].lower():
            return ["NONE"]*len(s_pha)

        if is_none( pars[parname] ):
            return [None]*len(s_pha)

        ss = stk.build( pars[parname] )
        if len(ss) != len(s_pha):
            raise ValueError("ERROR: Number of {} ({}) does not match the number of source files ({})".format(parname, len(ss), len(s_pha)))
        return ss

    s_arf = build_or_none( "src_arfs")
    s_rmf = build_or_none( "src_rmfs")
    b_pha = build_or_none( "bkg_spectra")
    b_arf = build_or_none( "bkg_arfs")
    b_rmf = build_or_none( "bkg_rmfs")

    srcs = [ FileSet( *v ) for v in zip( s_pha, s_arf, s_rmf )]
    bkgs = [ FileSet( *v ) for v in zip( b_pha, b_arf, b_rmf )]


    return srcs,bkgs



def check_consistency( srcs ):
    """
    Run checks on the data before combining.  Make sure stacks have
    enough elemetns and values are reasonable.
    """

    if len(srcs) < 1:
        raise ValueError("No valid spectra files found")

    chk = [ s.source.arffile for s in srcs ]
    if any(chk) and not all(chk):
        verb0("Not all source spectra have ARFs, they will not be combined")
        for s in srcs: s.source.arffile = None
        for s in srcs: s.source.rmffile = None

    chk = [ s.source.rmffile for s in srcs ]
    if any(chk) and not all(chk):
        verb0("Not all source spectra have RMFs, they will not be combined")
        for s in srcs: s.source.arffile = None
        for s in srcs: s.source.rmffile = None

    chans = srcs[0].source.channels # better be 1 source

    chk = [ len(s.source.channels) == len(chans) for s in srcs ]
    if not all(chk):
        raise ValueError("Channels values do not match in all source files")

    chk = [ (c[1] == chans[c[0]]) for s in srcs for c in enumerate(s.source.channels)]
    if not all(chk):
        raise ValueError("Channels values do not match in all source files")

    for s in srcs:
        if s.background:
            chk = [ s.background.arffile for s in srcs ]
            if any(chk) and not all(chk):
                verb0("Not all background spectra have ARFs, they will not be combined")
                for s in srcs: s.background.arffile = None
                for s in srcs: s.background.rmffile = None

            chk = [ s.background.rmffile for s in srcs ]
            if any(chk) and not all(chk):
                verb0("Not all background spectra have RMFs, they will not be combined")
                for s in srcs: s.background.arffile = None
                for s in srcs: s.background.rmffile = None

            chk = [ s.source.backfile for s in srcs ]
            if any(chk) and not all(chk):
                raise ValueError("Not all source spectra have background")

            chk = [ (len(s.background.channels) == len(chans)) for s in srcs ]
            if not all(chk):
                raise ValueError("Channel values do not match in all background files")

            chk = [ (c[1] == chans[c[0]]) for s in srcs for c in enumerate(s.background.channels)]
            if not all(chk):
                raise ValueError("Channel values do not match in all background files")

    verb1( "Prepared to combine {} spectra\n".format(len(srcs)))

    for s in srcs:
        verb1( "source PHA: {}".format(s.source.infile) )
        verb1( "       ARF: {}".format(s.source.arffile) )
        verb1( "       RMF: {}".format(s.source.rmffile ))
        if s.background:
            verb1( "    background PHA: {}".format(s.background.infile))
            verb1( "               ARF: {}".format(s.background.arffile))
            verb1( "               RMF: {}".format(s.background.rmffile))


def fix_arf_exposure( arfs, outarf, method ):
    """
    The DM wants to recompute the ONTIME/etc when no GTIs are present.

    Need to rest them here
    """
    from pycrates import read_file
    if not outarf or not all(arfs):
        return

    keys_to_fix = ["ONTIME"]
    keys_to_fix.extend( ["ONTIME{}".format(x) for x in range(0,10)] )
    keys_to_fix.extend( ["LIVETIME"] )
    keys_to_fix.extend( ["LIVTIME{}".format(x) for x in range(0,10)] )
    keys_to_fix.extend( ["EXPOSURE"] )
    keys_to_fix.extend( ["EXPOSUR{}".format(x) for x in range(0,10)] )

    # create empty arrays to store the values from input files
    keys = { k : [] for k in keys_to_fix } # map( lambda x: [], keys_to_fix)

    # read values from input arfs
    for aa in arfs:
        tab = read_file( aa )
        for kk in keys:
            if tab.key_exists( kk ):
                keys[kk].append( tab.get_key_value( kk ) )


    tab = read_file(outarf, mode="rw")
    # Update the output file
    for kk in keys:
        if not tab.key_exists(kk):
            continue
        if not keys[kk]:
            continue
        if 'sum' == method:
            val = np.sum( keys[kk] )
        elif 'avg' == method:
            val = np.average( keys[kk] )
        else:
            raise RuntimeError("Unknown option method value '{}'".format(method))

        tab.get_key(kk).value = val

    tab.write()


def fix_rmf_numkeywords(outrmf):
    """
    The NUMGRP and NUMELT keywords should be equal to the sum of 
    the N_GRP and N_CHAN columns, respectively.   It looks like 
    instead they are just copied from the 1st input file.
    
    As these are optional keywords, just delete them.
    """
    
    if "" == outrmf:
        return
    
    # Since RMF files are special: variable length columns and 
    # multiple extensions (EBOUNDS); I don't trust that crates will 
    # open in rw mode will work correctly.  So I'm going to use
    # dmhedit to just modify keywords in-place.

    from pycrates import read_file
    tab=read_file(outrmf, mode="r")
    
    from ciao_contrib.runtool import dmhedit
    dmhedit.punlearn()
    dmhedit.infile=outrmf
    dmhedit.op="delete"

    if tab.get_key("NUMGRP") is not None:
        dmhedit.key="NUMGRP"
        dmhedit()
    
    if tab.get_key("NUMELT") is not None:
        dmhedit.key="NUMELT"
        dmhedit()



def run_addresp( arfs, rmfs, phas, root, method ):
    """
    run addresp to combine ARF and RMF files.
    """
    from ciao_contrib.runtool import addresp

    aa = [is_none(a) for a in arfs ]
    if any( aa ):
        #all or nothing
        return "", ""

    addresp.infile = rmfs if all(rmfs) else ""
    addresp.arffile = arfs if all(arfs) else ""
    addresp.phafile = phas if all(phas) else ""
    addresp.outarf = root+".arf"
    addresp.outfile = root+".rmf"
    addresp.method = method
    addresp.clobber = True
    addresp.type = "rmf"
    addresp()

    # Either arfs or rmfs may not actually be produced based on inputs.
    # easiest way to check is if files exist then use them.

    outrmf = addresp.outfile if os.path.exists( addresp.outfile ) else ""
    outarf = addresp.outarf if os.path.exists( addresp.outarf ) else ""

    fix_arf_exposure( arfs, outarf, method)
    fix_rmf_numkeywords(outrmf)

    return outrmf,outarf


def full_path_or_none( val ):
    """
    Return the full path to a file name.  This is needed since
    the runtool expect file name to be full names when passed in
    as a list (the stack file it creates in /tmp needs full paths
    to find the files)
    """
    if not val:
        return None
    try:
        return os.path.abspath( val )
    except:
        return None


def sum_bkg_counts( srcs, pars ):
    """
    Sum the background counts and compute the combined
    exposure time and BSCALE value.

    The origianl 'asca' method produces COUNTS that
    are no longer integer values.  This doesn't seem to
    upset sherpa (nor crates) but does deviate from the OGIP
    standard.

    The 'time' method produces essentially the same but keeps
    everything as integers.

    The 'arf' method produces results according to the
    Nowak/Huenemoerder memo

    """

    if not srcs[0].background:  # must be one to get this far
        return ""

    def vec_mul( exp, bscl ):
        rv = [ e*b for e,b in zip(exp,bscl)]
        return rv # map( lambda e,b: e*b, exp, bscl )

    cts = np.array([ s.background.counts for s in srcs ])
    out_cts = sum(cts)

    e_s = np.array([ s.source.exposure for s in srcs])
    e_b = np.array([ s.background.exposure for s in srcs])
    b_s = np.array([ s.source.backscal for s in srcs])
    b_b = np.array([ s.background.backscal for s in srcs])

    bratio = b_s/b_b

    # TODO need to check average for bkg
    if pars["method"] == "sum":
        out_exp = sum(e_b) if e_b is not None else None
    else:
        out_exp = np.mean(e_b) if e_b is not None else None

    out_bsc = sum(e_s)/sum( vec_mul(e_s,bratio)) # Original HEASARC value

    c2 = vec_mul( (e_s/e_b), bratio)
    coefs = sum(e_b)/sum(vec_mul( e_s, bratio))*c2
    #coefs = sum(e_b)/sum(e_s*(b_s/b_b))*((e_s/e_b)*(b_s/b_b))


    out_cts = sum([ cts[i]*coefs[i] for i in range(len(cts))] )

    #XXX### maybe be x/0 which is a divide error or 0/0 which is an invalid warning
    #XXX##div_err = np.geterr()['divide']
    #XXX##ign_err = np.geterr()['invalid']
    #XXX##np.seterr(divide= 'ignore')
    #XXX##np.seterr(invalid = 'ignore')
    #XXX##
    #XXX##out_cfa_bsc = out_bsc * ( out_cts / out_cfa_cts )
    #XXX##out_cfa_bsc[ np.isnan( out_cfa_bsc ) ] = 1.0
    #XXX##np.seterr(divide= div_err)
    #XXX##np.seterr(invalid=ign_err)

    # BGD_COUNTS = bkg_counts1 + bkg_counts2 + ... bkg_countsN
    #
    # SOURCE_BACKSCAL = [(src_exp1*src_backscal1) + ... + (src_expN*src_backscalN)] / (src_exp1 + ... + src_expN)
    #
    # BGD_BACKSCAL = [(bkg_exp1*bkg_backscal1) + ... + (bkg_expN*bkg_backscalN)] / (bkg_exp1 + ... + bkg_expN)
    #
    # or since we're free to define however, we want,
    # SOURCE_BACKSCAL = 1.0
    # and
    # BGD_BACKSCAL = ratio of above
    #

    out_cfa_cts = sum(cts)
    out_cfa_bsc2 = (sum( vec_mul(e_b,b_b) )/sum(e_b)) / (sum( vec_mul(e_s,b_s) )/sum(e_s))

    # MIT way
    r1 = np.array(vec_mul(e_s,b_s))/np.array(vec_mul(e_b,b_b))
    m1 = sum([ (cts[i]*r1[i]) for i in range(len(cts))] )
    m2 = sum([ cts[i]*(r1[i]**2) for i in range(len(cts))] )
    m2 = [ x if x > 0 else 1 for x in m2] # no divide by zeros

    out_mit_cts = (m1*m1) / m2
    mit_src_bsc = sum( vec_mul(e_s, b_s) ) / sum(e_s)
    out_mit_bsc = mit_src_bsc*(sum(e_s)/sum(e_b))*m1/m2
    zeros, = np.where(out_mit_bsc == 0 )
    out_mit_bsc[zeros] = 1.0

    phas = [full_path_or_none(s.background.infile) for s in srcs]
    arfs = [full_path_or_none(s.background.arffile) for s in srcs]
    rmfs = [full_path_or_none(s.background.rmffile) for s in srcs]

    outrmf,outarf = run_addresp( arfs, rmfs, phas, pars["outroot"]+"bkg", pars["method"] )

    if srcs[0].background.is_grating:
        outfile = pars["outroot"]+"bkg.pha"
    else:
        outfile = pars["outroot"]+"bkg.pi"

    if pars["bscale_method"] == "time":
        write_spectrum( outfile, phas, srcs[0].source.channels, out_cfa_cts,
            out_exp, out_cfa_bsc2, outrmf, outarf, None )
    elif pars["bscale_method"] == "counts":
        write_spectrum( outfile, phas, srcs[0].source.channels, out_mit_cts,
            out_exp, out_mit_bsc, outrmf, outarf, None )
    else:
        write_spectrum( outfile, phas, srcs[0].source.channels, out_cts,
            out_exp, out_bsc, outrmf, outarf, None )

    return outfile



def sum_src_counts( srcs, pars):
    """
    Sum the source counts.

    The backscale is set = 1.0 and the appropriate ratio is take care of in
    the background bscale (above) except for the MIT method

    """

    cts = np.array([ s.source.counts for s in srcs ])
    out_cts = sum(cts)

    exposure = np.array([ s.source.exposure for s in srcs])
    backscal = np.array([ s.source.backscal for s in srcs])

    if pars["method"] == "sum":
        out_exp = sum(exposure) if exposure is not None else None
    else:
        out_exp = np.mean(exposure) if exposure is not None else None

    if pars["bscale_method"] == "counts":
        eb = [ e*b for e,b in zip(exposure,backscal) ]
        out_bsc = sum(eb) / sum(exposure)

    else:
        out_bsc = np.array( [1.0]*len(out_cts))  # source backscale is set to 1.0


    phas = [full_path_or_none(s.source.infile) for s in srcs]
    arfs = [full_path_or_none(s.source.arffile) for s in srcs]
    rmfs = [full_path_or_none(s.source.rmffile) for s in srcs]

    # combine arf/rmfs

    if srcs[0].source.is_grating == True:
        myroot = pars["outroot"]
        if myroot.endswith("_"):
            myroot=myroot[:-1]

        outrmf,outarf = run_addresp( arfs, rmfs, phas, myroot, pars["method"] )
        outfile = myroot+".pha"
    else:
        outrmf,outarf = run_addresp( arfs, rmfs, phas, pars["outroot"]+"src", pars["method"] )
        outfile = pars["outroot"]+"src.pi"

    # sum background files. We do this here, first so that we get the file name
    # that can be written to the source file header.
    outbkg = sum_bkg_counts( srcs, pars )

    write_spectrum( outfile, phas, srcs[0].source.channels, out_cts, out_exp, out_bsc, outrmf, outarf, outbkg )



def write_spectrum( outfile, phas, chans, out_cts, out_exp, out_bsc, outrmf, outarf, outbkg ):
    """
    Write the output spectrum.

    We use dmmerge to combine the indivudal PHA files so that we get all
    the other header merges applied.

    For sanity, we remove any time and sky subspaces as this would make a
    very mess output file.  We also don't care about the merged data so we
    filter to remove all the rows.  (Just column definitions remain)

    """

    cols2rm = "[cols -"+",-".join(['grouping', 'quality', 'grp_num', 'chans_per_grp', 'grp_data', 'grp_stat_err'])+"]"

    from ciao_contrib.runtool import dmmerge
    inf = [phas[0]+"[subspace -time,-expno,-sky,-pos,-tg_m]"+cols2rm  ]
    if len(phas) > 1:
        inf.extend([p+"[#row=0][subspace -time,-expno,-sky,-pos,-tg_m]"+cols2rm for p in phas[1:] ])
    dmmerge.infile = inf
    dmmerge.outfile = outfile+".tmp"
    dmmerge.clobber=True
    dmmerge()

    # Open temp file and set column/keyword values
    tab = pc.read_file( dmmerge.outfile)
    tab.get_column("channel").values = chans
    tab.get_column("counts").values = out_cts

    if tab.column_exists("stat_err"):
        stat_err = 1.0+np.sqrt( out_cts +0.75)
        etype = tab.get_column("stat_err").values.dtype
        tab.get_column("stat_err").values = stat_err.astype(etype)
        verb1("Using Gehrel's approximation for STAT_ERR values.")

    if tab.key_exists("EXPOSURE"):
        tab.get_key("EXPOSURE").value = out_exp
    if tab.key_exists("LIVETIME"):
        tab.get_key("LIVETIME").value = out_exp

    if 'count_rate' in [x.lower() for x in tab.get_colnames()]:
        tab.get_column("count_rate").values = out_cts / out_exp

    unique_vals = set(out_bsc)
    if 1 == len(unique_vals):
        pc.set_key( tab, "BACKSCAL", out_bsc[0] )
    else:
        try:
            tab.delete_key( "BACKSCAL" )
            tab.delete_column("BACKSCAL")
        except:
            pass
        bc = pc.CrateData()
        bc.name = "BACKSCAL"
        bc.values = out_bsc
        tab.add_column( bc )

    tab.get_key("RESPFILE").value = os.path.basename(outrmf) if outrmf else 'NONE'
    tab.get_key("ANCRFILE").value = os.path.basename(outarf) if outarf else 'NONE'
    tab.get_key("BACKFILE").value = os.path.basename(outbkg) if outbkg else 'none'

    tab.write( outfile, clobber=True)  #clobber check done at the beginning
    tab.__del__()

    os.remove( dmmerge.outfile ) # cleanup temp file



#
# Main Routine
#
@lw.handle_ciao_errors( toolname, __revision__)
def main():
    """
    """
    # get parameters
    from ciao_contrib.param_soaker import get_params
    from ciao_contrib._tools.fileio import outfile_clobber_checks
    from ciao_contrib.runtool import add_tool_history

    pars = get_params(toolname, "rw", sys.argv,
      verbose={"set":lw.set_verbosity, "cmd":verb2},
      musthave=["src_spectra","outroot","src_arfs","src_rmfs",
        "bkg_spectra","bkg_arfs","bkg_rmfs","method","bscale_method",
        "exp_origin","clobber","verbose"])

    srcstk,bkgstk = match_stacks( pars )

    srcs = [ Source(s,b,pars["exp_origin"]) for s,b in zip( srcstk,bkgstk )]

    check_consistency( srcs )

    outroot_is_a_dir = os.path.isdir( pars["outroot"] )
    strip_outroot = True
    dot="."
    if not outroot_is_a_dir:
        pars["outroot"] = pars["outroot"]+"_"
    elif not pars["outroot"].endswith("/"):
        pars["outroot"] = pars["outroot"]+"/"
        dot=""
    else:
        strip_outroot = False

    if srcs[0].source.is_grating == True:
        suffixes = [ dot+"pha", "bkg.pha", dot+"arf", "bkg.arf", dot+"rmf", "bkg.rmf"]
    else:
        suffixes = [ "src.pi", "bkg.pi", "src.arf", "bkg.arf", "src.rmf", "bkg.rmf"]


    for extn in suffixes:
        outfile_clobber_checks( pars["clobber"] , pars["outroot"]+extn )

    sum_src_counts( srcs, pars )

    outroot = pars["outroot"]
    if strip_outroot:
        pars["outroot"] = outroot[:-1]

    verb1( "\nThe following files were created:")
    for extn in suffixes:
        outfile = outroot + extn
        outfile = outfile.replace("_.",".")
                    
        if os.path.exists(outfile):
            add_tool_history( outfile, toolname, pars, __revision__ )
            verb1("  {}".format(outfile))




if __name__ == "__main__":
    try:
        main()
    except Exception as E:
        print("\n# "+toolname+" ("+__revision__+"): ERROR "+str(E)+"\n", file=sys.stderr)
        sys.exit(1)
    sys.exit(0)
