import os
import sys
import warnings

import numpy as np
import math

from ...core import utils
from ...core.utils import logger
from .. import utils as gwutils
from .interferometer import Interferometer
from .psd import PowerSpectralDensity
from .strain_data import InterferometerStrainData


class InterferometerList(list):
    """ A list of Interferometer objects """

    def __init__(self, interferometers):
        """ Instantiate a InterferometerList

        The InterferometerList is a list of Interferometer objects, each
        object has the data used in evaluating the likelihood

        Parameters
        ==========
        interferometers: iterable
            The list of interferometers
        """

        super(InterferometerList, self).__init__()
        if type(interferometers) == str:
            raise TypeError("Input must not be a string")
        for ifo in interferometers:
            if type(ifo) == str:
                ifo = get_empty_interferometer(ifo)
            if type(ifo) not in [Interferometer, TriangularInterferometer]:
                raise TypeError("Input list of interferometers are not all Interferometer objects")
            else:
                self.append(ifo)
        self._check_interferometers()

    def _check_interferometers(self):
        """Verify IFOs 'duration', 'start_time', 'sampling_frequency' are the same.

        If the above attributes are not the same, then the attributes are checked to
        see if they are the same up to 5 decimal places.

        If both checks fail, then a ValueError is raised.
        """
        consistent_attributes = ['duration', 'start_time', 'sampling_frequency']
        for attribute in consistent_attributes:
            x = [getattr(interferometer.strain_data, attribute)
                 for interferometer in self]
            try:
                if not all(y == x[0] for y in x):
                    ifo_strs = ["{ifo}[{attribute}]={value}".format(
                        ifo=ifo.name,
                        attribute=attribute,
                        value=getattr(ifo.strain_data, attribute))
                        for ifo in self]
                    raise ValueError(
                        "The {} of all interferometers are not the same: {}".format(
                            attribute, ', '.join(ifo_strs)))
            except ValueError as e:
                if not all(math.isclose(y, x[0], abs_tol=1e-5) for y in x):
                    raise ValueError(e)
                else:
                    logger.warning(e)

    def set_strain_data_from_power_spectral_densities(self, sampling_frequency, duration, start_time=0):
        """ Set the `Interferometer.strain_data` from the power spectral densities of the detectors

        This uses the `interferometer.power_spectral_density` object to set
        the `strain_data` to a noise realization. See
        `bilby.gw.detector.InterferometerStrainData` for further information.

        Parameters
        ==========
        sampling_frequency: float
            The sampling frequency (in Hz)
        duration: float
            The data duration (in s)
        start_time: float
            The GPS start-time of the data

        """
        for interferometer in self:
            interferometer.set_strain_data_from_power_spectral_density(sampling_frequency=sampling_frequency,
                                                                       duration=duration,
                                                                       start_time=start_time)

    def set_strain_data_from_zero_noise(self, sampling_frequency, duration, start_time=0):
        """ Set the `Interferometer.strain_data` from the power spectral densities of the detectors

        This uses the `interferometer.power_spectral_density` object to set
        the `strain_data` to zero noise. See
        `bilby.gw.detector.InterferometerStrainData` for further information.

        Parameters
        ==========
        sampling_frequency: float
            The sampling frequency (in Hz)
        duration: float
            The data duration (in s)
        start_time: float
            The GPS start-time of the data

        """
        for interferometer in self:
            interferometer.set_strain_data_from_zero_noise(sampling_frequency=sampling_frequency,
                                                           duration=duration,
                                                           start_time=start_time)

    def inject_signal(self, parameters=None, injection_polarizations=None, waveform_generator=None):
        """ Inject a signal into noise in each of the three detectors.

        Parameters
        ==========
        parameters: dict
            Parameters of the injection.
        injection_polarizations: dict
           Polarizations of waveform to inject, output of
           `waveform_generator.frequency_domain_strain()`. If
           `waveform_generator` is also given, the injection_polarizations will
           be calculated directly and this argument can be ignored.
        waveform_generator: bilby.gw.waveform_generator.WaveformGenerator
            A WaveformGenerator instance using the source model to inject. If
            `injection_polarizations` is given, this will be ignored.

        Notes
        ==========
        if your signal takes a substantial amount of time to generate, or
        you experience buggy behaviour. It is preferable to provide the
        injection_polarizations directly.

        Returns
        =======
        injection_polarizations: dict

        """
        if injection_polarizations is None:
            if waveform_generator is not None:
                injection_polarizations = \
                    waveform_generator.frequency_domain_strain(parameters)
            else:
                raise ValueError(
                    "inject_signal needs one of waveform_generator or "
                    "injection_polarizations.")

        all_injection_polarizations = list()
        for interferometer in self:
            all_injection_polarizations.append(
                interferometer.inject_signal(parameters=parameters, injection_polarizations=injection_polarizations))

        return all_injection_polarizations

    def save_data(self, outdir, label=None):
        """ Creates a save file for the data in plain text format

        Parameters
        ==========
        outdir: str
            The output directory in which the data is supposed to be saved
        label: str
            The string labelling the data
        """
        for interferometer in self:
            interferometer.save_data(outdir=outdir, label=label)

    def plot_data(self, signal=None, outdir='.', label=None):
        if utils.command_line_args.bilby_test_mode:
            return

        for interferometer in self:
            interferometer.plot_data(signal=signal, outdir=outdir, label=label)

    @property
    def number_of_interferometers(self):
        return len(self)

    @property
    def duration(self):
        return self[0].strain_data.duration

    @property
    def start_time(self):
        return self[0].strain_data.start_time

    @property
    def sampling_frequency(self):
        return self[0].strain_data.sampling_frequency

    @property
    def frequency_array(self):
        return self[0].strain_data.frequency_array

    def append(self, interferometer):
        if isinstance(interferometer, InterferometerList):
            super(InterferometerList, self).extend(interferometer)
        else:
            super(InterferometerList, self).append(interferometer)
        self._check_interferometers()

    def extend(self, interferometers):
        super(InterferometerList, self).extend(interferometers)
        self._check_interferometers()

    def insert(self, index, interferometer):
        super(InterferometerList, self).insert(index, interferometer)
        self._check_interferometers()

    @property
    def meta_data(self):
        """ Dictionary of the per-interferometer meta_data """
        return {interferometer.name: interferometer.meta_data
                for interferometer in self}

    @staticmethod
    def _filename_from_outdir_label_extension(outdir, label, extension="h5"):
        return os.path.join(outdir, label + f'.{extension}')

    _save_docstring = """ Saves the object to a {format} file

    {extra}

    Parameters
    ==========
    outdir: str, optional
        Output directory name of the file
    label: str, optional
        Output file name, is 'ifo_list' if not given otherwise. A list of
        the included interferometers will be appended.
    """

    _load_docstring = """ Loads in an InterferometerList object from a {format} file

    Parameters
    ==========
    filename: str
        If given, try to load from this filename

    """

    def to_hdf5(self, outdir='outdir', label='ifo_list'):
        import deepdish
        if sys.version_info[0] < 3:
            raise NotImplementedError('Pickling of InterferometerList is not supported in Python 2.'
                                      'Use Python 3 instead.')
        label = label + '_' + ''.join(ifo.name for ifo in self)
        utils.check_directory_exists_and_if_not_mkdir(outdir)
        try:
            filename = self._filename_from_outdir_label_extension(outdir, label, "h5")
            deepdish.io.save(filename, self)
        except AttributeError:
            logger.warning("Saving to hdf5 using deepdish failed. Pickle dumping instead.")
            self.to_pickle(outdir=outdir, label=label)

    @classmethod
    def from_hdf5(cls, filename=None):
        import deepdish
        if sys.version_info[0] < 3:
            raise NotImplementedError('Pickling of InterferometerList is not supported in Python 2.'
                                      'Use Python 3 instead.')
        res = deepdish.io.load(filename)
        if res.__class__ == list:
            res = cls(res)
        if res.__class__ != cls:
            raise TypeError('The loaded object is not a InterferometerList')
        return res

    def to_pickle(self, outdir="outdir", label="ifo_list"):
        import dill
        utils.check_directory_exists_and_if_not_mkdir('outdir')
        label = label + '_' + ''.join(ifo.name for ifo in self)
        filename = self._filename_from_outdir_label_extension(outdir, label, extension="pkl")
        with open(filename, "wb") as ff:
            dill.dump(self, ff)

    @classmethod
    def from_pickle(cls, filename=None):
        import dill
        with open(filename, "rb") as ff:
            res = dill.load(ff)
        if res.__class__ != cls:
            raise TypeError('The loaded object is not an InterferometerList')
        return res

    to_hdf5.__doc__ = _save_docstring.format(
        format="hdf5", extra=""".. deprecated:: 1.1.0
    Use :func:`to_pickle` instead."""
    )
    to_pickle.__doc__ = _save_docstring.format(
        format="pickle", extra=".. versionadded:: 1.1.0"
    )
    from_hdf5.__doc__ = _load_docstring.format(format="hdf5")
    from_pickle.__doc__ = _load_docstring.format(format="pickle")


