# Licensed under an MIT style license -- see LICENSE.md

import os
import sys
import logging
import contextlib
import time
import copy
import shutil

import numpy as np
from scipy.integrate import cumtrapz
from scipy.interpolate import interp1d
from scipy import stats
import h5py
from pesummary import conf

__author__ = ["Charlie Hoy <charlie.hoy@ligo.org>"]

try:
    from coloredlogs import ColoredFormatter as LogFormatter
except ImportError:
    LogFormatter = logging.Formatter

CACHE_DIR = os.path.join(
    os.getenv(
        "XDG_CACHE_HOME",
        os.path.expanduser(os.path.join("~", ".cache")),
    ),
    "pesummary",
)
STYLE_CACHE = os.path.join(CACHE_DIR, "style")
LOG_CACHE = os.path.join(CACHE_DIR, "log")


def resample_posterior_distribution(posterior, nsamples):
    """Randomly draw nsamples from the posterior distribution

    Parameters
    ----------
    posterior: ndlist
        nd list of posterior samples. If you only want to resample one
        posterior distribution then posterior=[[1., 2., 3., 4.]]. For multiple
        posterior distributions then posterior=[[1., 2., 3., 4.], [1., 2., 3.]]
    nsamples: int
        number of samples that you wish to randomly draw from the distribution
    """
    if len(posterior) == 1:
        n, bins = np.histogram(posterior, bins=50)
        n = np.array([0] + [i for i in n])
        cdf = cumtrapz(n, bins, initial=0)
        cdf /= cdf[-1]
        icdf = interp1d(cdf, bins)
        samples = icdf(np.random.rand(nsamples))
    else:
        posterior = np.array([i for i in posterior])
        keep_idxs = np.random.choice(
            len(posterior[0]), nsamples, replace=False)
        samples = [i[keep_idxs] for i in posterior]
    return samples


def check_file_exists_and_rename(file_name):
    """Check to see if a file exists and if it does then rename the file

    Parameters
    ----------
    file_name: str
        proposed file name to store data
    """
    if os.path.isfile(file_name):
        import shutil

        old_file = "{}_old".format(file_name)
        while os.path.isfile(old_file):
            old_file += "_old"
        logger.warning(
            "The file '{}' already exists. Renaming the existing file to "
            "{} and saving the data to the requested file name".format(
                file_name, old_file
            )
        )
        shutil.move(file_name, old_file)


def check_condition(condition, error_message):
    """Raise an exception if the condition is not satisfied
    """
    if condition:
        raise Exception(error_message)


def rename_group_or_dataset_in_hf5_file(base_file, group=None, dataset=None):
    """Rename a group or dataset in an hdf5 file

    Parameters
    ----------
    group: list, optional
        a list containing the path to the group that you would like to change
        as the first argument and the new name of the group as the second
        argument
    dataset: list, optional
        a list containing the name of the dataset that you would like to change
        as the first argument and the new name of the dataset as the second
        argument
    """
    condition = not os.path.isfile(base_file)
    check_condition(condition, "The file %s does not exist" % (base_file))
    f = h5py.File(base_file, "a")
    if group:
        f[group[1]] = f[group[0]]
        del f[group[0]]
    elif dataset:
        f[dataset[1]] = f[dataset[0]]
        del f[dataset[0]]
    f.close()


def make_dir(path):
    if os.path.isdir(os.path.expanduser(path)):
        pass
    else:
        os.makedirs(os.path.expanduser(path), exist_ok=True)


