#!/usr/bin/env python
#
#  Copyright (C) 2016-2024
#            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__ = "blanksky"
__revision__  = "18 March 2024"


import os
import sys
import shutil
import tempfile
import paramio
import stk
import pycrates as pcr

from ciao_contrib.logger_wrapper import initialize_logger, make_verbose_level, set_verbosity, handle_ciao_errors
from ciao_contrib.param_wrapper import open_param_file

from ciao_contrib._tools import fluximage, utils, fileio, obsinfo #, bands
from ciao_contrib.runtool import make_tool, add_tool_history, new_pfiles_environment
from ciao_contrib.proptools import dates

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

# Set up the logging/verbose code
initialize_logger(__toolname__)

# Use v<n> to display messages at the given verbose level.
v0 = make_verbose_level(__toolname__, 0)
v1 = make_verbose_level(__toolname__, 1)
v2 = make_verbose_level(__toolname__, 2)
v3 = make_verbose_level(__toolname__, 3)
v4 = make_verbose_level(__toolname__, 4)
v5 = make_verbose_level(__toolname__, 5)


class ScriptError(RuntimeError):
    """Error found during running the script. This class is introduced
    in case there is a need to catch such an error and deal with it
    appropriately (e.g. recognize it as distinct from an error raised
    by the code).
    """
    pass


#########################################################################################
#
# suppress warnings printed to screen from fluximage.blanksky_hrci when probing for
# HRC-I background files
# http://stackoverflow.com/questions/11130156/suppress-stdout-stderr-print-from-python-functions
#
#########################################################################################
class suppress_stdout_stderr(object):
    '''
    A context manager for doing a "deep suppression" of stdout and stderr in
    Python, i.e. will suppress all print, even if the print originates in a
    compiled C/Fortran sub-function.
       This will not suppress raised exceptions, since exceptions are printed
    to stderr just before a script exits, and after the context manager has
    exited (at least, I think that is why it lets exceptions through).
    '''
    #import stat

    def __init__(self):
        # Open a pair of null files
        self.null_fds =  [os.open(os.devnull,os.O_RDWR) for x in range(2)]
        # Save the actual stdout (1) and stderr (2) file descriptors.
        self.save_fds = (os.dup(1), os.dup(2))

    def __enter__(self):
        # Assign the null pointers to stdout and stderr.
        os.dup2(self.null_fds[0],1)
        os.dup2(self.null_fds[1],2)

    def __exit__(self, *_):
        # Re-assign the real stdout/stderr back to (1) and (2)
        os.dup2(self.save_fds[0],1)
        os.dup2(self.save_fds[1],2)
        # Close the null files
        os.close(self.null_fds[0])
        os.close(self.null_fds[1])


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


def caldb_blanksky(detector):
    """Check if CALDB blank-sky maps are installed"""
    caldb = os.environ["CALDB"]

    if os.path.isdir(f"{caldb}/data/chandra"):
        if detector == "HRC":
            bkg_path = f"{caldb}/data/chandra/hrc/bkgrnd"
        elif detector ==  "ACIS":
            bkg_path = f"{caldb}/data/chandra/acis/bkgrnd"
    else:
        raise ScriptError("Check that CalDB has been installed.")

    # the CALDB 'main' tarball only contains a readme and manifest text
    # file in the 'bkgrnd' sub-directory
    return len(os.listdir(bkg_path)) > 2



def t_norm(chip,evt_keywords,bkg_keywords):
    """
    return the scaling factor based on chip exposure time
    """
    # use pre-filtered evtfile that matches the filtering applied to the bkgfile

    det = evt_keywords["INSTRUME"]

    if det == "HRC":
        obs_time = evt_keywords["EXPOSURE"]
        bkg_time = bkg_keywords["EXPOSURE"]

    else:
        try:
            bkg_time = bkg_keywords[f"LIVETIM{chip}"]
        except KeyError:
            try:
                bkg_time = bkg_keywords[f"LIVTIME{chip}"]
            except KeyError:
                bkg_time = bkg_keywords["LIVETIME"]

        try:
            obs_time = evt_keywords[f"LIVETIM{chip}"]
        except KeyError:
            try:
                obs_time = evt_keywords[f"LIVTIME{chip}"]
            except KeyError:
                obs_time = evt_keywords["LIVETIME"]

    return obs_time/bkg_time



