import itertools
import matplotlib.pyplot as plt
import numpy as np
from scipy.io import wavfile
from scipy.signal import detrend, lfilter, bilinear, spectrogram, filtfilt, resample, fftconvolve
import acoustics

from acoustics.standards.iso_tr_25417_2007 import REFERENCE_PRESSURE
from acoustics.standards.iec_61672_1_2013 import WEIGHTING_SYSTEMS
from acoustics.standards.iec_61672_1_2013 import (NOMINAL_OCTAVE_CENTER_FREQUENCIES,
                                                  NOMINAL_THIRD_OCTAVE_CENTER_FREQUENCIES)

class Signal(np.ndarray):
    """A signal consisting of samples (array) and a sample frequency (float).

    """

    def __new__(cls, data, fs):
        obj = np.asarray(data).view(cls)
        obj.fs = fs
        return obj

    def __array_prepare__(self, array, context=None):
        try:
            a = context[1][0]
            b = context[1][1]
        except IndexError:
            return array

        if hasattr(a, 'fs') and hasattr(b, 'fs'):
            if a.fs == b.fs:
                return array
            else:
                raise ValueError("Sample frequencies do not match.")
        else:
            return array

    def __array_wrap__(self, out_arr, context=None):
        return np.ndarray.__array_wrap__(self, out_arr, context)

    def __array_finalize__(self, obj):
        # see InfoArray.__array_finalize__ for comments
        if obj is None:
            return

        self.fs = getattr(obj, 'fs', None)

    def __reduce__(self):
        # Get the parent's __reduce__ tuple
        pickled_state = super(Signal, self).__reduce__()
        # Create our own tuple to pass to __setstate__
        new_state = pickled_state[2] + (self.fs, )
        # Return a tuple that replaces the parent's __setstate__ tuple with our own
        return (pickled_state[0], pickled_state[1], new_state)

    def __setstate__(self, state):
        self.fs = state[-1]  # Set the info attribute
        # Call the parent's __setstate__ with the other tuple elements.
        super(Signal, self).__setstate__(state[0:-1])

    def __repr__(self):
        return "Signal({})".format(str(self))

    def _construct(self, x):
        """Construct signal like x."""
        return Signal(x, self.fs)

    @property
    def samples(self):
        """Amount of samples in signal."""
        return self.shape[-1]

    @property
    def channels(self):
        """Amount of channels.
        """
        if self.ndim > 1:
            return self.shape[-2]
        else:
            return 1

    @property
    def duration(self):
        """Duration of signal in seconds.
        """
        return float(self.samples / self.fs)

    @property
    def values(self):
        """Return the values of this signal as an instance of :class:`np.ndarray`."""
        return np.array(self)

    def calibrate_to(self, decibel, inplace=False):
        """Calibrate signal to value `decibel`.

        :param decibel: Value to calibrate to.
        :param inplace: Whether to perform inplace or not.
        :returns: Calibrated signal.
        :rtype: :class:`Signal`

        Values of `decibel` are broadcasted. To set a value per channel, use `decibel[...,None]`.
        """
        decibel = decibel * np.ones(self.shape)
        gain = decibel - self.leq()[..., None]
        return self.gain(gain, inplace=inplace)

    def calibrate_with(self, other, decibel, inplace=False):
        """Calibrate signal with other signal.

        :param other: Other signal/array.
        :param decibel: Signal level of `other`.
        :param inplace: Whether to perform inplace or not.
        :returns: calibrated signal.
        :rtype: :class:`Signal`
        """
        if not isinstance(other, Signal):
            other = Signal(other, self.fs)
        gain = decibel - other.leq()
        return self.gain(gain, inplace=inplace)

    def decimate(self, factor, zero_phase=False, ftype='iir', order=None):
        """Decimate signal by integer `factor`. Before downsampling a low-pass filter is applied.

        :param factor: Downsampling factor.
        :param zero_phase: Prevent phase shift by filtering with ``filtfilt`` instead of ``lfilter``.
        :param ftype: Filter type.
        :param order: Filter order.
        :returns: Decimated signal.
        :rtype: :class:`Signal`

        .. seealso:: :func:`scipy.signal.decimate`
        .. seealso:: :meth:`resample`

        """
        return Signal(
            acoustics.signal.decimate(x=self, q=factor, n=order, ftype=ftype, zero_phase=zero_phase), self.fs / factor)

    def resample(self, nsamples, times=None, axis=-1, window=None):
        """Resample signal.

        :param samples: New amount of samples.
        :param times: Times corresponding to samples.
        :param axis: Axis.
        :param window: Window.

        .. seealso:: :func:`scipy.signal.resample`
        .. seealso:: :meth:`decimate`

        You might want to low-pass filter this signal before resampling.

        """
        return Signal(resample(self, nsamples, times, axis, window), nsamples / self.samples * self.fs)

    def upsample(self, factor, axis=-1):
        """Upsample signal with integer factor.

        :param factor: Upsample factor.
        :param axis: Axis.

        .. seealso:: :meth:`resample`
        """
        return self.resample(int(self.samples * factor), axis=axis)

    def gain(self, decibel, inplace=False):
        """Apply gain of `decibel` decibels.

        :param decibel: Decibels
        :param inplace: In place
        :returns: Amplified signal.
        :rtype: :class:`Signal`
        """
        factor = 10.0**(decibel / 20.0)
        if inplace:
            self *= factor
            return self
        else:
            return self * factor

    def pick(self, start=0.0, stop=None):
        """Get signal from start time to stop time.

        :param start: Start time.
        :type start: float
        :param stop: End time.
        :type stop: float
        :returns: Selected part of the signal.
        :rtype: :class:`Signal`

        """
        if start is not None:
            start = int(np.floor(start * self.fs))
        if stop is not None:
            stop = int(np.floor(stop * self.fs))
        return self[..., start:stop]

    def times(self):
        """Time vector.

        :returns: A vector with a timestamp for each sample.
        :rtype: :class:`np.ndarray`

        """
        return np.arange(0, self.samples) / self.fs

    def energy(self):
        """Signal energy.

        :returns: Total energy per channel.
        :rtype: :class:`np.ndarray`

        .. math:: E = \\sum_{n=0}^{N-1} |x_n|^2

        """
        return float((self * self).sum())

    def power(self):
        """Signal power.

        .. math:: P = \\frac{1}{N} \\sum_{n=0}^{N-1} |x_n|^2
        """
        return self.energy() / len(self)

    def ms(self):
        """Mean value squared of signal.

        .. seealso:: :func:`acoustics.signal.ms`

        """
        return acoustics.signal.ms(self)

    def rms(self):
        """Root mean squared of signal.

        .. seealso:: :func:`acoustics.signal.rms`

        """
        return acoustics.signal.rms(self)
        #return np.sqrt(self.power())

    def weigh(self, weighting='A', zero_phase=False):
        """Apply frequency-weighting. By default 'A'-weighting is applied.

        :param weighting: Frequency-weighting filter to apply.
            Valid options are 'A', 'C' and 'Z'. Default weighting is 'A'.
        :returns: Weighted signal.
        :rtype: :class:`Signal`.

        By default the weighting filter is applied using
        :func:`scipy.signal.lfilter` causing a frequency-dependent delay. In case a
        delay is undesired, the filter can be applied using :func:`scipy.signal.filtfilt`
        by setting `zero_phase=True`.

        """
        num, den = WEIGHTING_SYSTEMS[weighting]()
        b, a = bilinear(num, den, self.fs)
        func = filtfilt if zero_phase else lfilter
        return self._construct(func(b, a, self))

    def correlate(self, other=None, mode='full'):
        """Correlate signal with `other` signal. In case `other==None` this
        method returns the autocorrelation.

        :param other: Other signal.
        :param mode: Mode.

        .. seealso:: :func:`np.correlate`, :func:`scipy.signal.fftconvolve`

        """
        if other is None:
            other = self
        if self.fs != other.fs:
            raise ValueError("Cannot correlate. Sample frequencies are not the same.")
        if self.channels > 1 or other.channels > 1:
            raise ValueError("Cannot correlate. Not supported for multichannel signals.")
        return self._construct(fftconvolve(self, other[::-1], mode=mode))

    def amplitude_envelope(self):
        """Amplitude envelope.

        :returns: Amplitude envelope of signal.
        :rtype: :class:`Signal`

        .. seealso:: :func:`acoustics.signal.amplitude_envelope`

        """
        return self._construct(acoustics.signal.amplitude_envelope(self, self.fs))

    def instantaneous_frequency(self):
        """Instantaneous frequency.

        :returns: Instantaneous frequency of signal.
        :rtype: :class:`Signal`

        .. seealso:: :func:`acoustics.signal.instantaneous_frequency`

        """
        return self._construct(acoustics.signal.instantaneous_frequency(self, self.fs))

    def instantaneous_phase(self):
        """Instantaneous phase.

        :returns: Instantaneous phase of signal.
        :rtype: :class:`Signal`

        .. seealso:: :func:`acoustics.signal.instantaneous_phase`

        """
        return self._construct(acoustics.signal.instantaneous_phase(self, self.fs))

    def detrend(self, **kwargs):
        """Detrend signal.

        :returns: Detrended version of signal.
        :rtype: :class:`Signal`

        .. seealso:: :func:`scipy.signal.detrend`

        """
        return self._construct(detrend(self, **kwargs))

    def unwrap(self):
        """Unwrap signal in case the signal represents wrapped phase.

        :returns: Unwrapped signal.
        :rtype: :class:`Signal`

        .. seealso:: :func:`np.unwrap`

        """
        return self._construct(np.unwrap(self))

    def complex_cepstrum(self, N=None):
        """Complex cepstrum.

        :param N: Amount of bins.
        :returns: Quefrency, complex cepstrum and delay in amount of samples.

        .. seealso:: :func:`acoustics.cepstrum.complex_cepstrum`

        """
        if N is not None:
            times = np.linspace(0.0, self.duration, N, endpoint=False)
        else:
            times = self.times()
        cepstrum, ndelay = acoustics.cepstrum.complex_cepstrum(self, n=N)
        return times, cepstrum, ndelay

    def real_cepstrum(self, N=None):
        """Real cepstrum.

        :param N: Amount of bins.
        :returns: Quefrency and real cepstrum.

        .. seealso:: :func:`acoustics.cepstrum.real_cepstrum`

        """
        if N is not None:
            times = np.linspace(0.0, self.duration, N, endpoint=False)
        else:
            times = self.times()
        return times, acoustics.cepstrum.real_cepstrum(self, n=N)

    def power_spectrum(self, N=None):
        """Power spectrum.

        :param N: Amount of bins.

        .. seealso:: :func:`acoustics.signal.power_spectrum`

        """
        return acoustics.signal.power_spectrum(self, self.fs, N=N)

    def angle_spectrum(self, N=None):
        """Phase angle spectrum. Wrapped.

        :param N: amount of bins.

        .. seealso::

            :func:`acoustics.signal.angle_spectrum`, :func:`acoustics.signal.phase_spectrum`
            and :meth:`phase_spectrum`.

        """
        return acoustics.signal.angle_spectrum(self, self.fs, N=N)

    def phase_spectrum(self, N=None):
        """Phase spectrum. Unwrapped.

        :param N: Amount of bins.

        .. seealso::

            :func:`acoustics.signal.phase_spectrum`, :func:`acoustics.signal.angle_spectrum`
            and :meth:`angle_spectrum`.

        """
        return acoustics.signal.phase_spectrum(self, self.fs, N=N)

    def peak(self, axis=-1):
        """Peak sound pressure.

        :param axis: Axis.

        .. seealso::

            :func:`acoustic.standards.iso_tr_25417_2007.peak_sound_pressure`

        """
        return acoustics.standards.iso_tr_25417_2007.peak_sound_pressure(self, axis=axis)

    def peak_level(self, axis=-1):
        """Peak sound pressure level.

        :param axis: Axis.

        .. seealso::

            :func:`acoustics.standards.iso_tr_25417_2007.peak_sound_pressure_level`

        """
        return acoustics.standards.iso_tr_25417_2007.peak_sound_pressure_level(self, axis=axis)

    def min(self, axis=-1):
        """Return the minimum along a given axis.

        Refer to `np.amin` for full documentation.
        """
        return np.ndarray.min(self, axis=axis)

    def max(self, axis=-1):
        """Return the minimum along a given axis.

        Refer to `np.amax` for full documentation.
        """
        return np.ndarray.max(self, axis=axis)

    def max_level(self, axis=-1):
        """Maximum sound pressure level.

        :param axis: Axis.

        .. seealso:: :func:`acoustics.standards.iso_tr_25417_2007.max_sound_pressure_level`

        """
        return acoustics.standards.iso_tr_25417_2007.max_sound_pressure_level(self, axis=axis)

    def sound_exposure(self, axis=-1):
        """Sound exposure.

        :param axis: Axis.

        .. seealso:: :func:`acoustics.standards.iso_tr_25417_2007.sound_exposure`

        """
        return acoustics.standards.iso_tr_25417_2007.sound_exposure(self, self.fs, axis=axis)

    def sound_exposure_level(self, axis=-1):
        """Sound exposure level.

        :param axis: Axis.

        .. seealso:: :func:`acoustics.standards.iso_tr_25417_2007.sound_exposure_level`

        """
        return acoustics.standards.iso_tr_25417_2007.sound_exposure_level(self, self.fs, axis=axis)

    def plot_complex_cepstrum(self, N=None, **kwargs):
        """Plot complex cepstrum of signal.

        Valid kwargs:

        * xscale
        * yscale
        * xlim
        * ylim
        * frequency: Boolean indicating whether the x-axis should show time in seconds or quefrency
        * xlabel_frequency: Label in case frequency is shown.

        """
        params = {
            'xscale': 'linear',
            'yscale': 'linear',
            'xlabel': "$t$ in s",
            'ylabel': "$C$",
            'title': 'Complex cepstrum',
            'frequency': False,
            'xlabel_frequency': "$f$ in Hz",
        }
        params.update(kwargs)

        t, ceps, _ = self.complex_cepstrum(N=N)
        if params['frequency']:
            t = 1. / t
            params['xlabel'] = params['xlabel_frequency']
            t = t[::-1]
            ceps = ceps[::-1]
        return _base_plot(t, ceps, params)

    def plot_real_cepstrum(self, N=None, **kwargs):
        """Plot real cepstrum of signal.

        Valid kwargs:

        * xscale
        * yscale
        * xlim
        * ylim
        * frequency: Boolean indicating whether the x-axis should show time in seconds or quefrency
        * xlabel_frequency: Label in case frequency is shown.

        """
        params = {
            'xscale': 'linear',
            'yscale': 'linear',
            'xlabel': "$t$ in s",
            'ylabel': "$C$",
            'title': 'Real cepstrum',
            'frequency': False,
            'xlabel_frequency': "$f$ in Hz",
        }
        params.update(kwargs)

        t, ceps = self.real_cepstrum(N=N)
        if params['frequency']:
            t = 1. / t
            params['xlabel'] = params['xlabel_frequency']
            t = t[::-1]
            ceps = ceps[::-1]
        return _base_plot(t, ceps, params)

    def plot_power_spectrum(self, N=None, **kwargs):  #filename=None, scale='log'):
        """Plot spectrum of signal.

        Valid kwargs:

        * xscale
        * yscale
        * xlim
        * ylim
        * reference: Reference power

        .. seealso:: :meth:`power_spectrum`

        """
        params = {
            'xscale': 'log',
            'yscale': 'linear',
            'xlabel': "$f$ in Hz",
            'ylabel': "$L_{p}$ in dB",
            'title': 'SPL',
            'reference': REFERENCE_PRESSURE**2.0,
        }
        params.update(kwargs)

        f, o = self.power_spectrum(N=N)
        return _base_plot(f, 10.0 * np.log10(o / params['reference']), params)

    def plot_angle_spectrum(self, N=None, **kwargs):
        """Plot phase angle spectrum of signal. Wrapped.

        Valid kwargs:

        * xscale
        * yscale
        * xlim
        * ylim
        * reference: Reference power

        """
        params = {
            'xscale': 'linear',
            'yscale': 'linear',
            'xlabel': "$f$ in Hz",
            'ylabel': r"$\angle \phi$",
            'title': 'Phase response (wrapped)',
        }
        params.update(kwargs)
        f, o = self.angle_spectrum(N=N)
        return _base_plot(f, o, params)

    def plot_phase_spectrum(self, N=None, **kwargs):
        """Plot phase spectrum of signal. Unwrapped.

        Valid kwargs:

        * xscale
        * yscale
        * xlim
        * ylim
        * reference: Reference power

        """
        params = {
            'xscale': 'linear',
            'yscale': 'linear',
            'xlabel': "$f$ in Hz",
            'ylabel': r"$\angle \phi$",
            'title': 'Phase response (unwrapped)',
        }
        params.update(kwargs)
        f, o = self.phase_spectrum(N=N)
        return _base_plot(f, o, params)

    def spectrogram(self, **kwargs):
        """Spectrogram of signal.

        :returns: Spectrogram.

        See :func:`scipy.signal.spectrogram`. Some of the default values have been changed.
        The generated spectrogram consists by default of complex values.

        """
        params = {
            'nfft': 4096,
            'noverlap': 128,
            'mode': 'complex',
        }
        params.update(kwargs)

        t, s, P = spectrogram(self, fs=self.fs, **params)

        return t, s, P

    def plot_spectrogram(self, **kwargs):
        """
        Plot spectrogram of the signal.

        Valid kwargs:

        * xlim
        * ylim
        * clim
        .. note:: This method only works for a single channel.

        """
        # To do, use :meth:`spectrogram`.
        params = {
            'xlim': None,
            'ylim': None,
            'clim': None,
            'NFFT': 4096,
            'noverlap': 128,
            'title': 'Spectrogram',
            'xlabel': '$t$ in s',
            'ylabel': '$f$ in Hz',
            'clabel': 'SPL in dB',
            'colorbar': True,
        }
        params.update(kwargs)

        if self.channels > 1:
            raise ValueError("Cannot plot spectrogram of multichannel signal. Please select a single channel.")

        # Check if an axes object is passed in. Otherwise, create one.
        ax0 = params.get('ax', plt.figure().add_subplot(111))
        ax0.set_title(params['title'])

        data = np.squeeze(self)
        try:
            _, _, _, im = ax0.specgram(data, Fs=self.fs, noverlap=params['noverlap'], NFFT=params['NFFT'],
                                       mode='magnitude', scale_by_freq=False)
        except AttributeError:
            raise NotImplementedError(
                "Your version of matplotlib is incompatible due to lack of support of the mode keyword argument to matplotlib.mlab.specgram."
            )

        if params['colorbar']:
            cb = ax0.get_figure().colorbar(mappable=im)
            cb.set_label(params['clabel'])

        ax0.set_xlim(params['xlim'])
        ax0.set_ylim(params['ylim'])
        im.set_clim(params['clim'])

        ax0.set_xlabel(params['xlabel'])
        ax0.set_ylabel(params['ylabel'])

        return ax0

    def levels(self, time=0.125, method='average'):
        """Calculate sound pressure level as function of time.

        :param time: Averaging time or integration time constant. Default value is 0.125 corresponding to FAST.
        :param method: Use time `average` or time `weighting`. Default option is `average`.
        :returns: sound pressure level as function of time.

        .. seealso:: :func:`acoustics.standards.iec_61672_1_2013.time_averaged_sound_level`
        .. seealso:: :func:`acoustics.standards.iec_61672_1_2013.time_weighted_sound_level`

        """
        if method == 'average':
            return acoustics.standards.iec_61672_1_2013.time_averaged_sound_level(self.values, self.fs, time)
        elif method == 'weighting':
            return acoustics.standards.iec_61672_1_2013.time_weighted_sound_level(self.values, self.fs, time)
        else:
            raise ValueError("Invalid method")

    def leq(self):
        """Equivalent level. Single-value number.

        .. seealso:: :func:`acoustics.standards.iso_tr_25417_2007.equivalent_sound_pressure_level`

        """
        return acoustics.standards.iso_tr_25417_2007.equivalent_sound_pressure_level(self.values)

    def plot_levels(self, **kwargs):
        """Plot sound pressure level as function of time.

        .. seealso:: :meth:`levels`

        """
        params = {
            'xscale': 'linear',
            'yscale': 'linear',
            'xlabel': '$t$ in s',
            'ylabel': '$L_{p,F}$ in dB',
            'title': 'SPL',
            'time': 0.125,
            'method': 'average',
            'labels': None,
        }
        params.update(kwargs)
        t, L = self.levels(params['time'], params['method'])
        L_masked = np.ma.masked_where(np.isinf(L), L)
        return _base_plot(t, L_masked, params)

    #def octave(self, frequency, fraction=1):
    #"""Determine fractional-octave `fraction` at `frequency`.

    #.. seealso:: :func:`acoustics.signal.fractional_octaves`

    #"""
    #return acoustics.signal.fractional_octaves(self, self.fs, frequency,
    #frequency, fraction, False)[1]

    def bandpass(self, lowcut, highcut, order=8, zero_phase=False):
        """Filter signal with band-pass filter.

        :param lowcut: Lower cornerfrequency.
        :param highcut: Upper cornerfrequency.
        :param order: Filter order.
        :param zero_phase: Prevent phase error by filtering in both directions (filtfilt).

        :returns: Band-pass filtered signal.
        :rtype: :class:`Signal`.

        .. seealso:: :func:`acoustics.signal.bandpass`
        """
        return type(self)(acoustics.signal.bandpass(self, lowcut, highcut, self.fs, order=order, zero_phase=zero_phase),
                          self.fs)

    def bandstop(self, lowcut, highcut, order=8, zero_phase=False):
        """Filter signal with band-stop filter.

        :param lowcut: Lower cornerfrequency.
        :param highcut: Upper cornerfrequency.
        :param order: Filter order.
        :param zero_phase: Prevent phase error by filtering in both directions (filtfilt).

        :returns: Band-pass filtered signal.
        :rtype: :class:`Signal`.

        .. seealso:: :func:`acoustics.signal.bandstop`
        """
        return type(self)(acoustics.signal.bandstop(self, lowcut, highcut, self.fs, order=order, zero_phase=zero_phase),
                          self.fs)

    def highpass(self, cutoff, order=4, zero_phase=False):
        """Filter signal with high-pass filter.

        :param cutoff: Cornerfrequency.
        :param order: Filter order.
        :param zero_phase: Prevent phase error by filtering in both directions (filtfilt).
        :returns: High-pass filtered signal.
        :rtype: :class:`Signal`.

        .. seealso:: :func:`acoustics.signal.highpass`
        """
        return type(self)(acoustics.signal.highpass(self, cutoff, self.fs, order=order, zero_phase=zero_phase), self.fs)

    def lowpass(self, cutoff, order=4, zero_phase=False):
        """Filter signal with low-pass filter.

        :param cutoff: Cornerfrequency.
        :param order: Filter order.
        :param zero_phase: Prevent phase error by filtering in both directions (filtfilt).
        :returns: Low-pass filtered signal.
        :rtype: :class:`Signal`.

        .. seealso:: :func:`acoustics.signal.lowpass`
        """
        return type(self)(acoustics.signal.lowpass(self, cutoff, self.fs, order=order, zero_phase=zero_phase), self.fs)

    def octavepass(self, center, fraction, order=8, zero_phase=False):
        """Filter signal with fractional-octave band-pass filter.

        :param center: Center frequency. Any value in the band will suffice.
        :param fraction: Band designator.
        :param order: Filter order.
        :param zero_phase: Prevent phase error by filtering in both directions (filtfilt).
        :returns: Band-pass filtered signal.
        :rtype: :class:`Signal`.

        .. seealso:: :func:`acoustics.signal.octavepass`
        """
        return type(self)(acoustics.signal.octavepass(self, center, self.fs, fraction=fraction, order=order,
                                                      zero_phase=zero_phase), self.fs)

    def bandpass_frequencies(self, frequencies, order=8, purge=True, zero_phase=False):
        """Apply bandpass filters for frequencies.

        :param frequencies: Band-pass filter frequencies.
        :type frequencies: Instance of :class:`acoustics.signal.Frequencies`
        :param order: Filter order.
        :param purge: Discard bands of which the upper corner frequency is above the Nyquist frequency.
        :param zero_phase: Prevent phase error by filtering in both directions (filtfilt).
        :returns: Frequencies and band-pass filtered signal.

        .. seealso:: :func:`acoustics.signal.bandpass_frequencies`
        """
        frequencies, filtered = acoustics.signal.bandpass_frequencies(self, self.fs, frequencies, order, purge,
                                                                      zero_phase=zero_phase)
        return frequencies, type(self)(filtered, self.fs)

    def octaves(self, frequencies=NOMINAL_OCTAVE_CENTER_FREQUENCIES, order=8, purge=True, zero_phase=False):
        """Apply 1/1-octaves bandpass filters.

        :param frequencies: Band-pass filter frequencies.
        :type frequencies: :class:`np.ndarray` with (approximate) center-frequencies or an instance of :class:`acoustics.signal.Frequencies`
        :param order: Filter order.
        :param purge: Discard bands of which the upper corner frequency is above the Nyquist frequency.
        :param zero_phase: Prevent phase error by filtering in both directions (filtfilt).
        :returns: Frequencies and band-pass filtered signal.

        .. seealso:: :func:`acoustics.signal.bandpass_octaves`
        """
        frequencies, octaves = acoustics.signal.bandpass_octaves(self, self.fs, frequencies, order, purge,
                                                                 zero_phase=zero_phase)
        return frequencies, type(self)(octaves, self.fs)

    def third_octaves(self, frequencies=NOMINAL_THIRD_OCTAVE_CENTER_FREQUENCIES, order=8, purge=True, zero_phase=False):
        """Apply 1/3-octaves bandpass filters.

        :param frequencies: Band-pass filter frequencies.
        :type frequencies: :class:`np.ndarray` with (approximate) center-frequencies or an instance of :class:`acoustics.signal.Frequencies`
        :param order: Filter order.
        :param purge: Discard bands of which the upper corner frequency is above the Nyquist frequency.
        :param zero_phase: Prevent phase error by filtering in both directions (filtfilt).
        :returns: Frequencies and band-pass filtered signal.

        .. seealso:: :func:`acoustics.signal.bandpass_third_octaves`
        """
        frequencies, octaves = acoustics.signal.bandpass_third_octaves(self, self.fs, frequencies, order, purge,
                                                                       zero_phase=zero_phase)
        return frequencies, type(self)(octaves, self.fs)

    def fractional_octaves(self, frequencies=None, fraction=1, order=8, purge=True, zero_phase=False):
        """Apply 1/N-octaves bandpass filters.

        :param frequencies: Band-pass filter frequencies.
        :type frequencies: Instance of :class:`acoustics.signal.Frequencies`
        :param fraction: Default band-designator of fractional-octaves.
        :param order: Filter order.
        :param purge: Discard bands of which the upper corner frequency is above the Nyquist frequency.
        :param zero_phase: Prevent phase error by filtering in both directions (filtfilt).
        :returns: Frequencies and band-pass filtered signal.

        .. seealso:: :func:`acoustics.signal.bandpass_fractional_octaves`
        """
        if frequencies is None:
            frequencies = acoustics.signal.OctaveBand(fstart=NOMINAL_THIRD_OCTAVE_CENTER_FREQUENCIES[0],
                                                      fstop=self.fs / 2.0, fraction=fraction)
        frequencies, octaves = acoustics.signal.bandpass_fractional_octaves(self, self.fs, frequencies, fraction, order,
                                                                            purge, zero_phase=zero_phase)
        return frequencies, type(self)(octaves, self.fs)

    def plot_octaves(self, **kwargs):
        """Plot octaves.

        .. seealso:: :meth:`octaves`

        """
        params = {
            'xscale': 'log',
            'yscale': 'linear',
            'xlabel': '$f$ in Hz',
            'ylabel': '$L_{p}$ in dB',
            'title': '1/1-Octaves SPL',
        }
        params.update(kwargs)
        f, o = self.octaves()
        print(len(f.center), len(o.leq()))
        return _base_plot(f.center, o.leq().T, params)

    def plot_third_octaves(self, **kwargs):
        """Plot 1/3-octaves.

        .. seealso:: :meth:`third_octaves`

        """
        params = {
            'xscale': 'log',
            'yscale': 'linear',
            'xlabel': '$f$ in Hz',
            'ylabel': '$L_{p}$ in dB',
            'title': '1/3-Octaves SPL',
        }
        params.update(kwargs)
        f, o = self.third_octaves()
        return _base_plot(f.center, o.leq().T, params)

    def plot_fractional_octaves(self, frequencies=None, fraction=1, order=8, purge=True, zero_phase=False, **kwargs):
        """Plot fractional octaves.
        """
        title = '1/{}-Octaves SPL'.format(fraction)

        params = {
            'xscale': 'log',
            'yscale': 'linear',
            'xlabel': '$f$ in Hz',
            'ylabel': '$L_p$ in dB',
            'title': title,
        }
        params.update(kwargs)
        f, o = self.fractional_octaves(frequencies=frequencies, fraction=fraction, order=order, purge=purge,
                                       zero_phase=zero_phase)
        return _base_plot(f.center, o.leq().T, params)

    def plot(self, **kwargs):
        """Plot signal as function of time. By default the entire signal is plotted.

        :param filename: Name of file.
        :param start: First sample index.
        :type start: Start time in seconds from start of signal.
        :param stop: Last sample index.
        :type stop: Stop time in seconds. from stop of signal.
        """
        params = {
            'xscale': 'linear',
            'yscale': 'linear',
            'xlabel': '$t$ in s',
            'ylabel': '$x$ in -',
            'title': 'Signal',
        }
        params.update(kwargs)
        return _base_plot(self.times(), self, params)

    #def plot_scalo(self, filename=None):
    #"""
    #Plot scalogram
    #"""
    #from scipy.signal import ricker, cwt

    #wavelet = ricker
    #widths = np.logspace(-1, 3.5, 10)
    #x = cwt(self, wavelet, widths)

    #interpolation = 'nearest'

    #from matplotlib.ticker import LinearLocator, AutoLocator, MaxNLocator
    #majorLocator = LinearLocator()
    #majorLocator = MaxNLocator()

    #fig = plt.figure()
    #ax = fig.add_subplot(111)
    #ax.set_title('Scaleogram')
    ##ax.set_xticks(np.arange(0, x.shape[1])*self.fs)
    ##ax.xaxis.set_major_locator(majorLocator)

    ##ax.imshow(10.0 * np.log10(x**2.0), interpolation=interpolation, aspect='auto', origin='lower')#, extent=[0, 1, 0, len(x)])
    #ax.pcolormesh(np.arange(0.0, x.shape[1])/self.fs, widths, 10.0*np.log(x**2.0))
    #if filename:
    #fig.savefig(filename)
    #else:
    #return fig

    #def plot_scaleogram(self, filename):
    #"""
    #Plot scaleogram
    #"""
    #import pywt

    #wavelet = 'dmey'
    #level = pywt.dwt_max_level(len(self), pywt.Wavelet(wavelet))
    #print level
    #level = 20
    #order = 'freq'
    #interpolation = 'nearest'

    #wp = pywt.WaveletPacket(self, wavelet, 'sym', maxlevel=level)
    #nodes = wp.get_level(level, order=order)
    #labels = [n.path for n in nodes]
    #values = np.abs(np.array([n.data for n in nodes], 'd'))

    #fig = plt.figure()
    #ax = fig.add_subplot(111)
    #ax.set_title('Scaleogram')
    #ax.imshow(values, interpolation=interpolation, aspect='auto', origin='lower', extent=[0, 1, 0, len(values)])
    ##ax.set_yticks(np.arange(0.5, len(labels) + 0.5))
    ##ax.set_yticklabels(labels)

    #fig.savefig(filename)

    def normalize(self, gap=6.0, inplace=False):
        """Normalize signal.

        :param gap: Gap between maximum value and ceiling in decibel.
        :param inplace: Normalize signal in place.

        The parameter `gap` can be understood as using `gap` decibels fewer for the dynamic range.
        By default a 6 decibel gap is used.

        """
        factor = (np.abs(self).max() * 10.0**(gap/20.0))
        if inplace:
            self /= factor[..., None]
            return self
        else:
            return self / factor[..., None]

    def to_wav(self, filename, depth=16):
        """Save signal as WAV file.

        :param filename: Name of file to save to.
        :param depth: If given, convert to integer with specified depth. Else, try to store using the original data type.

        By default, this function saves a normalized 16-bit version of the signal with at least 6 dB range till clipping occurs.

        """
        data = self
        dtype = data.dtype if not depth else 'int' + str(depth)
        if depth:
            data = (data * 2**(depth - 1) - 1).astype(dtype)
        wavfile.write(filename, int(self.fs), data.T)
        #wavfile.write(filename, int(self.fs), self._data/np.abs(self._data).max() *  0.5)
        #wavfile.write(filename, int(self.fs), np.int16(self._data/(np.abs(self._data).max()) * 32767) )

    @classmethod
    def from_wav(cls, filename, normalize=True):
        """
        Create an instance of `Signal` from a WAV file.

        :param filename: Filename
        :param normalize: Whether to normalize the signal.

        """
        fs, data = wavfile.read(filename)
        data = data.astype(np.float32, copy=False).T
        if normalize:
            data /= np.max(np.abs(data))
        return cls(data, fs=fs)