def guess_url(web_dir, host, user):
    """Guess the base url from the host name

    Parameters
    ----------
    web_dir: str
        path to the web directory where you want the data to be saved
    host: str
        the host name of the machine where the python interpreter is currently
        executing
    user: str
        the user that is current executing the python interpreter
    """
    ligo_data_grid = False
    if 'public_html' in web_dir:
        ligo_data_grid = True
    if ligo_data_grid:
        path = web_dir.split("public_html")[1]
        if "raven" in host or "arcca" in host:
            url = "https://geo2.arcca.cf.ac.uk/~{}".format(user)
        elif 'ligo-wa' in host:
            url = "https://ldas-jobs.ligo-wa.caltech.edu/~{}".format(user)
        elif 'ligo-la' in host:
            url = "https://ldas-jobs.ligo-la.caltech.edu/~{}".format(user)
        elif "cit" in host or "caltech" in host:
            url = "https://ldas-jobs.ligo.caltech.edu/~{}".format(user)
        elif 'uwm' in host or 'nemo' in host:
            url = "https://ldas-jobs.phys.uwm.edu/~{}".format(user)
        elif 'phy.syr.edu' in host:
            url = "https://sugar-jobs.phy.syr.edu/~{}".format(user)
        elif 'vulcan' in host:
            url = "https://galahad.aei.mpg.de/~{}".format(user)
        elif 'atlas' in host:
            url = "https://atlas1.atlas.aei.uni-hannover.de/~{}".format(user)
        elif 'iucaa' in host:
            url = "https://ldas-jobs.gw.iucaa.in/~{}".format(user)
        elif 'alice' in host:
            url = "https://dumpty.alice.icts.res.in/~{}".format(user)
        elif 'hawk' in host:
            url = "https://ligo.gravity.cf.ac.uk/~{}".format(user)
        else:
            url = "https://{}/~{}".format(host, user)
        url += path
    else:
        url = "https://{}".format(web_dir)
    return url


def map_parameter_names(dictionary, mapping):
    """Modify keys in dictionary to use different names according to a map

    Parameters
    ----------
    mapping: dict
        dictionary mapping existing keys to new names.

    Returns
    -------
    standard_dict: dict
        dict object with new parameter names
    """
    standard_dict = {}
    for key, item in dictionary.items():
        if key not in mapping.keys():
            standard_dict[key] = item
            continue
        standard_dict[mapping[key]] = item
    return standard_dict


def command_line_arguments():
    """Return the command line arguments
    """
    return sys.argv[1:]


def command_line_dict():
    """Return a dictionary of command line arguments
    """
    from pesummary.gw.cli.parser import ArgumentParser
    parser = ArgumentParser()
    parser.add_all_known_options_to_parser()
    opts = parser.parse_args()
    return vars(opts)


def gw_results_file(opts):
    """Determine if a GW results file is passed
    """
    from pesummary.gw.cli.parser import ArgumentParser

    attrs, defaults = ArgumentParser().gw_options
    condition = any(
        hasattr(opts, attr) and getattr(opts, attr) and getattr(opts, attr)
        != default for attr, default in zip(attrs, defaults)
    )
    if condition:
        return True
    return False


def functions(opts, gw=False):
    """Return a dictionary of functions that are either specific to GW results
    files or core.
    """
    from pesummary.core.cli.inputs import (
        WebpagePlusPlottingPlusMetaFileInput as Input
    )
    from pesummary.gw.cli.inputs import (
        WebpagePlusPlottingPlusMetaFileInput as GWInput
    )
    from pesummary.core.file.meta_file import MetaFile
    from pesummary.gw.file.meta_file import GWMetaFile
    from pesummary.core.finish import FinishingTouches
    from pesummary.gw.finish import GWFinishingTouches

    dictionary = {}
    dictionary["input"] = GWInput if gw_results_file(opts) or gw else Input
    dictionary["MetaFile"] = GWMetaFile if gw_results_file(opts) or gw else MetaFile
    dictionary["FinishingTouches"] = \
        GWFinishingTouches if gw_results_file(opts) or gw else FinishingTouches
    return dictionary


def _logger_format():
    return '%(asctime)s %(name)s %(levelname)-8s: %(message)s'


def setup_logger():
    """Set up the logger output.
    """
    import tempfile

    def get_console_handler(stream_level="INFO"):
        console_handler = logging.StreamHandler()
        console_handler.setLevel(level=getattr(logging, stream_level))
        console_handler.setFormatter(FORMATTER)
        return console_handler

    def get_file_handler(log_file):
        file_handler = logging.FileHandler(log_file, mode='w')
        file_handler.setLevel(level=logging.DEBUG)
        file_handler.setFormatter(FORMATTER)
        return file_handler

    make_dir(LOG_CACHE)
    dirpath = tempfile.mkdtemp(dir=LOG_CACHE)
    stream_level = 'INFO'
    if "-v" in sys.argv or "--verbose" in sys.argv:
        stream_level = 'DEBUG'

    FORMATTER = LogFormatter(_logger_format(), datefmt='%Y-%m-%d  %H:%M:%S')
    LOG_FILE = '%s/pesummary.log' % (dirpath)
    logger = logging.getLogger('PESummary')
    logger.propagate = False
    logger.setLevel(level=logging.DEBUG)
    logger.addHandler(get_console_handler(stream_level=stream_level))
    logger.addHandler(get_file_handler(LOG_FILE))
    return logger, LOG_FILE