class TriangularInterferometer(InterferometerList):

    def __init__(self, name, power_spectral_density, minimum_frequency, maximum_frequency,
                 length, latitude, longitude, elevation, xarm_azimuth, yarm_azimuth,
                 xarm_tilt=0., yarm_tilt=0.):
        super(TriangularInterferometer, self).__init__([])
        self.name = name
        # for attr in ['power_spectral_density', 'minimum_frequency', 'maximum_frequency']:
        if isinstance(power_spectral_density, PowerSpectralDensity):
            power_spectral_density = [power_spectral_density] * 3
        if isinstance(minimum_frequency, float) or isinstance(minimum_frequency, int):
            minimum_frequency = [minimum_frequency] * 3
        if isinstance(maximum_frequency, float) or isinstance(maximum_frequency, int):
            maximum_frequency = [maximum_frequency] * 3

        for ii in range(3):
            self.append(Interferometer(
                '{}{}'.format(name, ii + 1), power_spectral_density[ii], minimum_frequency[ii], maximum_frequency[ii],
                length, latitude, longitude, elevation, xarm_azimuth, yarm_azimuth, xarm_tilt, yarm_tilt))

            xarm_azimuth += 240
            yarm_azimuth += 240

            latitude += np.arctan(length * np.sin(xarm_azimuth * np.pi / 180) * 1e3 / utils.radius_of_earth)
            longitude += np.arctan(length * np.cos(xarm_azimuth * np.pi / 180) * 1e3 / utils.radius_of_earth)