_PLOTTING_PARAMS = {
    'title': None,
    'xlabel': None,
    'ylabel': None,
    'xscale': 'linear',
    'yscale': 'linear',
    'xlim': (None, None),
    'ylim': (None, None),
    'labels': None,
    'linestyles': ['-', '-.', '--', ':'],
}

def _get_plotting_params():
    d = dict()
    d.update(_PLOTTING_PARAMS)
    return d

def _base_plot(x, y, given_params):
    """Common function for creating plots.

    :returns: Axes object.
    :rtype: :class:`matplotlib.Axes`
    """

    params = _get_plotting_params()
    params.update(given_params)

    linestyles = itertools.cycle(iter(params['linestyles']))

    # Check if an axes object is passed in. Otherwise, create one.
    ax0 = params.get('ax', plt.figure().add_subplot(111))

    ax0.set_title(params['title'])
    if y.ndim > 1:
        for channel in y:
            ax0.plot(x, channel, linestyle=next(linestyles))
    else:
        ax0.plot(x, y)
    ax0.set_xlabel(params['xlabel'])
    ax0.set_ylabel(params['ylabel'])
    ax0.set_xscale(params['xscale'])
    ax0.set_yscale(params['yscale'])
    ax0.set_xlim(params['xlim'])
    ax0.set_ylim(params['ylim'])

    if params['labels'] is None and y.ndim > 1:
        params['labels'] = np.arange(y.shape[-2]) + 1
    if params['labels'] is not None:
        ax0.legend(labels=params['labels'])

    return ax0