def remove_tmp_directories():
    """Remove the temporary directories created by PESummary
    """
    import shutil
    from glob import glob

    directories = glob(".tmp/pesummary/*")

    for i in directories:
        if os.path.isdir(i):
            shutil.rmtree(i)
        elif os.path.isfile(i):
            os.remove(i)


def _add_existing_data(namespace):
    """Add existing data to namespace object
    """
    for num, i in enumerate(namespace.existing_labels):
        if hasattr(namespace, "labels") and i not in namespace.labels:
            namespace.labels.append(i)
        if hasattr(namespace, "samples") and i not in list(namespace.samples.keys()):
            namespace.samples[i] = namespace.existing_samples[i]
        if hasattr(namespace, "weights") and i not in list(namespace.weights.keys()):
            if namespace.existing_weights is None:
                namespace.weights[i] = None
            else:
                namespace.weights[i] = namespace.existing_weights[i]
        if hasattr(namespace, "injection_data"):
            if i not in list(namespace.injection_data.keys()):
                namespace.injection_data[i] = namespace.existing_injection_data[i]
        if hasattr(namespace, "file_versions"):
            if i not in list(namespace.file_versions.keys()):
                namespace.file_versions[i] = namespace.existing_file_version[i]
        if hasattr(namespace, "file_kwargs"):
            if i not in list(namespace.file_kwargs.keys()):
                namespace.file_kwargs[i] = namespace.existing_file_kwargs[i]
        if hasattr(namespace, "config"):
            if namespace.existing_config[num] not in namespace.config:
                namespace.config.append(namespace.existing_config[num])
            elif namespace.existing_config[num] is None:
                namespace.config.append(None)
        if hasattr(namespace, "priors"):
            if hasattr(namespace, "existing_priors"):
                for key, item in namespace.existing_priors.items():
                    if key in namespace.priors.keys():
                        for label in item.keys():
                            if label not in namespace.priors[key].keys():
                                namespace.priors[key][label] = item[label]
                    else:
                        namespace.priors.update({key: item})
        if hasattr(namespace, "approximant") and namespace.approximant is not None:
            if i not in list(namespace.approximant.keys()):
                if i in list(namespace.existing_approximant.keys()):
                    namespace.approximant[i] = namespace.existing_approximant[i]
        if hasattr(namespace, "psds") and namespace.psds is not None:
            if i not in list(namespace.psds.keys()):
                if i in list(namespace.existing_psd.keys()):
                    namespace.psds[i] = namespace.existing_psd[i]
                else:
                    namespace.psds[i] = {}
        if hasattr(namespace, "calibration") and namespace.calibration is not None:
            if i not in list(namespace.calibration.keys()):
                if i in list(namespace.existing_calibration.keys()):
                    namespace.calibration[i] = namespace.existing_calibration[i]
                else:
                    namespace.calibration[i] = {}
        if hasattr(namespace, "skymap") and namespace.skymap is not None:
            if i not in list(namespace.skymap.keys()):
                if i in list(namespace.existing_skymap.keys()):
                    namespace.skymap[i] = namespace.existing_skymap[i]
                else:
                    namespace.skymap[i] = None
        if hasattr(namespace, "maxL_samples"):
            if i not in list(namespace.maxL_samples.keys()):
                namespace.maxL_samples[i] = {
                    key: val.maxL for key, val in namespace.samples[i].items()
                }
        if hasattr(namespace, "pepredicates_probs"):
            if i not in list(namespace.pepredicates_probs.keys()):
                from pesummary.gw.classification import PEPredicates
                try:
                    namespace.pepredicates_probs[i] = PEPredicates(
                        namespace.existing_samples[i]
                    ).dual_classification()
                except Exception:
                    namespace.pepredicates_probs[i] = None
        if hasattr(namespace, "pastro_probs"):
            if i not in list(namespace.pastro_probs.keys()):
                from pesummary.gw.classification import PAstro
                try:
                    namespace.pastro_probs[i] = PAstro(
                        namespace.existing_samples[i]
                    ).dual_classification()
                except Exception:
                    namespace.pastro_probs[i] = None
    if hasattr(namespace, "result_files"):
        number = len(namespace.labels)
        while len(namespace.result_files) < number:
            namespace.result_files.append(namespace.existing_metafile)
    parameters = [list(namespace.samples[i].keys()) for i in namespace.labels]
    namespace.same_parameters = list(
        set.intersection(*[set(l) for l in parameters])
    )
    namespace.same_samples = {
        param: {
            i: namespace.samples[i][param] for i in namespace.labels
        } for param in namespace.same_parameters
    }
    return namespace


