# Copyright (C) 2011  Nickolas Fotopoulos
# Copyright (C) 2014-2017  Stephen Privitera
#
# 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.

from math import (log, ceil)

from numpy import (vectorize, arange, seterr, inf, ones_like)

import lalsimulation as lalsim
import lal
from lal import series as lalseries

from ligo.lw import (param, utils)

seterr(over="ignore")  # the PSD overflows frequently, but that's OK

#
# Analytical PSDs
#
noise_models = {
    "LIGOIPsd": vectorize(lambda f: lal.LIGOIPsd(f)),  # what most lalapps_* uses
    # what most lalapps_* uses; hasn't been XLALified, so doesn't have scaling factor. Gah!
    "AdvLIGOPsd": vectorize(lambda f: 1e-49 * lal.AdvLIGOPsd(f)),
    "iLIGOSRD": vectorize(lambda f: lalsim.SimNoisePSDiLIGOSRD(f)),
    "aLIGONoSRMLowPower": vectorize(lambda f: lalsim.SimNoisePSDaLIGONoSRMLowPower(f)),
    "aLIGONoSRMHighPower": vectorize(lambda f: lalsim.SimNoisePSDaLIGONoSRMHighPower(f)),
    "aLIGONSNSOpt": vectorize(lambda f: lalsim.SimNoisePSDaLIGONSNSOpt(f)),
    "aLIGOBHBH20Deg": vectorize(lambda f: lalsim.SimNoisePSDaLIGOBHBH20Deg(f)),
    "aLIGOHighFrequency": vectorize(lambda f: lalsim.SimNoisePSDaLIGOHighFrequency(f)),
    "aLIGOZeroDetLowPower": vectorize(lambda f: lalsim.SimNoisePSDaLIGOZeroDetLowPower(f)),
    "aLIGOZeroDetHighPower": vectorize(lambda f: lalsim.SimNoisePSDaLIGOZeroDetHighPower(f)),
}

psd_cache = {}  # keyed by df, flow, f_max


def get_PSD(df, flow, f_max, noise_model):
    """
    Return the frequency vector and sampled PSD using the noise_model,
    which are vectorized wrappings of the noise models in lalsimulation.
    flow sets the first non-zero frequency.
    """
    if (df, flow, f_max) in psd_cache:
        return psd_cache[df, flow, f_max]
    f = arange(f_max / df + 1) * df
    LIGO_PSD = inf * ones_like(f)
    ind_low = int(flow / df)
    LIGO_PSD[ind_low:] = noise_model(f[ind_low:])
    return psd_cache.setdefault((df, flow, f_max), LIGO_PSD)


asd_cache = {}


def get_ASD(df, flow, f_max, noise_model):
    """
    Some routines prefer ASDs over PSDs. Keep cache of ASDs, but we may
    be able to speed things up by drawing upon cache of PSDs.
    """
    if (df, flow, f_max) in asd_cache:
        return asd_cache[df, flow, f_max]
    return asd_cache.setdefault((df, flow, f_max), get_PSD(df, flow, f_max, noise_model)**0.5)

#
# Determine PSD for neighborhood
#


def next_pow2(n):
    return 1 << int(ceil(log(n, 2)))


def prev_pow2(n):
    return 1 << int(log(n, 2))


def get_neighborhood_df_fmax(waveforms, flow):
    """
    Return PSD that is optimized for this neighborhood, with small enough
    df and big enough f_max to cover all waveforms.
    """
    max_dur = max(w.dur for w in waveforms)
    assert 16384 * max_dur > 1   # chirp lasts long enough for one LIGO sample
    if max_dur >= 1:
        df = 1 / next_pow2(max_dur)
    else:
        df = prev_pow2(1 / max_dur)
    max_ffinal = max(w.f_final for w in waveforms)
    f_max = next_pow2(max_ffinal)  # will always be greater than 1
    assert f_max - flow >= 2 * df  # need a few frequencies at least!
    return df, f_max


def get_neighborhood_PSD(waveforms, flow, noise_model):
    """
    Return PSD that is optimized for this neighborhood, with small enough
    df and big enough f_max to cover all waveforms.
    """
    max_dur = max(w.dur for w in waveforms)
    assert 16384 * max_dur > 1   # chirp lasts long enough for one LIGO sample
    if max_dur >= 1:
        df = 1 / next_pow2(max_dur)
    else:
        df = prev_pow2(1 / max_dur)
    max_ffinal = max(w.f_final for w in waveforms)
    f_max = next_pow2(max_ffinal)  # will always be greater than 1
    assert f_max - flow >= 2 * df  # need a few frequencies at least!
    return df, get_PSD(df, flow, f_max, noise_model)


def get_neighborhood_ASD(waveforms, flow, noise_model):
    """
    Return ASD that is optimized for this neighborhood, with small enough
    df and big enough f_max to cover all waveforms.
    """
    max_dur = max(w.dur for w in waveforms)
    assert 16384 * max_dur > 1   # chirp lasts long enough for one LIGO sample
    if max_dur >= 1:
        df = 1 / next_pow2(max_dur)
    else:
        df = prev_pow2(1 / max_dur)
    max_ffinal = max(w.f_final for w in waveforms)
    f_max = next_pow2(max_ffinal)  # will always be greater than 1
    assert f_max - flow >= 2 * df  # need a few frequencies at least!
    return df, get_ASD(df, flow, f_max, noise_model)

#
# Read PSD from arrays in XML documents
#


def psd_instrument_dict(elem):
    out = {}
    for lw in elem.getElementsByTagName(u"LIGO_LW"):
        if not lw.hasAttribute(u"Name"):
            continue
        if lw.getAttribute(u"Name") != u"REAL8FrequencySeries":
            continue
        ifo = param.get_pyvalue(lw, u"instrument")
        out[ifo] = lalseries.parse_REAL8FrequencySeries(lw)
    return out


def read_psd(filename, verbose=False):
    return psd_instrument_dict(
        utils.load_filename(filename, verbose=verbose, contenthandler=lalseries.PSDContentHandler),
    )