__all__ = ["Signal"]


from typing import Dict, Optional, Sequence, Union

import numpy as np
import astropy.units as u

from zodipy._astroquery import query_target_positions
from zodipy._emissivities import get_emissivities
from zodipy._integration_config import integration_config_registry
from zodipy._simulation import instantaneous_emission, time_ordered_emission
from zodipy.models import model_registry

class InterplanetaryDustModel:
    """The Zodipy simulation interface.

    The Interplanetary Dust Model used by Zodipy is the Kelsall et al. (1998)
    Interplanetary Dust Model, which includes five (six) Zodiacal components:
        - The Diffuse Cloud (cloud)
        - Three Asteroidal Bands (band1, band2, band3)
        - The Circumsolar Ring (ring) + The Earth-trailing Feature (feature)

    Optionally, it is possible to scale the K98 emission with component
    specific emissivities, as done by the Planck Collaboration. This is
    achieved by specifiying one of the following implemented models:
        - Planck13 (all five Kelsall components + emissivity fits for each)
        - Planck15 (cloud + bands + new emissivity fits)
        - Planck18 (cloud + bands + the latest emissivity fits)

    NOTE: No extrapolation is done when using the fitted emissivities, and as
    such, these models can only be evaluated within the frequency range covered
    by the Planck HFI Bands for which the emissivities were fitted.
    """

    def __init__(self, model: str = "K98") -> None:
        """Initializes the interface given an Interplanetary Dust Model.

        Parameters
        ----------
        model
            The name of the model to initialize.
        """

        self.model = model_registry.get_model(model)
        integration_config = integration_config_registry.get_config("default")
        self.line_of_sights = [
            line_of_sight for line_of_sight in integration_config.values()
        ]

    @u.quantity_input(freq_or_wavelength=("Hz", "m", "micron"))
    def get_instantaneous_emission(
        self,
        freq_or_wavelength: u.Quantity,
        nside: int,
        *,
        observer: str = "L2",
        epochs: Optional[Union[float, Sequence[float], Dict[str, str]]] = None,
        return_comps: bool = False,
        coord_out: str = "E",
    ) -> Union[np.ndarray, Dict[str, np.ndarray]]:
        """Simulates and returns the instantaneous Zodiacal Emission [MJy/sr].

        By instantaneous emission we mean the emission observed at an instant
        in time. If multiple epochs are given, the returned emission will be
        the mean of all simulated instantaneous observations.

        The observer location, given by the parameter `observer` (and
        optionally the location of the Earth if either of the Feature or the
        Ring components are included in the selected Interplanetary Dust Model)
        are queried from the Horizons JPL ephemerides, given some epoch defined
        by the `epochs` parameter.

        NOTE: This function returns the fullsky emission from at a single time.
        This means that we in the simulation evaluate line-of-sights that
        sometimes points directly towards the inner Solar System and through
        the Sun, where the dust density increases exponentially. Such
        line-of-sights are unlikely to be observed by an actual observer, and
        as such, the simulated emission will appear very bright in these
        regions.

        Parameters
        ----------
        freq_or_wavelength
            Frequency or wavelength at which to evaluate the Zodiacal emission.
        nside
            HEALPIX map resolution parameter of the returned emission map.
        observer
            The name of the observer for which we quiery its location given
            the `epochs` parameter. Defaults to 'L2'.
        epochs
            The observeration times given as a single epoch, or a list of epochs
            in JD or MJD format, or a dictionary defining a range of times and
            dates; the range dictionary has to be of the form
            {'start':'YYYY-MM-DD [HH:MM:SS]', 'stop':'YYYY-MM-DD [HH:MM:SS]',
            'step':'n[y|d|h|m|s]'}. If no epochs are provided, the current time
            is used in UTC.
        return_comps
            If True, the emission is returned component-wise in a dictionary.
            Defaults to False.
        coord_out
            Coordinate frame of the output map. Defaults to 'E' (heliocentered
            ecliptic coordinates).

        Returns
        -------
        emission
            Simulated (mean) instantaneous Zodiacal emission [MJy/sr].
        """

        observer_positions = query_target_positions(observer, epochs)
        if self.model.includes_earth_neighboring_components:
            earth_positions = query_target_positions("earth", epochs)
        else:
            earth_positions = observer_positions.copy()

        emissivities = get_emissivities(
            ν_or_λ=freq_or_wavelength,
            emissivity=self.model.emissivities,
            components=list(self.model.components.keys()),
        )

        freq = freq_or_wavelength.to("Hz", equivalencies=u.spectral()).value
        emission = instantaneous_emission(
            nside=nside,
            freq=freq,
            components=list(self.model.components.values()),
            emissivities=emissivities,
            observer_positions=observer_positions,
            earth_positions=earth_positions,
            line_of_sights=self.line_of_sights,
            coord_out=coord_out,
        )

        if return_comps:
            return {
                comp.value: emission[idx]
                for idx, comp in enumerate(self.model.components)
            }

        return emission.sum(axis=0)

    @u.quantity_input(freq_or_wavelength=("Hz", "m", "micron"))
    def get_time_ordered_emission(
        self,
        freq_or_wavelength: u.Quantity,
        nside: int,
        *,
        pixels: np.ndarray,
        observer_position: np.ndarray,
        earth_position: Optional[np.ndarray] = None,
        bin: bool = False,
        return_comps: bool = False,
        coord_out: str = "E",
    ) -> Union[np.ndarray, Dict[str, np.ndarray]]:
        """Simulates and returns the Zodiacal emission [MJy/sr] in a timestream.

        Given a sequence of time-ordered pixels, the Zodiacal emission is
        evaluated from a constant location in space given by the
        `observer_position`. The `earth_position` is required for Interplanetary
        Dust models that include the Earth-trailing Feature and Circum-solar
        Ring components.

        Parameters
        ----------
        freq_or_wavelength
            Frequency or wavelength at which to evaluate the Zodiacal emission.
        nside
            HEALPIX map resolution parameter of the returned emission map.
        pixels
            Sequence of time-ordered pixels.
        observer_position
            The heliocentric ecliptic cartesian position of the observer at
            the time of observing the tods.
        earth_position
            The heliocentric ecliptic cartesian position of the Earth at the
            time of observing the tods. If None, the observer is assumed to be
            the Earth. Defaults to None.
        bin
            If True, the time-ordered sequence of emission per pixel is binned
            into a HEALPIX map. Defaults to False.
        return_comps
            If True, the emission is returned component-wise in a dictionary.
            Defaults to False.
        coord_out
            Coordinate frame of the output map. Defaults to 'E' (heliocentric
            ecliptic coordinates).

        Returns
        -------
        emission
            Simulated timestream of Zodiacal emission [MJy/sr] (optionally
            binned into a HEALPIX map).
        """

        if earth_position is None:
            earth_position = observer_position

        emissivities = get_emissivities(
            ν_or_λ=freq_or_wavelength,
            emissivity=self.model.emissivities,
            components=list(self.model.components.keys()),
        )

        freq = freq_or_wavelength.to("Hz", equivalencies=u.spectral()).value
        emission = time_ordered_emission(
            nside=nside,
            freq=freq,
            components=list(self.model.components.values()),
            emissivities=emissivities,
            line_of_sights=self.line_of_sights,
            observer_position=observer_position,
            earth_position=earth_position,
            pixel_chunk=pixels,
            bin=bin,
            coord_out=coord_out,
        )

        if return_comps:
            return {
                comp.value: emission[idx]
                for idx, comp in enumerate(self.model.components)
            }

        return emission.sum(axis=0)

    def __str__(self) -> str:
        """String representation of the InterplanetaryDustModel."""

        reprs = []
        for label, component in self.model.components.items():
            component_repr = f"{component.__class__.__name__}" + "\n"
            reprs.append(f"({label.value}): {component_repr}")

        main_repr = "InterplanetaryDustModel("
        main_repr += f"\n  name: {self.model.name}"
        main_repr += "\n  components( "
        main_repr += "\n    " + "    ".join(reprs)
        main_repr += "  )"
        main_repr += "\n)"

        return main_repr