def customwarn(message, category, filename, lineno, file=None, line=None):
    """
    """
    import sys
    import warnings

    sys.stdout.write(
        warnings.formatwarning("%s" % (message), category, filename, lineno)
    )


def determine_gps_time_and_window(maxL_samples, labels):
    """Determine the gps time and window to use in the spectrogram and
    omegascan plots
    """
    times = [
        maxL_samples[label]["geocent_time"] for label in labels
    ]
    gps_time = np.mean(times)
    time_range = np.max(times) - np.min(times)
    if time_range < 4.:
        window = 4.
    else:
        window = time_range * 1.5
    return gps_time, window


def number_of_columns_for_legend(labels):
    """Determine the number of columns to use in a legend

    Parameters
    ----------
    labels: list
        list of labels in the legend
    """
    max_length = np.max([len(i) for i in labels]) + 5.
    if max_length > 50.:
        return 1
    else:
        return int(50. / max_length)


class RedirectLogger(object):
    """Class to redirect the output from other codes to the `pesummary`
    logger

    Parameters
    ----------
    level: str, optional
        the level to display the messages
    """
    def __init__(self, code, level="Debug"):
        self.logger = logging.getLogger('PESummary')
        self.level = getattr(logging, level)
        self._redirector = contextlib.redirect_stdout(self)
        self.code = code

    def isatty(self):
        pass

    def write(self, msg):
        """Write the message to stdout

        Parameters
        ----------
        msg: str
            the message you wish to be printed to stdout
        """
        if msg and not msg.isspace():
            self.logger.log(self.level, "[from %s] %s" % (self.code, msg))

    def flush(self):
        pass

    def __enter__(self):
        self._redirector.__enter__()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self._redirector.__exit__(exc_type, exc_value, traceback)


def draw_conditioned_prior_samples(
    samples_dict, prior_samples_dict, conditioned, xlow, xhigh, N=100,
    nsamples=1000
):
    """Return a prior_dict that is conditioned on certain parameters

    Parameters
    ----------
    samples_dict: pesummary.utils.samples_dict.SamplesDict
        SamplesDict containing the posterior samples
    prior_samples_dict: pesummary.utils.samples_dict.SamplesDict
        SamplesDict containing the prior samples
    conditioned: list
        list of parameters that you wish to condition your prior on
    xlow: dict
        dictionary of lower bounds for each parameter
    xhigh: dict
        dictionary of upper bounds for each parameter
    N: int, optional
        number of points to use within the grid. Default 100
    nsamples: int, optional
        number of samples to draw. Default 1000
    """
    for param in conditioned:
        indices = _draw_conditioned_prior_samples(
            prior_samples_dict[param], samples_dict[param], xlow[param],
            xhigh[param], xN=N, N=nsamples
        )
        for key, val in prior_samples_dict.items():
            prior_samples_dict[key] = val[indices]

    return prior_samples_dict