def e_norm(evtfile,bkgfile,chip,bkgparams,instrument):
    """
    return the high-energy (total) photon count scaling factor
    """

    # use pre-filtered evtfile that matches the filtering applied to the bkgfile

    det = instrument

    if det == "ACIS":
        det = "ccd_id"
    else:
        det = "chip_id"

    ccd = f"[{det}={chip}]"

    obs_counts = float(pcr.read_file(f"{evtfile}{ccd}{bkgparams}").get_nrows())
    bkg_counts = float(pcr.read_file(f"{bkgfile}{ccd}{bkgparams}").get_nrows())

    return obs_counts/bkg_counts



def he_rate_norm(evtfile,bkgfile,chip,bkgparams,instrument,evt_keywords,bkg_keywords):
    """
    return scaling factor based on the the high-energy photon count rate
    """

    he = e_norm(evtfile,bkgfile,chip,bkgparams,instrument)
    t = t_norm(chip,evt_keywords,bkg_keywords)

    return he/t



def check_tool_par(evt,tool,param):
    dmhistory = make_tool("dmhistory")

    dmhistory.punlearn()
    dmhistory.infile = evt
    dmhistory.tool = tool
    dmhistory.verbose = "1"

    try:
        hist = dmhistory()
    except OSError as exc:
        raise ValueError(f"{tool} was not used to generate {evt}") from exc

    hist = [t for t in hist.split("\n") if t.startswith(tool)]

    par_instance = []

    for h in hist:
        for par in h.split(" "):
            if par.startswith(param):
                par_instance.append(par)

    par_instance = [par.replace("\"","") for par in par_instance]

    return f"{param}=yes" in par_instance



def _chkvfpha(kw,evt):
    if kw["DATAMODE"] == "VFAINT":
        if "CHKVFPHA" in kw.keys():
            return kw["CHKVFPHA"] # kw introduced in 4.16 to replace fragile 'check_tool_par' approach
        return check_tool_par(evt=evt,tool="acis_process_events", param="check_vf_pha")
    return False



