"""an adaptation of h5py for reading and writing SE files

SE files are just HDF5 files with particular conventions to support
simple reading and writing of data from 1-D stellar evolution and
nucleosynthesis codes.  This module provides a Python wrapper for h5py
that implements the conventions and restrictions expressed in the SE
library.
"""

import h5py
import numpy

class seFile(h5py.File):

    """Represents an SE file."""

    compression_level = 0

    def __init__(self, name, mode=None, driver=None, **keywords):

        """Initialize the seFile object.

        This just sets the file-level SE_VERSION attribute for new
        files.
        """

        h5py.File.__init__(self, name, mode, driver, **keywords)

        if len(self) == 0:
            self.attrs['SE_VERSION'] = '1.2'

    def version(self):

        """Return the SE version string associated with the seFile object."""

        try:
            ver = self.attrs['SE_VERSION']
        except KeyError:
            ver = '1.0'

        return ver

    def read(self, cyclenumber, namelist):

        """Read compound dataset members.

        Read some subset of the columns (structure members) from a SE
        file, and return them as a list of numpy arrays.
        """

        cyclename = namefromcycle(cyclenumber, self.version())

        if self.version() == '1.0':
            group = self
            datasetname = cyclename
        else:
            group = self[cyclename]
            datasetname = 'SE_DATASET'

        if type(namelist) is list:
            return [group[datasetname][n] for n in namelist]
        else:
            return group[datasetname][namelist]

    def readattr(self, cyclenumber, name):

        """Read an attribute from an SE cycle.

        Returns the attribute value.  Pass -1 as cyclenumber to read
        global (i.e., file-level) attributes.
        """

        if cyclenumber == -1:
            attr = self.attrs[name]
        else:
            attr = self[namefromcycle(cyclenumber, self.version())].attrs[name]

        return attr

    def readarrayattr(self, cyclenumber, tablename, colname='data'):

        """Read an array attribute from an SE cycle.

        Pass -1 as cyclenumber to read global (i.e., file-level)
        attributes.

        This command can also be used to read a single column of a table.

        (Added March 2010: I'm not sure why this method was designed to
        have the ability to read a single column from any data table.
        It's true that, underneath, SE "array attributes" are just tables
        with a table name other than "SE_DATASET".  In the SE library,
        "cycle" reads and writes and "array attribute" reads and writes
        are implemented with the same functions, and the same thing could
        be done here.  But exposing the colname attribute in this method
        requires users to know that array attributes store their data in a
        column named "data", which is completely an implementation detail.

        On the other hand, there are many things that I would change about
        this module now, with the benefit of hindsight, if people weren't
        already using it.  So it stays.  But I can default it to "data"!
        Thanks Python.)
        """

        if cyclenumber == -1:
            group = self
        else:
            cname = namefromcycle(cyclenumber, self.version())
            group = self[cname]

        return group[tablename][colname]

    def write(self, cyclenumber, arrays):

        """Write some records to a SE file.

        Arrays to write should be specified as (name, data) pairs in a
        dictionary.  The 'data' can be literal numpy arrays or Python
        lists (or really anything that numpy.asarray can reinterpret
        as an array).
        """

        cyclename = namefromcycle(cyclenumber, self.version())

        if self.version() == '1.0':
            group = self
            datasetname = cyclename
        else:
            group = self.create_group(cyclename)
            datasetname = 'SE_DATASET'

        types = []
        for name, data in arrays.iteritems():
            if len(numpy.asarray(data).shape) > 2:
                raise ValueError("%s has dimensionality > 2" % name)

            types.append( (name,
                          numpy.asarray(data).dtype,
                          numpy.asarray(data[0]).shape) )

        dset = []
        for i in xrange(min(len(arrays[n]) for n in arrays)):
            dset.append(tuple(arrays[n][i] for n in arrays))

        group.create_dataset(datasetname,
                             data=numpy.asarray(dset, dtype=types),
                             compression='gzip',
                             compression_opts=self.compression_level)

    def writeattr(self, cyclenumber, name, value):

        """Attach an attribute to an SE cycle.

        Pass -1 as cyclenumber to write global (i.e., file-level)
        attributes.
        """

        if name[:3] == 'SE_':
            raise KeyError('SE_ is a reserved attribute prefix')

        if cyclenumber == -1:
            group = self
        else:
            cname = namefromcycle(cyclenumber, self.version())
            if cname not in self:
                self.create_group(cname)
            group = self[cname]

        group.attrs[name] = value

    def writearrayattr(self, cyclenumber, name, value):

        """Attach an array attribute to an SE cycle.

        Pass -1 as cyclenumber to write global (i.e., file-level)
        attributes.
        """

        ver = self.version()

        if ver == '1.0':
            raise Exception("v1.0 files don't support array attributes.")

        if cyclenumber == -1:
            group = self
        else:
            cname = namefromcycle(cyclenumber, ver)
            if cname not in self:
                self.create_group(cname)
            group = self[cname]

        dt = numpy.dtype([('data', numpy.asarray(value).dtype)])

        group.create_dataset(name,
                             data=numpy.asarray([i for i
                                                 in numpy.asarray(value)],
                                                dtype=dt),
                             compression='gzip',
                             compression_opts=self.compression_level)

def namefromcycle(cyclenumber, version='1.2'):

    """Return the dataset name corresponding to the given cycle number.

    SE 1.0 and 1.1 used dashes; SE 1.2 uses zero-padded integers with
    10-digit field width.
    """

    if version == '1.2':
        return 'cycle%010d' % cyclenumber
    else:
        return 'cycle-' + str(cyclenumber)