def _draw_conditioned_prior_samples(
    prior_samples, posterior_samples, xlow, xhigh, xN=1000, N=1000
):
    """Return a list of indices for the conditioned prior via rejection
    sampling. The conditioned prior will then be `prior_samples[indicies]`.
    Code from Michael Puerrer.

    Parameters
    ----------
    prior_samples: np.ndarray
        array of prior samples that you wish to condition
    posterior_samples: np.ndarray
        array of posterior samples that you wish to condition on
    xlow: float
        lower bound for grid to be used
    xhigh: float
        upper bound for grid to be used
    xN: int, optional
        Number of points to use within the grid
    N: int, optional
        Number of samples to generate
    """
    from pesummary.utils.bounded_1d_kde import ReflectionBoundedKDE

    prior_KDE = ReflectionBoundedKDE(prior_samples)
    posterior_KDE = ReflectionBoundedKDE(posterior_samples)

    x = np.linspace(xlow, xhigh, xN)
    idx_nz = np.nonzero(posterior_KDE(x))
    pdf_ratio = prior_KDE(x)[idx_nz] / posterior_KDE(x)[idx_nz]
    M = 1.1 / min(pdf_ratio[np.where(pdf_ratio < 1)])

    indicies = []
    i = 0
    while i < N:
        x_i = np.random.choice(prior_samples)
        idx_i = np.argmin(np.abs(prior_samples - x_i))
        u = np.random.uniform()
        if u < posterior_KDE(x_i) / (M * prior_KDE(x_i)):
            indicies.append(idx_i)
            i += 1
    return indicies


def unzip(zip_file, outdir=None, overwrite=False):
    """Extract the data from a zipped file and save in outdir.

    Parameters
    ----------
    zip_file: str
        path to the file you wish to unzip
    outdir: str, optional
        path to the directory where you wish to save the unzipped file. Default
        None which means that the unzipped file is stored in CACHE_DIR
    overwrite: Bool, optional
        If True, overwrite a file that has the same name
    """
    import gzip
    import shutil
    from pathlib import Path

    f = Path(zip_file)
    file_name = f.stem
    if outdir is None:
        outdir = CACHE_DIR
    out_file = os.path.join(outdir, file_name)
    if os.path.isfile(out_file) and not overwrite:
        raise FileExistsError(
            "The file '{}' already exists. Not overwriting".format(out_file)
        )
    with gzip.open(zip_file, 'rb') as input:
        with open(out_file, 'wb') as output:
            shutil.copyfileobj(input, output)
    return out_file


def iterator(
    iterable, desc=None, logger=None, tqdm=False, total=None, file=None,
    bar_format=None
):
    """Return either a tqdm iterator, if tqdm installed, or a simple range

    Parameters
    ----------
    iterable: func
        iterable that you wish to iterate over
    desc: str, optional
        description for the tqdm bar
    tqdm: Bool, optional
        If True, a tqdm object is used. Otherwise simply returns the iterator.
    logger_output: Bool, optional
        If True, the tqdm progress bar interacts with logger
    total: float, optional
        total length of iterable
    logger_name: str, optional
        name of the logger you wish to use
    file: str, optional
        path to file that you wish to write the output to
    """
    from pesummary.utils.tqdm import tqdm
    if tqdm:
        try:
            FORMAT, DESC = None, None
            if bar_format is None:
                FORMAT = (
                    '{desc} | {percentage:3.0f}% | {n_fmt}/{total_fmt} | {elapsed}'
                )
            if desc is not None:
                DESC = desc
            return tqdm(
                iterable, total=total, logger=logger, desc=DESC, file=file,
                bar_format=FORMAT,
            )
        except ImportError:
            return iterable
    else:
        return iterable


def _check_latex_install(force_tex=False):
    from matplotlib import rcParams
    from distutils.spawn import find_executable

    original = rcParams['text.usetex']
    if find_executable("latex") is not None:
        try:
            from matplotlib.texmanager import TexManager

            texmanager = TexManager()
            texmanager.make_dvi(r"$mass_{1}$", 12)
            if force_tex:
                original = True
            rcParams["text.usetex"] = original
        except RuntimeError:
            rcParams["text.usetex"] = False
    else:
        rcParams["text.usetex"] = False