import os
import subprocess
import glob
import sys

import numpy as np
from scipy.integrate import simps
from natsort import natsorted
from astropy import units, constants

path_program = os.path.dirname( os.path.dirname( os.path.realpath(__file__) ) ) + '/'

class galaxy_sed(object):

  def __init__(self,
         params,
         resolution = 'lr',
         model_name = 'Padova1994',
         library = 'BaSeL',
         library_vs = 2003,
         units = 'flambda',
         w1lambda = '700',
         w2lambda = '20000',
         SFH_tt = 0,
         SFH_sf = 0):

    '''
    metallicity:   Initial metallicity of the galaxy. Options
             supported:
             0.0001, 0.0004, 0.004, 0.008, 0.02, 0.05, 0.1 [Padova1994]
             0.0004, 0.001, 0.004, 0.008, 0.019, 0.03 [Padova2000]
    imf:       IMF options:
              salpeter, chabrier, and kroupa
    resolution:    Resolution of BC03 templates
              - 'hr':     High resolution
              - 'lr':     Low resolution
    library:     Specify specific library (only valid for 2012 version) -- 'stelib','BaSeL'
    library_vs: Specify which version of BC03 -- 2003, 2012
    dust:      Dust attenuation embedded in GALAXEV or post-processing
             Dictionary:
             flag : 0 GALAXEV 1 post-processing
             GALAXEV option (Bruzual & Fall 2000, BC03 eq.6)
              tau_V : grid
              tau_V from ISM : 0.3 with scatter
             Post-processing
              ext_law : calzetti, fitzpatrick07, fitzpatrick, odonnell, cardelli
              reddening : grid
    sed:       Return SED
             flag Y or N
              units Units to return the SEDs
                'lnu':     ergs/s/Hz
                'llambda':   ergs/s/Angs

              W1, W2 wavelength range to compute the SED
    '''

    # check that all required keys are in params
    required_keys = ['metallicity',
                     'IMF',
                     'dust',
                     'survey_filt',
                     'flag_mag',
                     'em_lines',
                     'flag_IGM_ext',
                     'cosmo',
                     'seed',
                     'work_dir']

    for key in required_keys:
      try:
        val = params[key]
      except KeyError:
        raise Exception(key + ' is not defined in params')

    path_one_lvl   = os.path.dirname(os.path.dirname(__file__)) + '/'
    self.seed    = params['seed']
    self.galaxev_dir = path_one_lvl + 'bc03/'
    self.em_line_dir = path_one_lvl + 'gutkin/'
    # self.work_dir  = path_one_lvl + 'runs/' + params['seed'] + '/'
    self.work_dir  = params['work_dir']
    self.survey_name = params['survey_filt']
    self.survey_filt = path_one_lvl + 'filters/' + params['survey_filt'] + '/'

    self.metallicity   = params['metallicity']
    self.imf       = params['IMF']
    self.dust      = params['dust']
    self.flag_mag    = params['flag_mag']
    self.em_lines    = params['em_lines']
    self.flag_IGM_ext  = params['flag_IGM_ext']

    self.cosmo     = params['cosmo']
    self.resolution  = resolution
    self.model_name  = model_name
    self.library   = library
    self.library_vs  = library_vs
    self.units     = units
    self.w1lambda  = w1lambda
    self.w2lambda  = w2lambda

    self.check_input()

    if(self.imf == 'chab'):
      self.imf2 = 'chabrier'
    if(self.imf == 'kroup'):
      self.imf2 = 'kroupa'
    if(self.imf == 'salp'):
      self.imf2 = 'salpeter'

    self.input_ised = (self.galaxev_dir
               + '/models/'
               + self.model_name
               + '/'
               + self.imf2
               +'/bc'
               + str(library_vs)
               + '_'
               + self.resolution
               + '_'
               + self.library
               + '_'
               + self.metallicity_key[self.metallicity]
               + '_'
               + self.imf
               + '_ssp.ised')

    self.csp_output = self.work_dir + self.seed + '_csp_out'
    self.gpl_output = self.work_dir + self.seed + '_gpl_out'

    # select filters old_version
    # self.select_filters()

    # export everything that we need to run galaxev
    self.define_env()

    if(np.isscalar(SFH_tt)):

      import pickle
      file_SFH  = (path_one_lvl
            + 'runs/SFHs/'
            + 'SFHn_'
            + 'gal_type_'
            + params['gal_type']
            + '.pkl')

      dict_save =  pickle.load( open( file_SFH, "rb" ) )
      aa = np.argmin( abs(params['mpeak'] - dict_save['arr_mpeak']) )
      self.age_SFH = dict_save['arr_tt']
      self.SFH = dict_save['arr_SFH'][aa,:].flatten()

    else:
      self.age_SFH = SFH_tt
      self.SFH = SFH_sf
    # st_mass = simps(self.SFH, self.age_SFH)

    self.file_SFH = self.work_dir + self.seed + '_SFH.txt'
    np.savetxt(self.file_SFH, np.transpose([self.age_SFH, self.SFH]))

    # run CSP GALAXEV
    self.run_galaxev_csp()

  def check_input(self):

    if(self.model_name == 'Padova1994'):
      self.metallicity_key = {0.0001:'m22',
                  0.0004:'m32',
                  0.004:'m42',
                  0.008:'m52',
                  0.02:'m62',
                  0.05:'m72',
                  0.1:'m82'}
    elif(self.model_name == 'Padova2000'):
      self.metallicity_key = {0.0004:'m122',
                  0.001:'m132',
                  0.004:'m142',
                  0.008:'m152',
                  0.019:'m162',
                  0.03:'m72'}

    if self.metallicity not in self.metallicity_key.keys():
      raise Exception('Incorrect metallicity provided: '
              + str(self.metallicity) + '\n' +
              'Please choose from:' + str(self.metallicity_key.keys()))

    imf_keys = ['salp', 'chab', 'kroup']
    if self.imf not in imf_keys:
      raise Exception( 'Incorrect IMF provided: '+ self.imf +'\n'
              + 'Please choose from:' + imf_keys )

    # gal_type_keys = ['sf', 'qs']
    # if self.gal_type not in gal_type_keys:
    #   raise Exception( 'Incorrect gal_type provided: '+ self.gal_type +'\n'
    #           + 'Please choose from:' + gal_type_keys )

    ext_keys = ['N', 'galaxev', 'calzetti', 'fitzpatrick07',
      'fitzpatrick', 'odonnell', 'cardelli']
    if self.dust['flag'] not in ext_keys:
      raise Exception('Dust flag not correct \n' +
              'Please choose from: ' + ext_keys)

    dust_keys = ['tau_V', 'etau_V']
    if(self.dust['flag'] == 'galaxev'):
      if not all (key in self.dust.keys() for key in dust_keys):
        raise Exception('Incorrect keys in dust,' +
          'you have to include tau_V and etau_V')

    dust_keys = ['Av']
    if( (self.dust['flag'] != 'galaxev') & (self.dust['flag'] != 'N') ):
      if not all (key in self.dust.keys() for key in dust_keys):
        raise Exception('Incorrect keys in dust,' +
          'you have to include Av')

    if not os.path.isdir(self.survey_filt):
      raise Exception('Filter directory not found at :'
        + str(self.survey_filt) )

    if os.path.isdir(self.work_dir) == False:
      try:
        os.makedirs(self.work_dir)
      except:
        raise(self.work_dir + ' cannot be created')

    model_name_keys = ['Padova1994', 'Padova2000']
    if self.model_name not in model_name_keys:
      raise Exception('Incorrect model provided: '
              + self.model_name +'\n' +
              'Please choose from:' + model_name_keys )

    resolution_keys = ['hr', 'lr']
    if self.resolution not in resolution_keys:
      raise Exception('Incorrect resolution provided: '
              +str(self.resolution)+'\n' +
              'Please choose from: ' + resolution_keys )

    units_keys = ['flambda', 'fnu']
    if self.units not in units_keys:
      raise Exception('Incorrect flux units provided: '
              + str(self.units) + '\n' +
              'Please choose from: ' + units_keys )

    library_vs_keys = [2003, 2012]
    if self.library_vs not in library_vs_keys:
      raise Exception('Invalid library_vs: '+ self.library_vs
              + '\n' + 'Please choose from: '
              + library_vs_keys)

    library_keys = ['stelib', 'BaSeL']
    if self.library not in library_keys:
      raise Exception('Incorrect library choice: '
              + library_keys +'\n' +
              'Please choose from: ' + library_keys)

  def define_env(self):
    ''' Needed to run GALAXEV.
      Note: this is only working in Linux'''

    self.env_string = ('export FILTERS='
              + self.galaxev_dir
              + 'src/FILTERBIN.RES;'
              + 'export A0VSED='
              + self.galaxev_dir
              + 'src/A0V_KURUCZ_92.SED;'
              + 'export RF_COLORS_ARRAYS='
              + self.galaxev_dir
              + 'src/RF_COLORS.filters;'
              + 'export SUNSED='
              + self.galaxev_dir
              + 'src/SUN_KURUCZ_92.SED;')

  def run_galaxev_csp(self):

    # In the presence of dust attenuation and emission lines, we need
    # to run the code twice. This is because we introduce emission lines
    # a posteriori, and we need to know the impact of dust attenuation
    # on them. This is only true for the galaxev dust attenuation model,
    # as it depends on the SFH. For other models it is not necessary

    if((self.dust['flag'] == 'galaxev') & (self.em_lines['flag'] == 'Y')):
      it = 2
    else:
      it = 1

    for ii in range(it):
      if(ii == 0):
        if(self.dust['flag'] == 'galaxev'):
          flag_dust = ('Y'
                + '\n'
                + self.dust['tau_V']
                + '\n'
                + self.dust['etau_V'])
        else:
          flag_dust = 'N'
        name_out = self.csp_output
      else:
        flag_dust = 'N'
        name_out = self.csp_output + '_nd'

      csp_input = (self.input_ised
             + '\n'
             + flag_dust
             + '\n'
             + '0\n'
             + '6\n'
             + self.file_SFH
             + '\n'
             + name_out
             + '\n')

      csp_input_file = self.work_dir + self.seed + '_csp.in'
      with open(csp_input_file, 'w') as file:
        file.write(csp_input)

      call_string = (self.env_string
              + self.galaxev_dir
              +'src/csp_galaxev < '
              + csp_input_file)

      # call csp_galaxev
      subprocess.call(call_string,
              cwd = self.work_dir,
              shell=True,
              # stdout = open(self.work_dir+'aa.txt', 'w'),
              # stderr = open(self.work_dir+'ab.txt', 'w'))
              stdout=open(os.devnull,'w'),
              stderr=open(os.devnull,'w'))

  def run_galaxev_gpl(self, zz0):

    # output zz, age, wav_em, lum_em

    self.zz = []
    self.age = []

    if(len(zz0) <= 50):
      zz = zz0
      it = 1
    else:
      # 50 is the maximum number of outputs from galaxevpl
      num = np.ceil(len(zz0)/50.)
      zz_arr = np.array_split(zz0, num)
      it = len(zz_arr)

    if((self.dust['flag'] == 'galaxev') & (self.em_lines['flag'] == 'Y')):
      it2 = 2
    else:
      it2 = 1

    for ii in range(it):
      if(len(zz0) > 50):
        zz = zz_arr[ii]

      age = np.array(self.cosmo.age(zz).value)
      self.zz.append(zz)
      self.age.append(np.around(age, decimals=3))
      ages = ','.join(age.astype(str))

      for jj in range(it2):
        if(jj == 0):
          infile = self.csp_output
          outfile = self.gpl_output+str(ii)+'.dat'
        else:
          infile = self.csp_output + '_nd'
          outfile = self.gpl_output+str(ii)+'_nd.dat'

        # call galaxevpl
        gpl_input = (infile
                   + '\n'
                   + ages
                   + '\n'
                   + self.w1lambda
                   + ','
                   + self.w2lambda
                   + '\n'
                   + outfile)

        gpl_input_file = self.work_dir + self.seed + '_gpl.in'
        with open(gpl_input_file, 'w') as file:
          file.write(gpl_input)

        verbose = 0
        if(verbose == 0):
          subprocess.call(self.galaxev_dir
                  + 'src/galaxevpl < '
                  + gpl_input_file,
                  cwd = self.work_dir,
                  shell = True,
                  # stdout = open(self.work_dir+'aa.txt', 'w'),
                  # stderr = open(self.work_dir+'ab.txt', 'w'))
                  stdout = open(os.devnull, 'w'),
                  stderr = open(os.devnull, 'w'))
        else:
          subprocess.call(self.galaxev_dir
                  + 'src/galaxevpl < '
                  + gpl_input_file,
                  cwd = self.work_dir,
                  shell = True,
                  stdout = open(self.work_dir+'aa.txt', 'w'),
                  stderr = open(self.work_dir+'ab.txt', 'w'))

      self.read_gpl(it=ii)

    # once we have read everything, we convert luminosities to
    # fluxes and magnitudes
    self.zz = np.concatenate(self.zz)
    self.n_zz = len(self.zz)
    self.age = np.concatenate(self.age)
    self.lum_em = np.concatenate(self.lum_em, axis=1)

    # apply dust attenuation now if model != galaxev
    if( (self.dust['flag'] != 'galaxev') & (self.dust['flag'] != 'N') ):
        self.apply_dust()

    self.lum2fluxmag()

  def read_gpl(self, it=0):

    # read gpl output (luminosity), introduce emission lines, and
    # apply IGM extinction
    # output wav_em, lum_em

    infile = self.gpl_output + str(it) + '.dat'
    # I want to get F_lambda erg cm^-2 s^-1 A^-1
    # The code is in Lsun/Angstrom
    fil = np.loadtxt(infile)
    if(it == 0):
      # Angstrom
      self.wav_em = fil[:, 0]
      self.lum_em = []

    # units Lsun/Angstrom
    self.lum_em.append(fil[:, 1:])

    # Introduce emission lines
    if(self.em_lines['flag'] == 'Y'):
      if(self.dust['flag'] == 'galaxev'):
        infile_nd = self.gpl_output + str(it) + '_nd.dat'
        fil_em = np.loadtxt(infile_nd)
        dust_att = fil[:, 1:]/fil_em[:, 1:]
      else:
        # multiplicative factor
        dust_att = 1

      self.add_emlines(dust_att = dust_att,
                       it = it,
                       pmetal = self.em_lines['metal_line'],
                       plogio = self.em_lines['log_io'])

    # Apply IGM extinction
    if(self.flag_IGM_ext == 'Y'):
      self.igm_ext(it=it)

  def lum2fluxmag(self, ini_wav=1300, out_wav=110000):

    ind = np.where( (self.wav_em >= ini_wav) & (self.wav_em <= out_wav) )[0]
    self.wav = self.wav_em[ind]
    self.obv_sed = np.zeros((len(self.wav), self.n_zz))

    # flux_lambda_obs = Lum_lambda_em / (4 pi D_L^2 (1+z))
    for ii in range(0, self.n_zz):
      if(self.zz[ii] > 0):
        # move sed to obs wav, interpolate, get results at the
        # self.wav (we want all spectra with the same wav)
        lum_at_obs = np.interp(self.wav,
                     self.wav_em*(1+self.zz[ii]),
                     self.lum_em[:, ii])

        dist_lum = self.cosmo.luminosity_distance(self.zz[ii]).value #Mpc
        num = lum_at_obs * units.L_sun.to('erg/s') # units lum erg s^-1 A^-1
        # BC03 Eq. 8
        #dist lum units Mpc -> cm
        den = 4 * np.pi * (dist_lum * units.Mpc.to('cm'))**2 * (1 + self.zz[ii])
        self.obv_sed[:, ii] = num/den

    if(self.flag_mag == 'Y'):
    # read the filters where we are going to compute magnitudes
      self.read_filters()
      self.compute_obs_mags()

  def compute_obs_mags(self, rest_frame=False):

    # mag = -2.5 * log10 (i1/i2)
    # i1 = dlambda lambda obv_sed * R(lambda)
    # i2 = dlambda lambda C_lambda R(lambda)

    clight = constants.c.to('Angstrom/s').value # A s^-1
    sysAB_nu = 3.631e-20 # ergs s^-1 cm^-2 Hz^-1
    # F_lambda erg cm^-2 s^-1 A^-1

    self.obs_flux = np.zeros( (self.n_filters, self.n_zz) )
    self.obs_mag = np.zeros( (self.n_filters, self.n_zz) )
    if(rest_frame == True):
        self.rest_mag = np.zeros( (self.n_filters, self.n_zz) )

    for jj in range(0, self.n_filters):
      xx = self.filters['wav_'+str(jj)]
      yy0 = xx * self.filters['trans_'+str(jj)]

      den_f = simps(yy0, xx)

      sysAB_lambda = sysAB_nu * clight / xx**2
      yy = sysAB_lambda * yy0
      den_m = simps(yy, xx)

      for ii in range(0, self.n_zz):
        if(self.zz[ii] > 0):

          flux_at_band_wav = np.interp(self.filters['wav_'+str(jj)],
                                       self.wav,
                                       self.obv_sed[:, ii])

          yy = flux_at_band_wav * yy0
          num = simps(yy, xx)

          self.obs_mag[jj, ii] = -2.5 * np.log10(num/den_m)
          self.obs_flux[jj, ii] = num/den_f

          if(rest_frame == True):
              # rest frame mag for color ev. (amplitude wrong)
              flux_at_band_wav = np.interp(self.filters['wav_'+str(jj)],
                                           self.wav_em,
                                           self.lum_em[:, ii])
              yy = flux_at_band_wav * yy0
              num = simps(yy, xx)
              self.rest_mag[jj, ii] = -2.5 * np.log10(num/den_m)

  # best-fit parameters to SDSS 7 data (0.04<z<0.2)
  # standard model
  # vary pmetal
  def add_emlines(self,
                  it = 0,
                  dust_att = 0,
                  pmetal = 0.014,
                  plogio = -3.5,
                  pd2m = 0.3,
                  phden = 100,
                  pC20 = 1., # C0 solar
                  pIMF = 100):

    # col 1 log ionization param
    # keys_logio = -(np.arange(7)/2. + 1)
    # -1, -4
    # plogio = -1
    # very important .44, .1, .2, .05

    # col 2 dust-to-metal ratio
    # keys_dust2metal = np.array([0.1, 0.3, 0.5])
    # pd2m = 0.3
    # important .2, .05

    # col 3 hydrogen gas density (per cubic cm)
    # keys_hden = np.array([100, 1000])
    # phden = 100
    # not very important .05, .06

    # col 4 C/O ratio in units of solar value
    # keys_C2O = np.array([0.10, 0.14, 0.20, 0.27, 0.38, 0.52,
    #   0.72, 1.00, 1.40])
    # pC20 = 0.38
    # not important, less than .05

    # col 5 cutoff IMF
    # keys_IMF = np.array([100, 300])
    # pIMF = 100
    # important .24, .14, .1

    # col 6-23:

    #[OII]3727
    # Hbeta
    #[OIII]4959
    #[OIII]5007
    #[NII]6548
    #Halpha
    #[NII]6584
    #[SII]6717
    #[SII]6731
    #NV1240
    #CIV1548
    #CIV1551
    #HeII1640
    #OIII]1661
    #OIII]1666
    #[SiIII]1883
    #SiIII]1888
    #CIII]1908

    if(it == 0):
      # Using line ratios from Gutkin et al. 2016
      keys_metal = np.array([0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.004,
        0.006, 0.008, 0.010, 0.014, 0.017, 0.020, 0.030, 0.040])

      ii = np.argmin( abs(pmetal - keys_metal) )
      frac, _ = np.modf(keys_metal[ii])
      str0 = str(frac)[2:]
      if(len(str0) < 3):
        str0 += '0'

      self.line_wav = np.array([3727, 4862, 4959, 5007, 6548, 6564,
        6584, 6717, 6731, 1240, 1548, 1551, 1640, 1661, 1666,
        1883, 1888, 1908])

      file_in = 'nebular_emission_Z' + str0 + '.txt'
      with open(self.em_line_dir + file_in, 'r') as file:
        fil = np.loadtxt(file)
      aa = np.where( (fil[:,0] == plogio) &
               (fil[:,1] == pd2m) &
               (fil[:,2] == phden) &
               (fil[:,3] == pC20) &
               (fil[:,4] == pIMF) )

      # Lsun per unit SFR [Msun/yr]
      self.line_rat = fil[aa, 5:].flatten()

    # Lyman alpha 1216
    # Ly alpha to H alpha ratio 8.7 (Hu et al. 1998)
    # From 9.1 to 11.6 Hummer & Storey et al. 1987
    # due to absorption in the position of the line, we have to
    # multiply the line intensity by a factor of a few to see a line!
    # the problem is that we need to include nebular emission
    # this is for the future
    # lyabs = 1.e3
    # self.line_wav = np.append(self.line_wav, [1216])
    # self.line_rat = np.append(self.line_rat, [self.line_wav[5] * 8.7 * lyabs])

    # position of lines
    ind_lines = np.zeros(self.line_wav.shape[0], dtype=int)
    # account for resolution of the spectrum
    res_lambda = np.zeros(self.line_wav.shape[0])

    # number of lines
    nlines = len(self.line_wav)
    for jj in range(0, nlines):
      ind_lines[jj] = np.argmin( abs(self.line_wav[jj] - self.wav_em) )
      res_lambda[jj] = abs(self.wav_em[ind_lines[jj]] - self.wav_em[ind_lines[jj]+1])

    flag0 = np.isscalar(dust_att)
    # introduce lines
    len_arr = self.lum_em[it].shape[1]
    for ii in range(0, len_arr):
      ind = np.argmin( abs(self.age_SFH - self.age[it][ii]*1.e9) )
      lum_lines = self.line_rat * self.SFH[ind]
      if(flag0 == True):
        self.lum_em[it][ind_lines, ii] += lum_lines / res_lambda
      else:
        self.lum_em[it][ind_lines, ii] += lum_lines * dust_att[ind_lines, ii] / res_lambda

    # introduce properly lines only in high resolution spectra
    # line_width 4 A
    # sigma_line = 4
    # for ii in range(0, self.n_zz):
    #   ind = np.argmin( abs(self.age_SFH - self.age[ii]) )
    #   SFR = self.SFH[ind]
    #   lum_lines = self.line_rat * SFR
    #   for jj in range(0, nlines):
    #     norm = (1./np.sqrt(2*np.pi)/sigma_line) * lum_lines[jj]
    #     line_prof = norm * np.exp(-0.5 * (self.wav_em - self.line_wav[jj])**2 / sigma_line**2)
    #     self.lum_em[:, ii] += line_prof

  def read_filters(self):
    # read filters
    filter_files = glob.glob(self.survey_filt
                 + self.survey_name
                 + "*")
    filter_files = natsorted(filter_files)

    self.n_filters = len(filter_files)
    self.filter_pivot = np.zeros(self.n_filters)
    self.filters = {}

    for ii, tempfile in enumerate(filter_files):
      with open(tempfile, 'r') as file_temp:
        fil = np.loadtxt(file_temp)
        self.filters['wav_'+ str(ii)] = fil[:,0]
        self.filters['trans_'+ str(ii)] = fil[:,1]
        # pivot wavelength Eq. A11 Tokunaga & Vacca 2005
        num = simps(fil[:,0] * fil[:,1], fil[:,0])
        den = simps(fil[:,1] / fil[:,0], fil[:,0])
        self.filter_pivot[ii] = np.sqrt(num/den)

  def igm_ext(self, it=0):
    # Becker et al. 2015
    # Extinction shortward Lyman alpha
    wav_LA = 1216.
    sig_LA = 44.88

    wav_LB = 1026.
    sig_LB = 7.18

    wav_LG = 972.
    sig_LG = 2.50

    len_arr = self.lum_em[it].shape[1]
    for ii in range(0, len_arr):

      # Barnett et al. 2017 Eq. 1
      if(self.zz[it][ii] <= 5.5):
        tauLA = 0.85 * ( (1.+self.zz[it][ii])/5. )**4.3
      else:
        tauLA = 2.63 * ( (1.+self.zz[it][ii])/6.5 )**11.
      tauLB = sig_LB/sig_LA*tauLA
      tauLG = sig_LG/sig_LA*tauLA

      extLA = np.exp(-tauLA)
      extLB = np.exp(-tauLB)
      extLG = np.exp(-tauLG)

      ind_LA = np.where(self.wav_em < wav_LA)[0]
      ind_LB = np.where(self.wav_em < wav_LB)[0]
      ind_LG = np.where(self.wav_em < wav_LG)[0]

      self.lum_em[it][ind_LA, ii] *= extLA
      self.lum_em[it][ind_LB, ii] *= extLB
      self.lum_em[it][ind_LG, ii] *= extLG

  def apply_dust(self):

    import extinction

    for ii in range(self.n_zz):
      if self.dust['flag'] == 'calzetti':
        self.lum_em[:, ii] = extinction.apply(extinction.calzetti00(self.wav_em,
          self.dust['Av'], 4.05), self.lum_em[:, ii])
      elif self.dust['flag'] == 'cardelli':
        self.lum_em[:, ii] = extinction.apply(extinction.ccm89(self.wav_em,
          self.dust['Av'], 4.05), self.lum_em[:, ii])
      elif self.dust['flag'] == 'odonnell':
        self.lum_em[:, ii] = extinction.apply(extinction.odonnell94(self.wav_em,
          self.dust['Av'], 4.05), self.lum_em[:, ii])
      elif self.dust['flag'] == 'fitzpatrick':
        self.lum_em[:, ii] = extinction.apply(extinction.fitzpatrick99(self.wav_em,
          self.dust['Av'], 3.1), self.lum_em[:, ii])
      elif self.dust['flag'] == 'fitzpatrick07':
        self.lum_em[:, ii] = extinction.apply(extinction.fm07(self.wav_em,
          self.dust['Av']), self.lum_em[:, ii])

    # for ii in range(0, self.n_zz):
    #   if self.dust['flag'] == 'calzetti':
    #     self.obv_sed[:, ii] = extinction.apply(extinction.calzetti00(self.wav,
    #       self.dust['Av'], 4.05), self.obv_sed[:, ii])
    #   elif self.dust['flag'] == 'cardelli':
    #     self.obv_sed[:, ii] = extinction.apply(extinction.ccm89(self.wav,
    #       self.dust['Av'], 4.05), self.obv_sed[:, ii])
    #   elif self.dust['flag'] == 'odonnell':
    #     self.obv_sed[:, ii] = extinction.apply(extinction.odonnell94(self.wav,
    #       self.dust['Av'], 4.05), self.obv_sed[:, ii])
    #   elif self.dust['flag'] == 'fitzpatrick':
    #     self.obv_sed[:, ii] = extinction.apply(extinction.fitzpatrick99(self.wav,
    #       self.dust['Av'], 3.1), self.obv_sed[:, ii])
    #   elif self.dust['flag'] == 'fitzpatrick07':
    #     self.obv_sed[:, ii] = extinction.apply(extinction.fm07(self.wav,
    #       self.dust['Av']), self.obv_sed[:, ii])

  # def select_filters_old(self):
  #   # create new file by appending new filters at the end
  #   # of filterfrm.res

  #   file_name = 'filterfrm.res'

  #   file_all_filt = self.galaxev_dir + 'src/' + file_name
  #   file_all_filt_temp = self.work_dir + file_name
  #   if os.path.isfile(file_all_filt):
  #     shutil.copyfile(file_all_filt, file_all_filt_temp)
  #   else:
  #     raise Exception('The file ' + file_all_filt + ' does not exist')

  #   filter_files = glob.glob(self.survey_filt + self.survey_name + "*")
  #   filter_files = natsorted(filter_files)

  #   with open(file_all_filt_temp, 'a+') as file:
  #     for tempfile in filter_files:
  #       with open(tempfile, 'r') as file_temp:
  #         tempfile_name_ext = os.path.basename(tempfile)
  #         t_name, t_ext = os.path.splitext(tempfile_name_ext)
  #         file.write('# ' + t_name + '\n')
  #         file.write(file_temp.read())

  #   input_string = (file_all_filt_temp
  #           + '\nn\ny\n'
  #           + self.galaxev_dir
  #           + 'src/FILTERBIN.RES')
  #   input_file = self.work_dir + self.seed + '_filt.in'
  #   with open(input_file, 'w') as file:
  #     file.write(input_string)

  #   subprocess.call(self.galaxev_dir
  #           + 'src/add_filters < '
  #           + input_file,
  #           cwd = self.work_dir,
  #           shell = True)

  #   file_name = 'filters.log'
  #   shutil.copyfile(self.work_dir + file_name,
  #           self.galaxev_dir + 'src/' + file_name)