def find_acis_stowed_bkg(evtfile,ccd_list):
    """
    #########################################################
    #########################################################
    ######                                             ######
    ###### TSTART Values in 'acis_bkgrnd_4.9.7.tar.gz' ######
    ######                                             ######
    #########################################################
    #########################################################
    ### Group D ###
    # $CALDB/data/chandra/acis/bkgrnd/acis0D2000-12-01bgstow_ctiN0005.fits
    # 92016001.0
    # $CALDB/data/chandra/acis/bkgrnd/acis1D2000-12-01bgstow_ctiN0005.fits
    # 92016001.0
    # $CALDB/data/chandra/acis/bkgrnd/acis2D2000-12-01bgstow_ctiN0003.fits
    # 92016001.0
    # $CALDB/data/chandra/acis/bkgrnd/acis3D2000-12-01bgstow_ctiN0005.fits
    # 92016001.0
    # $CALDB/data/chandra/acis/bkgrnd/acis5D2000-12-01bgstow_ctiN0002.fits
    # 92016001.0
    # $CALDB/data/chandra/acis/bkgrnd/acis6D2000-12-01bgstow_ctiN0005.fits
    # 92016001.0
    # $CALDB/data/chandra/acis/bkgrnd/acis7D2000-12-01bgstow_ctiN0002.fits
    # 92016001.0
    ## non-CTI corrected files ##
    # $CALDB/data/chandra/acis/bkgrnd/acis5D2000-12-01bgstowN0004.fits
    # 92016001.0
    # $CALDB/data/chandra/acis/bkgrnd/acis7D2000-12-01bgstowN0004.fits
    # 92016001.0

    ### Group E ###
    # $CALDB/data/chandra/acis/bkgrnd/acis0D2005-09-01bgstow_ctiN0002.fits
    # 241920064.0
    # $CALDB/data/chandra/acis/bkgrnd/acis1D2005-09-01bgstow_ctiN0002.fits
    # 241920064.0
    # $CALDB/data/chandra/acis/bkgrnd/acis2D2005-09-01bgstow_ctiN0002.fits
    # 241920064.0
    # $CALDB/data/chandra/acis/bkgrnd/acis3D2005-09-01bgstow_ctiN0002.fits
    # 241920064.0
    # $CALDB/data/chandra/acis/bkgrnd/acis5D2005-09-01bgstow_ctiN0002.fits
    # 241920064.0
    # $CALDB/data/chandra/acis/bkgrnd/acis6D2005-09-01bgstow_ctiN0002.fits
    # 241920064.0
    # $CALDB/data/chandra/acis/bkgrnd/acis7D2005-09-01bgstow_ctiN0002.fits
    # 241920064.0

    ### Group F ###
    # $CALDB/data/chandra/acis/bkgrnd/acis0D2009-09-21bgstow_ctiN0002.fits
    # 369878466.184
    # $CALDB/data/chandra/acis/bkgrnd/acis1D2009-09-21bgstow_ctiN0002.fits
    # 369878466.184
    # $CALDB/data/chandra/acis/bkgrnd/acis2D2009-09-21bgstow_ctiN0002.fits
    # 369878466.184
    # $CALDB/data/chandra/acis/bkgrnd/acis3D2009-09-21bgstow_ctiN0002.fits
    # 369878466.184
    # $CALDB/data/chandra/acis/bkgrnd/acis5D2009-09-21bgstow_ctiN0002.fits
    # 369878466.184
    # $CALDB/data/chandra/acis/bkgrnd/acis6D2009-09-21bgstow_ctiN0002.fits
    # 369878466.184
    # $CALDB/data/chandra/acis/bkgrnd/acis7D2009-09-21bgstow_ctiN0002.fits
    # 369878466.184
    """

    # Assumes data sets are CTI-corrected, ignore the non-CTI-corrected
    # Epoch D set of early ACIS-5 and -7 uncorrected stowed files.
    # This will almost certainly affect all graded-mode observations, but
    # if the observation's made in this mode, it's likely so bright that
    # the user wouldn't be using blanksky/particle backgrounds to being with
    try:
        ctiapp = fileio.get_keys_from_file(f"{evtfile}[#row=0]")["CTI_APP"]
    except KeyError as exc:
        raise IOError(f"{evtfile} is missing the 'CTI_APP' header keyword!") from exc

    if "N" in "".join(set(ctiapp)).upper():
        raise IOError(f"The '{__toolname__}' script only supports CTI-corrected event files for ACIS stowed background file selection.")

    caldb = os.environ["CALDB"]

    bkg_path = f"{caldb}/data/chandra/acis/bkgrnd"

    epoch_dict = {"Epoch D" : {"tstart" : 92016001.0,
                               "date" : "D2000-12-01"},
                  "Epoch E" : {"tstart" : 241920064.0,
                               "date" : "D2005-09-01"},
                  "Epoch F" : {"tstart" : 369878466.184,
                               "date" : "D2009-09-21"}}

    stowed_ccds = ( 0, 1, 2, 3, 5, 6, 7 )
    tstart = fileio.get_keys_from_file(f"{evtfile}[#row=0]")["TSTART"]

    ccds = set(stowed_ccds).intersection(set(ccd_list))

    if tstart <= epoch_dict["Epoch E"]["tstart"]:
        datestr = epoch_dict["Epoch D"]["date"]
    elif epoch_dict["Epoch E"]["tstart"] < tstart <= epoch_dict["Epoch F"]["tstart"]:
        datestr = epoch_dict["Epoch E"]["date"]
    else:
        datestr = epoch_dict["Epoch F"]["date"]

    stowed_caldb = [fn for fn in os.listdir(bkg_path) if "bgstow" in fn]
    stowed_substr = [f"acis{ccd}{datestr}bgstow_cti" for ccd in ccds]

    stowedfiles = [f"{bkg_path}/{fn}" for substr in stowed_substr for fn in stowed_caldb if fn.startswith(substr)]

    return ",".join(stowedfiles)