def smart_round(parameters, return_latex=False, return_latex_row=False):
    """Round a parameter according to the uncertainty. If more than one parameter
    and uncertainty is passed, each parameter is rounded according to the
    lowest uncertainty

    Parameters
    ----------
    parameter_dictionary: list/np.ndarray
        list containing the median, upper bound and lower bound for a given parameter
    return_latex: Bool, optional
        if True, return as a latex string
    return_latex_row: Bool, optional
        if True, return the rounded data as a single row in latex format

    Examples
    --------
    >>> data = [1.234, 0.2, 0.1]
    >>> smart_round(data)
    [ 1.2  0.2  0.1]
    >>> data = [
    ...     [6.093, 0.059, 0.055],
    ...     [6.104, 0.057, 0.052],
    ...     [6.08, 0.056, 0.052]
    ... ]
    >>> smart_round(data)
    [[ 6.09  0.06  0.06]
     [ 6.1   0.06  0.05]
     [ 6.08  0.06  0.05]]
    >>> smart_round(data, return_latex=True)
    6.09^{+0.06}_{-0.06}
    6.10^{+0.06}_{-0.05}
    6.08^{+0.06}_{-0.05}
    >>> data = [
    ...     [743.25, 43.6, 53.2],
    ...     [8712.5, 21.5, 35.2],
    ...     [196.46, 65.2, 12.5]
    ... ]
    >>> smart_round(data, return_latex_row=True)
    740^{+40}_{-50} & 8710^{+20}_{-40} & 200^{+70}_{-10}
    >>> data = [
    ...     [743.25, 43.6, 53.2],
    ...     [8712.5, 21.5, 35.2],
    ...     [196.46, 65.2, 8.2]
    ... ]
    >>> smart_round(data, return_latex_row=True)
    743^{+44}_{-53} & 8712^{+22}_{-35} & 196^{+65}_{-8}
    """
    rounded = copy.deepcopy(np.atleast_2d(parameters))
    lowest_uncertainty = np.min(np.abs(parameters))
    rounding = int(-1 * np.floor(np.log10(lowest_uncertainty)))
    for num, _ in enumerate(rounded):
        rounded[num] = [np.round(value, rounding) for value in rounded[num]]
    if return_latex or return_latex_row:
        if rounding > 0:
            _format = "%.{}f".format(rounding)
        else:
            _format = "%.f"
        string = "{0}^{{+{0}}}_{{-{0}}}".format(_format)
        latex = [string % (value[0], value[1], value[2]) for value in rounded]
        if return_latex:
            for ll in latex:
                print(ll)
        else:
            print(" & ".join(latex))
        return ""
    elif np.array(parameters).ndim == 1:
        return rounded[0]
    else:
        return rounded


def safe_round(a, decimals=0, **kwargs):
    """Try and round an array to the given number of decimals. If an exception
    is raised, return the original array

    Parameters
    ----------
    a: np.ndarray
        array you wish to round
    decimals: int
        the number of decimals you wish to round too
    **kwargs: dict
        all kwargs are passed to numpy.round
    """
    try:
        return np.round(a, decimals=decimals, **kwargs)
    except Exception:
        return a


def gelman_rubin(samples, decimal=5):
    """Return an approximation to the Gelman-Rubin statistic (see Gelman, A. and
     Rubin, D. B., Statistical Science, Vol 7, No. 4, pp. 457--511 (1992))

    Parameters
    ----------
    samples: np.ndarray
        2d array of samples for a given parameter, one for each chain
    decimal: int
        number of decimal places to keep when rounding

    Examples
    --------
    >>> from pesummary.utils.utils import gelman_rubin
    >>> samples = [[1, 1.5, 1.2, 1.4, 1.6, 1.2], [1.5, 1.3, 1.4, 1.7]]
    >>> gelman_rubin(samples, decimal=5)
    1.2972
    """
    means = [np.mean(data) for data in samples]
    variances = [np.var(data) for data in samples]
    BoverN = np.var(means)
    W = np.mean(variances)
    sigma = W + BoverN
    m = len(samples)
    Vhat = sigma + BoverN / m
    return np.round(Vhat / W, decimal)


def kolmogorov_smirnov_test(samples, decimal=5):
    """Return the KS p value between two PDFs

    Parameters
    ----------
    samples: 2d list
        2d list containing the 2 PDFs that you wish to compare
    decimal: int
        number of decimal places to keep when rounding
    """
    return np.round(stats.ks_2samp(*samples)[1], decimal)