def get_event_data(
        event, interferometer_names=None, duration=4, roll_off=0.2,
        psd_offset=-1024, psd_duration=100, cache=True, outdir='outdir',
        label=None, plot=True, filter_freq=None, **kwargs):
    """
    Get open data for a specified event.

    Parameters
    ==========
    event: str
        Event descriptor, this can deal with some prefixes, e.g., '150914',
        'GW150914', 'LVT151012'
    interferometer_names: list, optional
        List of interferometer identifiers, e.g., 'H1'.
        If None will look for data in 'H1', 'V1', 'L1'
    duration: float
        Time duration to search for.
    roll_off: float
        The roll-off (in seconds) used in the Tukey window.
    psd_offset, psd_duration: float
        The power spectral density (psd) is estimated using data from
        `center_time+psd_offset` to `center_time+psd_offset + psd_duration`.
    cache: bool
        Whether or not to store the acquired data.
    outdir: str
        Directory where the psd files are saved
    label: str
        If given, an identifying label used in generating file names.
    plot: bool
        If true, create an ASD + strain plot
    filter_freq: float
        Low pass filter frequency
    **kwargs:
        All keyword arguments are passed to
        `gwpy.timeseries.TimeSeries.fetch_open_data()`.

    Returns
    ======
    list: A list of bilby.gw.detector.Interferometer objects
    """

    warnings.warn(
        "get_event_data is deprecated, use set_strain_data_from_gwpy instead",
        DeprecationWarning
    )

    event_time = gwutils.get_event_time(event)

    interferometers = []

    if interferometer_names is None:
        interferometer_names = ['H1', 'L1', 'V1']

    for name in interferometer_names:
        try:
            interferometers.append(get_interferometer_with_open_data(
                name, trigger_time=event_time, duration=duration, roll_off=roll_off,
                psd_offset=psd_offset, psd_duration=psd_duration, cache=cache,
                outdir=outdir, label=label, plot=plot, filter_freq=filter_freq,
                **kwargs))
        except ValueError as e:
            logger.debug("Error raised {}".format(e))
            logger.warning('No data found for {}.'.format(name))

    return InterferometerList(interferometers)