def apply_gain(infile,outfile,kw,tmpdir,verbose):
    """
    run acis_process_events to update the gain correction, should be done alongside
    t_gain and CTI correction, which is currently not supported by a_p_e.
    """

    # The two gain files should have the same date and version number in the filename.
    # If they do not match, reprocess the background file with the event file gain file.

    # identify gainfiles #
    gainfile = kw["GAINFILE"]
    bkg_gain = fileio.get_keys_from_file(infile)["GAINFILE"]

    a_p_e = make_tool("acis_process_events")
    dmtcalc = make_tool("dmtcalc")

    if gainfile != bkg_gain:

        aciscaldb = f"{os.environ['CALDB']}/data/chandra/acis"
        ctifile = kw["CTIFILE"]
        tgainfile = kw["TGAINFIL"]

        ## grab eventdef and remove time def
        ## change status bits of background events to match infile w/a_p_e

        def _get_edef(evt,removecol=None):
            cr = pcr.read_file(f"{evt}[#row=0]")
            colnames = [c[:c.find("(")] if "(" in c else c for c in cr.get_colnames(vectors=True)]

            # The data types recognized by acis_process_events include
            # d (double-precision real), f (single-precision real), i (integer),
            # l (long integer), s (short integer), and x (logical)

            edef_type = { "float64" : "d",
                          "float32" : "f",
                          "int64" : "l",
                          "int32" : "i",
                          "int16" : "s",
                          "uint8" : "x" }

            edef = { col : edef_type[cr.get_column(col).values.dtype.name] for col in colnames }

            del cr

            if removecol is not None:
                try:
                    colnames.remove(removecol)
                except ValueError:
                    pass

            edefstr = [f"{edef[col]}:{col}" for col in colnames]

            return  f"{{{','.join(edefstr)}}}"


        eventdef = _get_edef(infile,removecol="time")

        ## CTI and TGAIN Calibration Files
        #
        # The calibration files used to apply the CTI and time-dependent
        # gain corrections should also match in the event and background data.
        # In practice, though, it is not currently possible to apply newer CTI
        # and TGAIN correction the background files in the CALDB; they lack
        # some columns required by acis_process_events.  The error introduced
        # by this mismatched calibration should not be very significant for
        # the faint, extended objects for which people use these backgrounds.

        a_p_e.punlearn()

        a_p_e.outfile = outfile
        a_p_e.acaofffile = "NONE"
        a_p_e.stop = "none"
        a_p_e.doevtgrade = False
        a_p_e.apply_cti = True
        a_p_e.apply_tgain = True #False
        a_p_e.calculate_pi = True
        a_p_e.pix_adj = "NONE"
        a_p_e.eventdef = eventdef
        a_p_e.clobber = True
        a_p_e.verbose = verbose

        a_p_e.gainfile = f"{aciscaldb}/det_gain/{gainfile}"
        a_p_e.ctifile = f"{aciscaldb}/t_gain/{tgainfile}"
        a_p_e.tgainfile = f"{aciscaldb}/cti/{ctifile}"

        try:
            a_p_e(infile = infile)

        except OSError:
            # add a time and expno column to the blanksky file, so acis_process_events
            # can run on it (change introduced in 4.8)
            with tempfile.NamedTemporaryFile(dir=tmpdir) as bkg_col:

                dmtcalc.punlearn()

                dmtcalc.infile = infile
                dmtcalc.outfile = bkg_col.name
                dmtcalc.expression = "time=#nan;expno=#nan"
                dmtcalc.clobber = True
                dmtcalc.verbose = "0"

                dmtcalc()

                a_p_e(infile = bkg_col.name)

    else:
        shutil.copyfile(infile,outfile)