def jensen_shannon_divergence(*args, **kwargs):
    import warnings
    warnings.warn(
        "The jensen_shannon_divergence function has changed its name to "
        "jensen_shannon_divergence_from_samples. jensen_shannon_divergence "
        "may not be supported in future releases. Please update"
    )
    return jensen_shannon_divergence_from_samples(*args, **kwargs)


def jensen_shannon_divergence_from_samples(
    samples, kde=stats.gaussian_kde, decimal=5, base=np.e, **kwargs
):
    """Calculate the JS divergence between two sets of samples

    Parameters
    ----------
    samples: list
        2d list containing the samples drawn from two pdfs
    kde: func
        function to use when calculating the kde of the samples
    decimal: int, float
        number of decimal places to round the JS divergence to
    base: float, optional
        optional base to use for the scipy.stats.entropy function. Default
        np.e
    kwargs: dict
        all kwargs are passed to the kde function
    """
    pdfs = samples_to_kde(samples, kde=kde, **kwargs)
    return jensen_shannon_divergence_from_pdfs(pdfs, decimal=decimal, base=base)


def jensen_shannon_divergence_from_pdfs(pdfs, decimal=5, base=np.e):
    """Calculate the JS divergence between two distributions

    Parameters
    ----------
    pdfs: list
        list of length 2 containing the distributions you wish to compare
    decimal: int, float
        number of decimal places to round the JS divergence to
    base: float, optional
        optional base to use for the scipy.stats.entropy function. Default
        np.e
    """
    if any(np.isnan(_).any() for _ in pdfs):
        return float("nan")
    a, b = pdfs
    a = np.asarray(a)
    b = np.asarray(b)
    a /= a.sum()
    b /= b.sum()
    m = 1. / 2 * (a + b)
    kl_forward = stats.entropy(a, qk=m, base=base)
    kl_backward = stats.entropy(b, qk=m, base=base)
    return np.round(kl_forward / 2. + kl_backward / 2., decimal)


def samples_to_kde(samples, kde=stats.gaussian_kde, **kwargs):
    """Generate KDE for a set of samples

    Parameters
    ----------
    samples: list
        list containing the samples to create a KDE for. samples can also
        be a 2d list containing samples from multiple analyses.
    kde: func
        function to use when calculating the kde of the samples
    """
    _SINGLE_ANALYSIS = False
    if not isinstance(samples[0], (np.ndarray, list, tuple)):
        _SINGLE_ANALYSIS = True
        _samples = [samples]
    else:
        _samples = samples
    kernel = []
    for i in _samples:
        try:
            kernel.append(kde(i, **kwargs))
        except np.linalg.LinAlgError:
            kernel.append(None)
    x = np.linspace(
        np.min([np.min(i) for i in _samples]),
        np.max([np.max(i) for i in _samples]),
        100
    )
    pdfs = [k(x) if k is not None else float('nan') for k in kernel]
    if _SINGLE_ANALYSIS:
        return pdfs[0]
    return pdfs


def make_cache_style_file(style_file):
    """Make a cache directory which stores the style file you wish to use
    when plotting

    Parameters
    ----------
    style_file: str
        path to the style file that you wish to use when plotting
    """
    make_dir(STYLE_CACHE)
    shutil.copyfile(
        style_file, os.path.join(STYLE_CACHE, "matplotlib_rcparams.sty")
    )


def get_matplotlib_style_file():
    """Return the path to the matplotlib style file that you wish to use
    """
    style_file = os.path.join(STYLE_CACHE, "matplotlib_rcparams.sty")
    if not os.path.isfile(style_file):
        from pesummary import conf

        return conf.style_file
    return os.path.join(style_file)


def get_matplotlib_backend(parallel=False):
    """Return the matplotlib backend to use for the plotting modules

    Parameters
    ----------
    parallel: Bool, optional
        if True, backend is always set to 'Agg' for the multiprocessing module
    """
    try:
        os.environ["DISPLAY"]
    except KeyError:
        try:
            __IPYTHON__
        except NameError:
            DISPLAY = False
        else:
            DISPLAY = True
    else:
        DISPLAY = True
    if DISPLAY and not parallel:
        backend = "TKAgg"
    else:
        backend = "Agg"
    return backend