def get_empty_interferometer(name):
    """
    Get an interferometer with standard parameters for known detectors.

    These objects do not have any noise instantiated.

    The available instruments are:
        H1, L1, V1, GEO600, CE

    Detector positions taken from:
        L1/H1: LIGO-T980044-10
        V1/GEO600: arXiv:gr-qc/0008066 [45]
        CE: located at the site of H1

    Detector sensitivities:
        H1/L1/V1: https://dcc.ligo.org/LIGO-P1200087-v42/public
        GEO600: http://www.geo600.org/1032083/GEO600_Sensitivity_Curves
        CE: https://dcc.ligo.org/LIGO-P1600143/public


    Parameters
    ==========
    name: str
        Interferometer identifier.

    Returns
    =======
    interferometer: Interferometer
        Interferometer instance
    """
    filename = os.path.join(os.path.dirname(__file__), 'detectors', '{}.interferometer'.format(name))
    try:
        return load_interferometer(filename)
    except OSError:
        raise ValueError('Interferometer {} not implemented'.format(name))


def load_interferometer(filename):
    """Load an interferometer from a file."""
    parameters = dict()
    with open(filename, 'r') as parameter_file:
        lines = parameter_file.readlines()
        for line in lines:
            if line[0] == '#' or line[0] == '\n':
                continue
            split_line = line.split('=')
            key = split_line[0].strip()
            value = eval('='.join(split_line[1:]))
            parameters[key] = value
    if 'shape' not in parameters.keys():
        ifo = Interferometer(**parameters)
        logger.debug('Assuming L shape for {}'.format('name'))
    elif parameters['shape'].lower() in ['l', 'ligo']:
        parameters.pop('shape')
        ifo = Interferometer(**parameters)
    elif parameters['shape'].lower() in ['triangular', 'triangle']:
        parameters.pop('shape')
        ifo = TriangularInterferometer(**parameters)
    else:
        raise IOError("{} could not be loaded. Invalid parameter 'shape'.".format(filename))
    return ifo


def get_interferometer_with_open_data(
        name, trigger_time, duration=4, start_time=None, roll_off=0.2,
        psd_offset=-1024, psd_duration=100, cache=True, outdir='outdir',
        label=None, plot=True, filter_freq=None, **kwargs):
    """
    Helper function to obtain an Interferometer instance with appropriate
    PSD and data, given an center_time.

    Parameters
    ==========

    name: str
        Detector name, e.g., 'H1'.
    trigger_time: float
        Trigger GPS time.
    duration: float, optional
        The total time (in seconds) to analyse. Defaults to 4s.
    start_time: float, optional
        Beginning of the segment, if None, the trigger is placed 2s before the end
        of the segment.
    roll_off: float
        The roll-off (in seconds) used in the Tukey window.
    psd_offset, psd_duration: float
        The power spectral density (psd) is estimated using data from
        `center_time+psd_offset` to `center_time+psd_offset + psd_duration`.
    cache: bool, optional
        Whether or not to store the acquired data
    outdir: str
        Directory where the psd files are saved
    label: str
        If given, an identifying label used in generating file names.
    plot: bool
        If true, create an ASD + strain plot
    filter_freq: float
        Low pass filter frequency
    **kwargs:
        All keyword arguments are passed to
        `gwpy.timeseries.TimeSeries.fetch_open_data()`.

    Returns
    =======
    bilby.gw.detector.Interferometer: An Interferometer instance with a PSD and frequency-domain strain data.

    """

    warnings.warn(
        "get_interferometer_with_open_data is deprecated, use set_strain_data_from_gwpy instead",
        DeprecationWarning
    )

    logger.warning(
        "Parameter estimation for real interferometer data in bilby is in "
        "alpha testing at the moment: the routines for windowing and filtering"
        " have not been reviewed.")

    utils.check_directory_exists_and_if_not_mkdir(outdir)

    if start_time is None:
        start_time = trigger_time + 2 - duration

    strain = InterferometerStrainData(roll_off=roll_off)
    strain.set_from_open_data(
        name=name, start_time=start_time, duration=duration,
        outdir=outdir, cache=cache, **kwargs)
    strain.low_pass_filter(filter_freq)

    strain_psd = InterferometerStrainData(roll_off=roll_off)
    strain_psd.set_from_open_data(
        name=name, start_time=start_time + duration + psd_offset,
        duration=psd_duration, outdir=outdir, cache=cache, **kwargs)
    # Low pass filter
    strain_psd.low_pass_filter(filter_freq)
    # Create and save PSDs
    psd_frequencies, psd_array = strain_psd.create_power_spectral_density(
        name=name, outdir=outdir, fft_length=strain.duration)

    interferometer = get_empty_interferometer(name)
    interferometer.power_spectral_density = PowerSpectralDensity(
        psd_array=psd_array, frequency_array=psd_frequencies)
    interferometer.strain_data = strain

    if plot:
        interferometer.plot_data(outdir=outdir, label=label)

    return interferometer