def get_par(argv):
    """
    Get data_products parameters from parameter file
    """

    pfile = open_param_file(argv,toolname=__toolname__)["fp"]

    # Common parameters:
    params: dict = {}
    pars: dict = {}

    # load all parameters to dictionary
    pars["evtfile"] = params["evtfile"] = paramio.pgetstr(pfile,"evtfile")
    pars["outfile"] = params["outfile"] = paramio.pgetstr(pfile,"outfile")
    pars["asolfile"] = params["asol"] = paramio.pgetstr(pfile,"asolfile")
    # pars["band"] = params["band"] = paramio.pgetstr(pfile,"band")
    pars["stowedbg"] = params["stowedbg"] = paramio.pgetstr(pfile,"stowedbg")
    pars["bkgparams"] = params["bkgparams"] = paramio.pgetstr(pfile,"bkgparams")
    pars["weight_method"] = params["weight"] = paramio.pgetstr(pfile,"weight_method")
    pars["random"] = params["randomseed"] = paramio.pgeti(pfile,"random")
    pars["tmpdir"] = params["tmpdir"] = paramio.pgetstr(pfile,"tmpdir")
    pars["verbose"] = params["verbose"] = paramio.pgeti(pfile,"verbose")
    pars["clobber"] = params["clobber"] = paramio.pgetstr(pfile, "clobber")
    pars["mode"] = params["mode"] = paramio.pgetstr(pfile, "mode")

    # print script info
    set_verbosity(pars["verbose"])
    utils.print_version(__toolname__, __revision__)

    ## check and modify parameters
    ################################

    # files with information to reproject
    if params["evtfile"] == "":
        raise ScriptError("Input event file must be specified.")
    if params["evtfile"].startswith("@"):
        raise ScriptError("Input event stacks not supported.")

    #params["evtfile"] = stk.build(params["evtfile"])
    params["infile_filter"] = fileio.get_filter(params["evtfile"])
    params["evtfile"] = fileio.get_file(params["evtfile"])

    # output file name
    if params["outfile"] == "":
        raise ScriptError("Please specify an output file name.")

    params["outdir"],outfile = utils.split_outroot(params["outfile"])

    if params["outfile"].endswith("_"):
        params["outfile"] = outfile
    else:
        params["outfile"] = outfile.rstrip("_")

    # check if output directory is writable
    fileio.validate_outdir(params["outdir"])

    # aspect solution files, listed in quotes.
    if params["asol"] != "":
        if params["asol"].lower() == "none":
            raise ScriptError(f"Aspect solutions are required to run {__toolname__}.")

        params["asol"] = ",".join(stk.build(params["asol"]))
    else:
        fobs = obsinfo.ObsInfo(params["evtfile"])

        #v3("Looking in header for ASOLFILE keyword\n") # already done by .get_asol() when verbose=2

        asols = fobs.get_asol()
        asolstr = ",".join(asols)

        # It should be okay to have multiple entries per source since
        # downstream code tries to match observations to asol files,
        # but is this true or the best way to do it?
        #get_asol.extend(asols)
        if len(asols) == 1:
            suffix = ''
        else:
            suffix = 's'

        asol_status = [fileio.check_valid_file(f) for f in asols]

        if utils.equal_elements(asol_status):
            v1(f"Aspect solution file{suffix} {asolstr} found.\n")

        params["asol"] = asols

    if params["stowedbg"].lower() == "yes":
        params["stowedbg"] = True
    else:
        params["stowedbg"] = False

    # # energy bands and PI filter
    # if pars["band"] == "":
    #     raise ValueError("The bands parameter can not be empty.")
    #
    # params["enband"] = pars['band']

    # check that the bkgparams parameter is appropriately formatted for energy- or PI-space
    if pars["bkgparams"].lower() != "default":
        if pars["bkgparams"].startswith("[") and pars["bkgparams"].endswith("]"):
            if True in ["energy" in pars["bkgparams"].lower(), "pi" in pars["bkgparams"].lower()]:
                pass
            else:
                raise ValueError("The bkgparams parameter requires an 'energy' or 'pi' filter")
        else:
            raise ValueError("The bkgparams parameter requires an 'energy' or 'pi' filter")

    ## error out if there are spaces in absolute paths of the various parameters
    checklist_spaces = [(pars["evtfile"], "input event"),
                        (pars["outfile"], "output")]
    checklist_spaces.extend([(asol, "asol") for asol in params["asol"]])

    for fn,filetype in checklist_spaces:
        _check_no_spaces(fn,filetype)

    # close parameters block after validation
    paramio.paramclose(pfile)

    v3(f"  Parameters: {params}")

    return params,pars



def _check_no_spaces(fn,filetype):
    abspath = os.path.abspath(fn)

    if " " in abspath:
        raise IOError(f"The absolute path for the {filetype} file, '{abspath}', cannot contain any spaces")