def _default_filename(default_filename, label=None):
    """Return a default filename

    Parameters
    ----------
    default_filename: str, optional
        the default filename to use if a filename is not provided. default_filename
        must be a formattable string with one empty argument for a label
    label: str, optional
        The label of the analysis. This is used in the filename
    """
    if not label:
        filename = default_filename.format(round(time.time()))
    else:
        filename = default_filename.format(label)
    return filename


def check_filename(
    default_filename="pesummary_{}.dat", outdir="./", label=None, filename=None,
    overwrite=False, delete_existing=False
):
    """Check to see if a file exists. If no filename is provided, a default
    filename is checked

    Parameters
    ----------
    default_filename: str, optional
        the default filename to use if a filename is not provided. default_filename
        must be a formattable string with one empty argument for a label
    outdir: str, optional
        directory to write the dat file
    label: str, optional
        The label of the analysis. This is used in the filename if a filename
        if not specified
    filename: str, optional
        The name of the file that you wish to write
    overwrite: Bool, optional
        If True, an existing file of the same name will be overwritten
    """
    if not filename:
        filename = _default_filename(default_filename, label=label)
    _file = os.path.join(outdir, filename)
    if os.path.isfile(_file) and not overwrite:
        raise FileExistsError(
            "The file '{}' already exists in the directory {}".format(
                filename, outdir
            )
        )
    if os.path.isfile(_file) and delete_existing:
        os.remove(_file)
    return _file


def string_match(string, substring):
    """Return True if a string matches a substring. This substring may include
    wildcards

    Parameters
    ----------
    string: str
        string you wish to match
    substring: str
        string you wish to match against
    """
    import re
    import sre_constants

    try:
        match = re.match(re.compile(substring), string)
        if match:
            return True
        return False
    except sre_constants.error:
        import fnmatch
        return string_match(string, fnmatch.translate(substring))


def glob_directory(base):
    """Return a list of files matching base

    Parameters
    ----------
    base: str
        string you wish to match e.g. "./", "./*.py"
    """
    import glob
    if "*" not in base:
        base = os.path.join(base, "*")
    return glob.glob(base)


def list_match(list_to_match, substring, return_true=True, return_false=False):
    """Match a list of strings to a substring. This substring may include
    wildcards

    Parameters
    ----------
    list_to_match: list
        list of string you wish to match
    substring: str, list
        string you wish to match against or a list of string you wish to match
        against
    return_true: Bool, optional
        if True, return a sublist containing only the parameters that match the
        substring. Default True
    """
    match = np.ones(len(list_to_match), dtype=bool)
    if isinstance(substring, str):
        substring = [substring]

    for _substring in substring:
        match *= np.array(
            [string_match(item, _substring) for item in list_to_match],
            dtype=bool
        )
    if return_false:
        return np.array(list_to_match)[~match]
    elif return_true:
        return np.array(list_to_match)[match]
    return match


class Empty(object):
    """Define an empty class which simply returns the input
    """
    def __new__(self, *args):
        return args[0]


def history_dictionary(program=None, creator=conf.user, command_line=None):
    """Create a dictionary containing useful information about the origin of
    a PESummary data product

    Parameters
    ----------
    program: str, optional
        program used to generate the PESummary data product
    creator: str, optional
        The user who created the PESummary data product
    command_line: str, optional
        The command line which was run to generate the PESummary data product
    """
    from astropy.time import Time

    _dict = {
        "gps_creation_time": Time.now().gps,
        "creator": creator,
    }
    if command_line is not None:
        _dict["command_line"] = (
            "Generated by running the following script: {}".format(
                command_line
            )
        )
    else:
        _dict["command_line"] = " ".join(sys.argv)
    if program is not None:
        _dict["program"] = program
    return _dict


def mute_logger():
    """Mute the PESummary logger
    """
    _logger = logging.getLogger('PESummary')
    _logger.setLevel(logging.CRITICAL + 10)
    return


def unmute_logger():
    """Unmute the PESummary logger
    """
    _logger = logging.getLogger('PESummary')
    _logger.setLevel(logging.INFO)
    return


_, LOG_FILE = setup_logger()
logger = logging.getLogger('PESummary')
