"""
MDF file format module

"""
import csv
import os
from warnings import warn

import numpy as np

from .mdf3 import MDF3
from .mdf4 import MDF4
from .v3constants import CHANNEL_TYPE_MASTER as V3_MASTER
from .v4constants import CHANNEL_TYPE_MASTER as V4_MASTER
from .v4constants import CHANNEL_TYPE_VIRTUAL_MASTER as V4_VIRTUAL_MASTER
from .utils import MdfException


MDF3_VERSIONS = ('3.00', '3.10', '3.20', '3.30')
MDF4_VERSIONS = ('4.00', '4.10', '4.11')


__all__ = ['MDF', ]


class MDF(object):
    """Unified access to MDF v3 and v4 files.

    Parameters
    ----------
    name : string
        mdf file name, if provided it must be a real file name
    load_measured_data : bool
        load data option; default *True*

        * if *True* the data group binary data block will be loaded in RAM
        * if *False* the channel data is read from disk on request
    version : string
        mdf file version ('3.00', '3.10', '3.20', '3.30', '4.00', '4.10', '4.11'); default '3.20'

    """
    def __init__(self, name=None, load_measured_data=True, version='3.20'):
        if name:
            if os.path.isfile(name):
                with open(name, 'rb') as file_stream:
                    file_stream.read(8)
                    version = file_stream.read(4).decode('ascii')
                if version in MDF3_VERSIONS:
                    self._file = MDF3(name, load_measured_data)
                elif version in MDF4_VERSIONS:
                    self._file = MDF4(name, load_measured_data)
                else:
                    raise MdfException('"{}" is not a supported MDF file;, "{}" file version was found'.format(name, version))
            else:
                raise MdfException('File "{}" does not exist'.format(name))
        else:
            if version in MDF3_VERSIONS:
                self._file = MDF3(version=version, load_measured_data=load_measured_data)
            elif version in MDF4_VERSIONS:
                self._file = MDF4(version=version, load_measured_data=load_measured_data)

        # link underlying _file attributes and methods to the new MDF object
        for attr in set(dir(self._file)) - set(dir(self)):
            setattr(self, attr, getattr(self._file, attr))

    def __enter__(self):
        return self

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

    def convert(self, to, load_measured_data=True):
        """convert MDF to other versions

        Parameters
        ----------
        to : str
            new mdf version from ('3.00', '3.10', '3.20', '3.30', '4.00', '4.10', '4.11')
        load_measured_data : bool
            load data option; default *True*

            * if *True* the data group binary data block will be loaded in RAM
            * if *False* the channel data is stored to a temporary file and read from disk on request

        Returns
        -------
        out : MDF
            new MDF object

        """
        if not to in MDF3_VERSIONS + MDF4_VERSIONS:
            warn('Unknown output mdf version "{}". Available versions are {}'.format(to, MDF4_VERSIONS + MDF3_VERSIONS))
            return
        else:
            out = MDF(version=to, load_measured_data=load_measured_data)

            # walk through all groups and get all channels
            for i, gp in enumerate(self.groups):
                sigs = []
                excluded_channels = [self.masters_db[i], ]
                channels = gp['channels']

                if self.version in MDF3_VERSIONS:
                    for dep in gp['channel_dependencies']:
                        if dep is None:
                            continue
                        for ch, ch_gp in dep.referenced_channels:
                            if ch_gp == gp:
                                excluded_channels.append(channels.index(ch))
                else:
                    for dependency_list in gp['channel_dependencies']:
                        if dependency_list is None:
                            continue
                        if all(dep['id'] == b'##CN' for dep in dependency_list):
                            for ch in dependency_list:
                                excluded_channels.append(channels.index(ch))
                        else:
                            for dep in dependency_list:
                                for ch in dep.referenced_channels:
                                    try:
                                        excluded_channels.append(channels.index(ch))
                                    except ValueError:
                                        continue

                for j, ch in enumerate(gp['channels']):
                    if j in excluded_channels:
                        continue
                    sigs.append(self.get(group=i, index=j))
                out.append(sigs,
                           'Converted from {} to {}'.format(self.version, to),
                           common_timebase=True)
            return out

    def cut(self, start=None, stop=None):
        """convert MDF to other versions

        Parameters
        ----------
        start : float
            start time, default None. If *None* then the start of measurement is used
        stop : float
            stop time, default . If *None* then the end of measurement is used

        Returns
        -------
        out : MDF
            new MDF object

        """
        out = MDF(version=self.version, load_measured_data=self.load_measured_data)

        # walk through all groups and get all channels
        for i, gp in enumerate(self.groups):
            sigs = []
            excluded_channels = [self.masters_db[i], ]
            channels = gp['channels']

            if self.version in MDF3_VERSIONS:
                for dep in gp['channel_dependencies']:
                    if dep is None:
                        continue
                    for ch, ch_gp in dep.referenced_channels:
                        if ch_gp == gp:
                            excluded_channels.append(channels.index(ch))
            else:
                for dependency_list in gp['channel_dependencies']:
                    if dependency_list is None:
                        continue
                    if all(dep['id'] == b'##CN' for dep in dependency_list):
                        for ch in dependency_list:
                            excluded_channels.append(channels.index(ch))
                    else:
                        for dep in dependency_list:
                            for ch in dep.referenced_channels:
                                try:
                                    excluded_channels.append(channels.index(ch))
                                except ValueError:
                                    continue

            for j, ch in enumerate(gp['channels']):
                if j in excluded_channels:
                    continue
                sigs.append(self.get(group=i, index=j).cut(start=start, stop=stop))

            out.append(sigs,
                       'Cut from {} to {}'.format('{}s'.format(start) if start else 'start of measurement',
                                                  '{}s'.format(stop) if stop else 'end of measurement'),
                       common_timebase=True)
        return out

    def export(self, format, filename=None):
        """ export MDF to other formats. The *MDF* file name is used is available,
        else the *filename* aragument must be provided.

        Parameters
        ----------
        format : string
            can be one of the following:

                * *csv* : CSV export that uses the ";" delimiter. This option will generate a new csv file for each data group (<MDFNAME>_DataGroup_XX.csv).
                * *hdf5* : HDF5 file output; each *MDF* data group is mapped to a *HDF5* group with the name 'DataGroup_xx' (where xx is the index)
                * *excel* : Excel file output (very slow). This option will generate a new excel file for each data group (<MDFNAME>_DataGroup_XX.xlsx)
                * *mat* : Matlab .mat version 5 export, for Matlab >= 7.6. IN the mat file the channels will be renamed to 'DataGroup_xx_<channel name>'.
                The channel group master will be renamed to 'DataGroup_xx_<channel name>_master'. ( *xx* is the data group index starting from 0).

        filename : string
            export file name

        """
        if filename is None and self.name is None:
            warn('Must specify filename for export if MDF was created without a file name')
        else:
            name = self.name if self.name else filename
            if format == 'hdf5':
                try:
                    from h5py import File as HDF5
                except ImportError:
                    warn('h5py not found; export to HDF5 is unavailable')
                    return
                else:
                    name = os.path.splitext(name)[0] + '.hdf'
                    with HDF5(name, 'w') as f:
                        # header information
                        group = f.create_group(os.path.basename(name))

                        if self.version in MDF3_VERSIONS:
                            for item in ('date', 'time', 'author', 'organization', 'project', 'subject'):
                                group.attrs[item] = self.header[item]

                        # save each data group in a HDF5 group called "DataGroup_xx"
                        # with the index starting from 1
                        # each HDF5 group will have a string attribute "master" that
                        # will hold the name of the master channel
                        for i, grp in enumerate(self.groups):
                            group = f.create_group(r'/' + 'DataGroup_{}'.format(i + 1))

                            master_index = self.masters_db[i]

                            for j, ch in enumerate(grp['channels']):
                                name = ch.name
                                sig = self.get(group=i, index=j)
                                if j == master_index:
                                    group.attrs['master'] = name
                                dataset = group.create_dataset(name, data=sig.samples)
                                dataset.attrs['unit'] = sig.unit
                                dataset.attrs['comment'] = sig.comment if sig.comment else ''

            elif format == 'excel':
                try:
                    import xlsxwriter
                except ImportError:
                    warn('xlsxwriter not found; export to Excel is unavailable')
                    return
                else:
                    excel_name = os.path.splitext(name)[0]
                    nr = len(self.groups)
                    for i, grp in enumerate(self.groups):
                        print('Exporting group {} of {}'.format(i+1, nr))

                        workbook = xlsxwriter.Workbook('{}_{}.xlsx'.format(excel_name, 'DataGroup_{}'.format(i + 1)))
                        bold = workbook.add_format({'bold': True})

                        ws = workbook.add_worksheet("Information")

                        if self.version in MDF3_VERSIONS:
                            for j, item in enumerate(('date', 'time', 'author', 'organization', 'project', 'subject')):
                                ws.write(j, 0, item.title(), bold)
                                ws.write(j, 1, self.header[item].decode('latin-1'))

                            ws = workbook.add_worksheet('DataGroup_{}'.format(i + 1))

                            # the sheet header has 3 rows
                            # the channel name and unit 'YY [xx]'
                            # the channel comment
                            # the flag for data grup master channel
                            ws.write(0, 0, 'Channel', bold)
                            ws.write(1, 0, 'comment', bold)
                            ws.write(2, 0, 'is master', bold)

                            master_index = self.masters_db[i]

                            for j in range(grp['channel_group']['cycles_nr']):
                                ws.write(j+3, 0, str(j))

                            for j, ch in enumerate(grp['channels']):
                                sig = self.get(group=i, index=j)

                                col = j + 1
                                ws.write(0, col, '{} [{}]'.format(sig.name, sig.unit))
                                ws.write(1, col, sig.comment if sig.comment else '')
                                if j == master_index:
                                    ws.write(2, col, 'x')
                                ws.write_column(3, col, sig.samples.astype(str))

                        workbook.close()

            elif format == 'csv':
                csv_name = os.path.splitext(name)[0]
                nr = len(self.groups)
                for i, grp in enumerate(self.groups):
                    print('Exporting group {} of {}'.format(i+1, nr))
                    with open('{}_{}.csv'.format(csv_name, 'DataGroup_{}'.format(i + 1)), 'w', newline='') as csvfile:
                        writer = csv.writer(csvfile, delimiter=';')

                        ch_nr = len(grp['channels'])
                        channels = [self.get(group=i, index=j) for j in range(ch_nr)]

                        master_index = self.masters_db[i]
                        cycles = grp['channel_group']['cycles_nr']

                        writer.writerow(['Channel',] + ['{} [{}]'.format(ch.name, ch.unit) for ch in channels])
                        writer.writerow(['comment',] + [ch.comment for ch in channels])
                        writer.writerow(['is master',] + ['x' if j == master_index else '' for j in range(ch_nr)])

                        vals = [np.array(range(cycles), dtype=np.uint32), ] + [ch.samples for ch in channels]

                        writer.writerows(zip(*vals))

            elif format == 'mat':
                try:
                    from scipy.io import savemat
                except ImportError:
                    raise
                    warn('scipy not found; export to mat is unavailable')
                    return
                else:
                    name = os.path.splitext(name)[0] + '.mat'
                    mdict = {}

                    for i, grp in enumerate(self.groups):