def blanksky(infile,filters,asols,tmpdir,keywords,stowedbg,verbose):
    """This routine will copy and tailor blank-sky maps to observations, but will leave
    reprojection to the observation as a separate step to avoid memory bandwidth limitations
    when trying to copy files in parallel.  Presume that infile has already been cleaned of flares"""

    obsid = keywords["OBS_ID"]
    detector = keywords["INSTRUME"]

    ## create binned images from events files, if no image files are provided
    # obtain background for HRC-I observations.
    if detector == "HRC":
        if keywords["DETNAM"] == "HRC-S":
            raise ScriptError("There are no HRC-S blank-sky maps available in \
CalDB to tailor into a background map.")

        v1("Creating HRC-I background files, if available.\n")

        # 1999-12-06T00:00:00 is the earliest observation date with valid background files (CALDB 4.3.1)
        #hrc_earliest = dates("1999-12-06T00:00:00",fromcal="GREG",tocal="CHANDRA")

        # 2000-01-01T00:00:00 is the earliest observation date with valid background files (CALDB 4.6.1)
        hrc_earliest = dates("2000-01-01T00:00:00",fromcal="GREG",tocal="CHANDRA")

        if not caldb_blanksky("HRC"):
            raise ScriptError("Blank-sky maps not available.  Please make sure that the \
CalDB blank-sky maps are installed.")

        if keywords["TSTART"] < hrc_earliest:
            raise ScriptError(f"ObsID {obsid}: 2000-01-01 is the earliest observation date \
with valid HRC-I background files.")

        # determine background file to use
        hrci_bkg = fluximage._find_blanksky_hrci_caldb(infile, verbose=verbose, tmpdir=tmpdir)

        v1(f"HRC-I background file {hrci_bkg} found.\n")

        if hrci_bkg == "":
            raise ScriptError(f"There is no available blank-sky map for {infile}.")

        with suppress_stdout_stderr():
            return fluximage.blanksky_hrci(hrci_bkg, infile, tmpdir, obsid, verbose), [0]

    else:
        ## produce ACIS blank-sky background map

        dmcopy = make_tool("dmcopy")
        dmmerge = make_tool("dmmerge")
        dmcoords = make_tool("dmcoords")
        dmhedit = make_tool("dmhedit")
        dmkeypar = make_tool("dmkeypar")
        acis_bkgrnd_lookup = make_tool("acis_bkgrnd_lookup")

        # grab unique CCDs for each observation, after filtering, if applied.  Otherwise, infile has been
        # stripped of filters earlier to prevent breaking due to tool syntax
        if filters == "":
            ccds = fileio.get_ccds(infile).tolist()
        else:
            with tempfile.NamedTemporaryFile(dir=tmpdir,suffix="_evtfilt") as infile_filt:
                dmcopy.punlearn()

                dmcopy.infile = f"{infile}{filters}"
                dmcopy.outfile = infile_filt.name
                dmcopy.clobber = True
                dmcopy.verbose = "0"
                dmcopy()

                ccds = fileio.get_ccds(infile_filt.name).tolist()

        # check for CTI_APP keyword before running acis_bkgrnd_lookup
        if "CTI_APP" not in keywords.keys():
            raise ScriptError(f"CTI_APP header keyword missing from {infile}. Please reprocess data with chandra_repro.")

        ## do some case filtering to prevent complete breaking of script
        # There are no background files for:
        # *      non-CTI-corrected data on ACIS-1
        # *      any data on ACIS-4
        # *      CTI-corrected data on ACIS-9
        # *      ACIS-0 data, when ACIS-S is in the focal plane (a SIM_Z limit is exceeded)
        if stowedbg and not set([4,8,9]).isdisjoint(set(ccds)):
            v1("ACIS-S0, -S4, and/or -S5 will be ignored, as no stowed background files are \
available for these chips.\n")

            for ccd in [4,8,9]:
                try:
                    ccds.remove(ccd)
                except ValueError:
                    pass

        if 4 in ccds:
            v1("Ignoring ACIS-S0, as no blank-sky map is available for this chip.\n")
            ccds.remove(4)

        if 1 in ccds and not keywords["CTI_CORR"]:
            v1("Ignoring ACIS-I1, as no blank-sky map is available for this chip without CTI corrections applied.\n")
            ccds.remove(1)

        if 9 in ccds and keywords["CTI_CORR"]:
            v1("Ignoring ACIS-S5, as no blank-sky map is available for this chip with CTI corrections applied.\n")
            ccds.remove(9)

        if [0 in ccds, set([4,5,6,7,8,9]).isdisjoint(set(ccds))] == [True,False]:
            # check location of aimpoint with dmcoords using the
            # time-averaged optical-axis coordinates
            ra_pnt = keywords["RA_PNT"]
            dec_pnt = keywords["DEC_PNT"]

            dmcoords.punlearn()

            dmcoords.infile = infile
            dmcoords.asol = asols
            dmcoords.ra = ra_pnt
            dmcoords.dec = dec_pnt
            dmcoords.opt = "cel"
            dmcoords.celfmt = "deg"
            dmcoords.verbose = "0"

            dmcoords()

            # chip aimpoint falls on
            aimpoint = dmcoords.chip_id

            # check if aimpoints fall on ACIS-S chips
            if aimpoint not in [0,1,2,3]:
                v1(f"Ignoring ACIS-I0, as no blank-sky map is available for this chips when aimpoint \
is on ACIS-{aimpoint}, due to exceeding of SIM-Z limit.\n")

                ccds.remove(0)

        if stowedbg:
            acis_bkg = find_acis_stowed_bkg(infile,ccds)
        else:
            # run acis_bkgrnd_lookup
            acis_bkgrnd_lookup.punlearn()

            acis_bkgrnd_lookup.infile = f"{infile}[ccd_id={','.join([str(i) for i in ccds])}]"

            acis_bkgrnd_lookup()

            acis_bkg = acis_bkgrnd_lookup.outfile

        if acis_bkg == "":
            raise ScriptError(f"There is no available blank-sky map for {infile}.")

        # and hunt down duplicate identifications, since
        # there are also a few cases that result in identical lookup results:
        #
        # *     For the front-illuminated (FI) chips, there is no difference between:
        #       o CTI_APP = PPPPPNPNPP
        #       o CTI_APP = PPPPPBPBPP
        #
        # since the parallel CTI-correction is applied to all FI chips either way.
        # *     For the back-illuminated (BI) chips, there is no difference between any of these:
        #       o CTI_APP = NNNNNNNNNN
        #       o CTI_APP = PPPPPNPNPP
        #       o CTI_CORR = YES
        #       o CTI_CORR = NO
        #
        # since no CTI correction is applied to BI chips for any of those configurations.

        acis_bkg = utils.getUniqueSynset(acis_bkg.split(","))

        # print list of blanksky files to be used
        if len(acis_bkg) == 1:
            suffix = ''
        else:
            suffix = 's'

        v1(f"ACIS background file{suffix} {','.join(acis_bkg)} found.\n")

        # make copy of background file to tailor
        bkg_calib = tempfile.NamedTemporaryFile(dir=tmpdir)

        if len(ccds) == 1:
            shutil.copyfile(acis_bkg[0],bkg_calib.name)

        else:
            # combine files
            dmmerge.punlearn()

            dmmerge.infile = acis_bkg
            dmmerge.outfile = bkg_calib.name
            dmmerge.clobber = True
            dmmerge.verbose = "0"

            dmmerge()

        # spatially handle sub-array observations
        if keywords["NROWS"] != 1024:
            nrows = keywords["NROWS"]
            firstrow = keywords["FIRSTROW"]

            bkg_subarray = tempfile.NamedTemporaryFile(suffix=".bkg",dir=tmpdir)

            dmcopy.punlearn()

            dmcopy.infile = f"{bkg_calib.name}[chipy={firstrow}:{firstrow+nrows-1}]"
            dmcopy.outfile = bkg_subarray.name
            dmcopy.verbose = "0"
            dmcopy.clobber = True

            dmcopy()

            bkg_calib.close()

            bkg_calib = bkg_subarray


        # ### apply new gain file, if necessary ###
        #
        # bkg_repro = tempfile.NamedTemporaryFile(dir=tmpdir)
        #
        # apply_gain(infile=bkg_calib.name,outfile=bkg_repro.name,
        #            kw=keywords,tmpdir=tmpdir,verbose=verbose)
        #
        # bkg_calib.close()
        # bkg_calib = bkg_repro


        # add keywords as needed; as of CIAO 4.11, reproject_events
        # handles the _PNT and _AVG keywords.
        for key in ["FIRSTROW", "NROWS"]:
            dmkeypar.punlearn()
            dmhedit.punlearn()

            try:
                dmkeypar.infile = infile
                dmkeypar.keyword = key
                dmkeypar()

            except IOError as exc:
                raise IOError(f"{infile} missing the {key} header keyword.") from exc

            finally:
                dmhedit.infile = f"{bkg_calib.name}[EVENTS]"
                dmhedit.filelist = ""
                dmhedit.operation = "add"
                dmhedit.key = key
                dmhedit.value = dmkeypar.value

                dmhedit()

        bkg = tempfile.NamedTemporaryFile(suffix=".bkg",dir=tmpdir)

        # if datamode is VFAINT and ran a_p_e with check_vf_pha=yes,
        # remove background events with non-0 status
        if keywords["DATAMODE"] == "VFAINT" and _chkvfpha(keywords,infile):
            dmcopy.punlearn()

            dmcopy.infile = f"{bkg_calib.name}[status=0]"
            dmcopy.outfile = bkg.name
            dmcopy.verbose = "0"
            dmcopy.clobber = True

            dmcopy()

        else:
            shutil.copyfile(bkg_calib.name,bkg.name)

        bkg_calib.close()

        return bkg,ccds



@handle_ciao_errors(__toolname__,__revision__)
def doit():

    params,pars = get_par(sys.argv)

    evtfile = params["evtfile"]
    filters = params["infile_filter"]
    outdir = params["outdir"]
    outfile = params["outfile"]
    asol = params["asol"]
    # enband = params["enband"]
    stowedbg = params["stowedbg"]
    bkgparams = params["bkgparams"]
    method = params["weight"].lower()
    seed = params["randomseed"]
    tmpdir = params["tmpdir"]
    clobber = params["clobber"]
    verbose = params["verbose"]

    kw = fileio.get_keys_from_file(f"{evtfile}[#row=0]")
    instrument = kw["INSTRUME"]

    if clobber.lower() == "no" and os.path.isfile(f"{outdir}/{outfile}"):
        raise IOError(f"clobber='no' and the file, {outdir}/{outfile}, already exists.")

    # # validate enband, but also modify the energy range if specified, since the validate_bands
    # # function requires a format of "elo:ehi:eff", even though effective energy is irrelevant
    # if enband.lower() == "none":
    #     enband = "::1"
    # elif len(enband.split(":")) == 2:
    #     enband = "{}:1".format(enband)
    #
    # enband = bands.validate_bands(instrument,enband)[0].dmfilterstr

    # check that there are high-energy counts in the input event file (w/filter),
    # will also use the filtered event file downstream to calculate scale factors for each chip

    reproject_events = make_tool("reproject_events")
    dmcopy = make_tool("dmcopy")
    dmhedit = make_tool("dmhedit")

    with new_pfiles_environment(ardlib=False), tempfile.NamedTemporaryFile(dir=tmpdir) as check_evt:

        dmcopy.punlearn()
        dmcopy.infile = f"{evtfile}{filters}"
        dmcopy.outfile = check_evt.name
        dmcopy.clobber = True
        dmcopy.verbose = "0"
        dmcopy()

        if method != "time":
            if instrument == "ACIS":
                if bkgparams.lower() == "default":
                    e_particle = "[energy=9000:12000]"
                else:
                    e_particle = bkgparams
            else:
                if bkgparams.lower() == "default":
                    e_particle = "[pi=300:500]"
                else:
                    e_particle = bkgparams

            check_counts = pcr.read_file(f"{check_evt.name}{e_particle}").get_nrows()

            if check_counts == 0:
                raise IOError("The input event file has zero high-energy \
counts defined by the 'bkgparams' parameter to scale the \
particle background.")

        # check that blanksky files are in CalDB
        if not caldb_blanksky(instrument):
            if instrument == "ACIS":
                raise ScriptError("ACIS blanksky background files are not installed in the CalDB")

            raise ScriptError("HRC blanksky background files are not installed in the CalDB")

        # tailor raw CalDB blanksky files to match observation
        bsky,ccds = blanksky(evtfile,filters,asol,tmpdir,kw,stowedbg,verbose)

        # reproject blank sky file to match observation coordinates
        reproject_events.punlearn()

        reproject_events.infile = f"{bsky.name}{filters}" #{enband}"
        reproject_events.outfile = f"{outdir}{outfile}"
        reproject_events.aspect = asol
        reproject_events.match = evtfile
        reproject_events.random = seed
        reproject_events.verbose = verbose
        reproject_events.clobber = clobber

        reproject_events()

        bsky.close()

        # calculate and write scale factors to header
        dmhedit.punlearn()

        dmhedit.infile = f"{outdir}{outfile}[EVENTS]"
        dmhedit.filelist = ""
        dmhedit.operation = "add"

        if "e_particle" in locals():
            dmhedit.key = "BKGPARAM"
            dmhedit.value = e_particle
            dmhedit.datatype = "string"
            dmhedit()

        dmhedit.key = "BKGMETH"
        dmhedit.value = method
        dmhedit.datatype = "string"
        dmhedit()

        # calculate scaling factor, add as keyword
        scale = []
        for ccd in sorted(ccds):
            if method == "particle":
                scalefactor = e_norm(check_evt.name,f"{outdir}{outfile}",
                                     ccd,e_particle,instrument)
            else:
                bkg_kw = fileio.get_keys_from_file(f"{outdir}{outfile}")

                if method == "time":
                    scalefactor = t_norm(ccd,kw,bkg_kw)
                else:
                    scalefactor = he_rate_norm(check_evt.name,f"{outdir}{outfile}",
                                               ccd,e_particle,instrument,kw,bkg_kw)

            if instrument == "HRC":
                dmhedit.key = "BKGSCALE"
            else:
                dmhedit.key = f"BKGSCAL{ccd}"

            dmhedit.value = scalefactor
            dmhedit.datatype = "float"
            dmhedit()

            scale.append((ccd,round(scalefactor,8)))


    if instrument == "HRC":
        v1(f"Calculated scale factor for 'weight_method={method}': {scalefactor}, \
written to the 'BKGSCALE' header keyword.")

    else:
        if len(ccds) == 1:
            v1(f"Calculated scale factor for 'weight_method={method}':")
        else:
            v1(f"Calculated scale factors for 'weight_method={method}':")

        for ccd,weight in scale:
            v1(f"{'ACIS':>8}-{ccd}: {weight} written to the 'BKGSCAL{ccd}' header keyword.")

    add_tool_history(f"{outdir}{outfile}",__toolname__,pars)



if __name__ == "__main__":
    doit()