#                        master_index = self.masters_db[i]
#                        master = self.get(group=i, index=master_index, samples_only=True)

                        for j, ch in enumerate(grp['channels']):
                            if j == master_index:
                                channel_name = 'DataGroup_{}_{}_master'.format(i, ch.name)
                            else:
                                channel_name = 'DataGroup_{}_{}'.format(i, ch.name)
                            mdict[channel_name] = self.get(group=i, index=j, samples_only=True)

                    savemat(name, mdict, long_field_names=True, do_compression=True)

    def filter(self, channels):
        """ return new *MDF* object that contains only the channels listed in *channels* argument

        Parameters
        ----------
        channels : list
            list of channel names to be filtered

        Returns
        -------
        mdf : MDF
            new MDF file

        """

        # group channels by group index
        gps = {}
        for ch in channels:
            if ch in self.channels_db:
                for group, index in self.channels_db[ch]:
                    if not group in gps:
                        gps[group] = []
                    gps[group].append(index)
            else:
                warn('MDF filter error: Channel "{}" not found, it will be ignored'.format(ch))
                continue

        mdf = MDF(version=self.version, load_measured_data=self.load_measured_data)

        # append filtered channels to new MDF
        for group in gps:
            sigs = []
            for index in gps[group]:
                sigs.append(self.get(group=group, index=index))
            mdf.append(sigs,
                       'Signals filtered from <{}>'.format(os.path.basename(self.name) if self.name else 'New MDF'),
                       common_timebase=True)

        return mdf

    @staticmethod
    def merge(files, load_measured_data=True):
        """ merge several files and return the merged MDF object. The files
        must have the same internal structure (same number of groups, and same
        channels in each group)

        Parameters
        ----------
        files : list | tuple
            list of MDF file names
        load_measured_data : bool
            load data option; default *True*

            * if *True* the data group binary data block will be loaded in RAM
            * if *False* the channel data is stored to a temporary file and read from disk on request

        Returns
        -------
        merged : MDF
            new MDF object with merged channels

        Raises
        ------
        MdfException : if there are inconsistances between the files
            merged MDF object
        """
        if files:
            merged = MDF(files[0], load_measured_data=load_measured_data)
            for file in files[1:]:
                mdf = MDF(file, load_measured_data=load_measured_data)
                out = merged
                merged = MDF(load_measured_data=load_measured_data)
                if len(out.groups) != len(mdf.groups):
                    raise MdfException("Can't merge files: merged has {} groups and {} has {} groups".format(len(out.groups),
                                                                                                             mdf.name,
                                                                                                             len(mdf.groups)))
                for i, (gp1, gp2) in enumerate(zip(out.groups, mdf.groups)):
                    sigs = []

                    excluded_channels = [mdf.masters_db[i], ]
                    channels = gp1['channels']

                    if mdf.version in MDF3_VERSIONS:
                        for dep in gp1['channel_dependencies']:
                            if dep is None:
                                continue
                            for ch, ch_gp in dep.referenced_channels:
                                if ch_gp == gp1:
                                    excluded_channels.append(channels.index(ch))
                    else:
                        for dependency_list in gp1['channel_dependencies']:
                            if dependency_list is None:
                                continue
                            if all(dep['id'] == b'##CN' for dep in dependency_list):
                                for ch in dependency_list:
                                    excluded_channels.append(channels.index(ch))
                            else:
                                for dep in dependency_list:
                                    for ch in dep.referenced_channels:
                                        try:
                                            excluded_channels.append(channels.index(ch))
                                        except ValueError:
                                            continue


                    if len(gp1['channels']) != len(gp2['channels']):
                        raise MdfException("Can't merge files: merged DataGroup {} has {} channels and {} DataGroup {} has {} channels".format(i,
                                                                                                                                               len(gp1['channels']),
                                                                                                                                               mdf.name,
                                                                                                                                               i,
                                                                                                                                               len(gp2['channels'])))

                    for j, (ch1, ch2) in enumerate(zip(gp1['channels'], gp2['channels'])):
                        if ch1.name != ch2.name:
                            raise MdfException("Can't merge files: merged DataGroup {} channel {} is named {} and {} DataGroup {} channel {} is named {}".format(i,
                                                                                                                                                                 j,
                                                                                                                                                                 ch1.name,
                                                                                                                                                                 mdf.name,
                                                                                                                                                                 i,
                                                                                                                                                                 j,
                                                                                                                                                                 ch2.name))
                        if j in excluded_channels:
                            continue
                        sig1 = out.get(group=i, index=j)
                        sig2 = mdf.get(group=i, index=j)
                        sigs.append(sig1.extend(sig2))

                    merged.append(sigs, common_timebase=True)
                mdf.close()
            return merged
        else:
            raise MdfException('No files given for merge')


    def iter_to_pandas(self):
        """ generator that yields channel groups as pandas DataFrames"""
        try:
            from pandas import DataFrame
        except ImportError:
            warn('pandas not found; export to pandas DataFrame is unavailable')
            return
        else:
            for i, gp in enumerate(self.groups):
                master_index = self.masters_db[i]
                pandas_dict = {gp['channels'][master_index].name: self.get(group=i, index=master_index, samples_only=True)}
                for j, ch in enumerate(gp['channels']):
                    if j == master_index:
                        continue
                    name = gp['channels'][j].name
                    pandas_dict[name] = self.get(group=i, index=j, samples_only=True)
                yield DataFrame.from_dict(pandas_dict)


if __name__ == '__main__':
    pass
