# Copyright (c) 2016-2020 The Regents of the University of Michigan
# This file is part of the General Simulation Data (GSD) project, released under
# the BSD 2-Clause License.

"""GSD file layer API.

Low level access to gsd files. :py:mod:`gsd.fl` allows direct access to create,
read, and write ``gsd`` files. The module is implemented in C and is optimized.
See :ref:`fl-examples` for detailed example code.

* :py:class:`GSDFile` - Class interface to read and write gsd files.
* :py:func:`open` - Open a gsd file.

"""

import logging
import numpy
import os
from pickle import PickleError
from libc.stdint cimport uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t,\
    uint64_t, int64_t
from libc.errno cimport errno
cimport gsd.libgsd as libgsd
cimport numpy

logger = logging.getLogger('gsd.fl')

####################
# Helper functions #

cdef __format_errno(fname):
    """Return a tuple for constructing an IOError."""
    return (errno, os.strerror(errno), fname)

cdef __raise_on_error(retval, extra):
    """Raise the appropriate error type.

    Args:
        retval: Return value from a gsd C API call
        extra: Extra string to pass along with the exception
    """
    if retval == libgsd.GSD_ERROR_IO:
        raise IOError(*__format_errno(extra))
    elif retval == libgsd.GSD_ERROR_NOT_A_GSD_FILE:
        raise RuntimeError("Not a GSD file: " + extra)
    elif retval == libgsd.GSD_ERROR_INVALID_GSD_FILE_VERSION:
        raise RuntimeError("Unsupported GSD file version: " + extra)
    elif retval == libgsd.GSD_ERROR_FILE_CORRUPT:
        raise RuntimeError("Corrupt GSD file: " + extra)
    elif retval == libgsd.GSD_ERROR_MEMORY_ALLOCATION_FAILED:
        raise MemoryError("Memory allocation failed: " + extra)
    elif retval == libgsd.GSD_ERROR_NAMELIST_FULL:
        raise RuntimeError("GSD namelist is full: " + extra)
    elif retval == libgsd.GSD_ERROR_FILE_MUST_BE_WRITABLE:
        raise RuntimeError("File must be writable: " + extra)
    elif retval == libgsd.GSD_ERROR_FILE_MUST_BE_READABLE:
        raise RuntimeError("File must be readable: " + extra)
    elif retval == libgsd.GSD_ERROR_INVALID_ARGUMENT:
        raise RuntimeError("Invalid gsd argument: " + extra)
    elif retval != 0:
        raise RuntimeError("Unknown error: " + extra)

# Getter methods for 2D numpy arrays of all supported types
# cython needs strongly typed numpy arrays to get a void *
# to the data, so we implement each by hand here and dispacth
# from the read and write routines

cdef void * __get_ptr_uint8(data):
    cdef numpy.ndarray[uint8_t, ndim=2, mode="c"] data_array_uint8
    data_array_uint8 = data
    if (data.size == 0):
        return NULL
    else:
        return <void*>&data_array_uint8[0, 0]

cdef void * __get_ptr_uint16(data):
    cdef numpy.ndarray[uint16_t, ndim=2, mode="c"] data_array_uint16
    data_array_uint16 = data
    if (data.size == 0):
        return NULL
    else:
        return <void*>&data_array_uint16[0, 0]

cdef void * __get_ptr_uint32(data):
    cdef numpy.ndarray[uint32_t, ndim=2, mode="c"] data_array_uint32
    data_array_uint32 = data
    if (data.size == 0):
        return NULL
    else:
        return <void*>&data_array_uint32[0, 0]

cdef void * __get_ptr_uint64(data):
    cdef numpy.ndarray[uint64_t, ndim=2, mode="c"] data_array_uint64
    data_array_uint64 = data
    if (data.size == 0):
        return NULL
    else:
        return <void*>&data_array_uint64[0, 0]

cdef void * __get_ptr_int8(data):
    cdef numpy.ndarray[int8_t, ndim=2, mode="c"] data_array_int8
    data_array_int8 = data
    if (data.size == 0):
        return NULL
    else:
        return <void*>&data_array_int8[0, 0]

cdef void * __get_ptr_int16(data):
    cdef numpy.ndarray[int16_t, ndim=2, mode="c"] data_array_int16
    data_array_int16 = data
    if (data.size == 0):
        return NULL
    else:
        return <void*>&data_array_int16[0, 0]

cdef void * __get_ptr_int32(data):
    cdef numpy.ndarray[int32_t, ndim=2, mode="c"] data_array_int32
    data_array_int32 = data
    if (data.size == 0):
        return NULL
    else:
        return <void*>&data_array_int32[0, 0]

cdef void * __get_ptr_int64(data):
    cdef numpy.ndarray[int64_t, ndim=2, mode="c"] data_array_int64
    data_array_int64 = data
    if (data.size == 0):
        return NULL
    else:
        return <void*>&data_array_int64[0, 0]

cdef void * __get_ptr_float32(data):
    cdef numpy.ndarray[float, ndim=2, mode="c"] data_array_float32
    data_array_float32 = data
    if (data.size == 0):
        return NULL
    else:
        return <void*>&data_array_float32[0, 0]

cdef void * __get_ptr_float64(data):
    cdef numpy.ndarray[double, ndim=2, mode="c"] data_array_float64
    data_array_float64 = data
    if (data.size == 0):
        return NULL
    else:
        return <void*>&data_array_float64[0, 0]


def open(name, mode, application=None, schema=None, schema_version=None):
    """open(name, mode, application=None, schema=None, schema_version=None)

    :py:func:`open` opens a GSD file and returns a :py:class:`GSDFile` instance.
    The return value of :py:func:`open` can be used as a context manager.

    Args:
        name (str): File name to open.

        mode (str): File access mode.

        application (str): Name of the application creating the file.

        schema (str): Name of the data schema.

        schema_version (`typing.Tuple` [int, int]): Schema version number
            (major, minor).

    Valid values for mode:

    +------------------+---------------------------------------------+
    | mode             | description                                 |
    +==================+=============================================+
    | ``'rb'``         | Open an existing file for reading.          |
    +------------------+---------------------------------------------+
    | ``'rb+'``        | Open an existing file for reading and       |
    |                  | writing.                                    |
    +------------------+---------------------------------------------+
    | ``'wb'``         | Open a file for writing. Creates the file   |
    |                  | if needed, or overwrites an existing file.  |
    +------------------+---------------------------------------------+
    | ``'wb+'``        | Open a file for reading and writing.        |
    |                  | Creates the file if needed, or overwrites   |
    |                  | an existing file.                           |
    +------------------+---------------------------------------------+
    | ``'xb'``         | Create a gsd file exclusively and opens it  |
    |                  | for writing.                                |
    |                  | Raise an :py:exc:`FileExistsError`          |
    |                  | exception if it already exists.             |
    +------------------+---------------------------------------------+
    | ``'xb+'``        | Create a gsd file exclusively and opens it  |
    |                  | for reading and writing.                    |
    |                  | Raise an :py:exc:`FileExistsError`          |
    |                  | exception if it already exists.             |
    +------------------+---------------------------------------------+
    | ``'ab'``         | Open an existing file for writing.          |
    |                  | Does *not* create or overwrite existing     |
    |                  | files.                                      |
    +------------------+---------------------------------------------+

    When opening a file for reading (``'r' and 'a' modes): ``application`` and
    ``schema_version`` are ignored and may be ``None``. When ``schema`` is not
    ``None``, :py:func:`open` throws an exception if the
    file's schema does not match ``schema``.

    When opening a file for writing (``'w' or 'x'`` modes): The given
    ``application``, ``schema``, and ``schema_version`` are saved in the file
    and must not be None.

    Example:

        .. ipython:: python

            with gsd.fl.open(name='file.gsd', mode='wb',
                             application="My application", schema="My Schema",
                             schema_version=[1,0]) as f:
                f.write_chunk(name='chunk1',
                              data=numpy.array([1,2,3,4], dtype=numpy.float32))
                f.write_chunk(name='chunk2',
                              data=numpy.array([[5,6],[7,8]],
                                               dtype=numpy.float32))
                f.end_frame()
                f.write_chunk(name='chunk1',
                              data=numpy.array([9,10,11,12],
                                               dtype=numpy.float32))
                f.write_chunk(name='chunk2',
                              data=numpy.array([[13,14],[15,16]],
                                               dtype=numpy.float32))
                f.end_frame()

            f = gsd.fl.open(name='file.gsd', mode='rb')
            if f.chunk_exists(frame=0, name='chunk1'):
                data = f.read_chunk(frame=0, name='chunk1')
            data
            f.close()
    """

    return GSDFile(str(name), mode, application, schema, schema_version)


cdef class GSDFile:
    """GSDFile

    GSD file access interface.

    GSDFile implements an object oriented class interface to the GSD file
    layer. Use :py:func:`open` to open a GSD file and obtain a GSDFile instance.
    :py:class:`GSDFile` can be used as a context manager.

    Attributes:

        name (str): Name of the open file.

        mode (str): Mode of the open file.

        gsd_version (`typing.Tuple` [int, int]): GSD file layer version number
            (major, minor).

        application (str): Name of the generating application.

        schema (str): Name of the data schema.

        schema_version (`typing.Tuple` [int, int]): Schema version number
            (major, minor).

        nframes (int): Number of frames.
    """

    cdef libgsd.gsd_handle __handle
    cdef bint __is_open
    cdef str mode
    cdef str name

    def __init__(self,
                 name,
                 mode,
                 application,
                 schema,
                 schema_version):
        cdef libgsd.gsd_open_flag c_flags
        cdef int exclusive_create = 0
        cdef int overwrite = 0

        if mode == 'wb':
            c_flags = libgsd.GSD_OPEN_APPEND
            overwrite = 1
        elif mode == 'wb+':
            c_flags = libgsd.GSD_OPEN_READWRITE
            overwrite = 1
        elif mode == 'rb':
            c_flags = libgsd.GSD_OPEN_READONLY
        elif mode == 'rb+':
            c_flags = libgsd.GSD_OPEN_READWRITE
        elif mode == 'xb':
            c_flags = libgsd.GSD_OPEN_APPEND
            overwrite = 1
            exclusive_create = 1
        elif mode == 'xb+':
            c_flags = libgsd.GSD_OPEN_READWRITE
            overwrite = 1
            exclusive_create = 1
        elif mode == 'ab':
            c_flags = libgsd.GSD_OPEN_APPEND
        else:
            raise ValueError("mode must be 'wb', 'wb+', 'rb', 'rb+', "
                             "'xb', 'xb+', or 'ab'")

        self.name = name
        self.mode = mode

        cdef char * c_name
        cdef char * c_application
        cdef char * c_schema
        cdef int _c_schema_version
        cdef str schema_truncated

        if overwrite:
            if application is None:
                raise ValueError("Provide application when creating a file")
            if schema is None:
                raise ValueError("Provide schema when creating a file")
            if schema_version is None:
                raise ValueError("Provide schema_version when creating a file")

            # create a new file or overwrite an existing one
            logger.info('overwriting file: ' + name + ' with mode: ' + mode
                        + ', application: ' + application
                        + ', schema: ' + schema
                        + ', and schema_version: ' + str(schema_version))
            name_e = name.encode('utf-8')
            c_name = name_e

            application_e = application.encode('utf-8')
            c_application = application_e

            schema_e = schema.encode('utf-8')
            c_schema = schema_e

            _c_schema_version = libgsd.gsd_make_version(schema_version[0],
                                                        schema_version[1])

            with nogil:
                retval = libgsd.gsd_create_and_open(&self.__handle,
                                                    c_name,
                                                    c_application,
                                                    c_schema,
                                                    _c_schema_version,
                                                    c_flags,
                                                    exclusive_create)
        else:
            # open an existing file
            logger.info('opening file: ' + name + ' with mode: ' + mode)
            name_e = name.encode('utf-8')
            c_name = name_e

            with nogil:
                retval = libgsd.gsd_open(&self.__handle, c_name, c_flags)

        __raise_on_error(retval, name)

        # validate schema
        if schema is not None:
            schema_truncated = schema
            if len(schema_truncated) > 64:
                schema_truncated = schema_truncated[0:63]
            if self.schema != schema_truncated:
                raise RuntimeError('file ' + name + ' has incorrect schema: '
                                   + self.schema)

        self.__is_open = True

    def close(self):
        """close()

        Close the file.

        Once closed, any other operation on the file object will result in a
        `ValueError`. :py:meth:`close()` may be called more than once.
        The file is automatically closed when garbage collected or when
        the context manager exits.

        Example:
            .. ipython:: python

                f = gsd.fl.open(name='file.gsd', mode='wb+',
                                application="My application",
                                schema="My Schema", schema_version=[1,0])
                f.write_chunk(name='chunk1',
                              data=numpy.array([1,2,3,4], dtype=numpy.float32))
                f.end_frame()
                data = f.read_chunk(frame=0, name='chunk1')

                f.close()
                # Read fails because the file is closed
                @okexcept
                data = f.read_chunk(frame=0, name='chunk1')

        """
        if self.__is_open:
            logger.info('closing file: ' + self.name)
            with nogil:
                retval = libgsd.gsd_close(&self.__handle)
            self.__is_open = False

            __raise_on_error(retval, self.name)

    def truncate(self):
        """truncate()

        Truncate all data from the file. After truncation, the file has no
        frames and no data chunks. The application, schema, and schema version
        remain the same.

        Example:
            .. ipython:: python

                with gsd.fl.open(name='file.gsd', mode='wb',
                                 application="My application",
                                 schema="My Schema", schema_version=[1,0]) as f:
                    for i in range(10):
                        f.write_chunk(name='chunk1',
                                      data=numpy.array([1,2,3,4],
                                                       dtype=numpy.float32))
                        f.end_frame()

                f = gsd.fl.open(name='file.gsd', mode='ab',
                                application="My application",
                                schema="My Schema", schema_version=[1,0])
                f.nframes
                f.schema, f.schema_version, f.application
                f.truncate()
                f.nframes
                f.schema, f.schema_version, f.application
                f.close()
        """

        if not self.__is_open:
            raise ValueError("File is not open")

        logger.info('truncating file: ' + self.name)
        with nogil:
            retval = libgsd.gsd_truncate(&self.__handle)

        __raise_on_error(retval, self.name)

    def end_frame(self):
        """end_frame()

        Complete writing the current frame. After calling :py:meth:`end_frame()`
        future calls to :py:meth:`write_chunk()` will write to the **next**
        frame in the file.

        .. danger::
            Call :py:meth:`end_frame()` to complete the current frame
            **before** closing the file. If you fail to call
            :py:meth:`end_frame()`, the last frame will not be written
            to disk.

        Example:
            .. ipython:: python

                f = gsd.fl.open(name='file.gsd', mode='wb',
                                application="My application",
                                schema="My Schema", schema_version=[1,0])

                f.write_chunk(name='chunk1',
                              data=numpy.array([1,2,3,4], dtype=numpy.float32))
                f.end_frame()
                f.write_chunk(name='chunk1',
                              data=numpy.array([9,10,11,12],
                                               dtype=numpy.float32))
                f.end_frame()
                f.write_chunk(name='chunk1',
                              data=numpy.array([13,14],
                                               dtype=numpy.float32))
                f.end_frame()
                f.nframes
                f.close()

        """

        if not self.__is_open:
            raise ValueError("File is not open")

        logger.debug('end frame: ' + self.name)

        with nogil:
            retval = libgsd.gsd_end_frame(&self.__handle)

        __raise_on_error(retval, self.name)

    def write_chunk(self, name, data):
        """write_chunk(name, data)

        Write a data chunk to the file. After writing all chunks in the
        current frame, call :py:meth:`end_frame()`.

        Args:
            name (str): Name of the chunk
            data: Data to write into the chunk. Must be a numpy
                  array, or array-like, with 2 or fewer
                  dimensions.

        Warning:
            :py:meth:`write_chunk()` will implicitly converts array-like and
            non-contiguous numpy arrays to contiguous numpy arrays with
            ``numpy.ascontiguousarray(data)``. This may or may not produce
            desired data types in the output file and incurs overhead.

        Example:
            .. ipython:: python

                f = gsd.fl.open(name='file.gsd', mode='wb',
                                application="My application",
                                schema="My Schema", schema_version=[1,0])

                f.write_chunk(name='float1d',
                              data=numpy.array([1,2,3,4],
                                               dtype=numpy.float32))
                f.write_chunk(name='float2d',
                              data=numpy.array([[13,14],[15,16],[17,19]],
                                               dtype=numpy.float32))
                f.write_chunk(name='double2d',
                              data=numpy.array([[1,4],[5,6],[7,9]],
                                               dtype=numpy.float64))
                f.write_chunk(name='int1d',
                              data=numpy.array([70,80,90],
                                               dtype=numpy.int64))
                f.end_frame()
                f.nframes
                f.close()
        """

        if not self.__is_open:
            raise ValueError("File is not open")

        data_array = numpy.ascontiguousarray(data)
        if data_array is not data:
            logger.warning('implicit data copy when writing chunk: ' + name)
        data_array = data_array.view()

        cdef uint64_t N
        cdef uint32_t M

        if len(data_array.shape) > 2:
            raise ValueError("GSD can only write 1 or 2 dimensional arrays: "
                             + name)

        if len(data_array.shape) == 1:
            data_array = data_array.reshape([data_array.shape[0], 1])

        N = data_array.shape[0]
        M = data_array.shape[1]

        cdef libgsd.gsd_type gsd_type
        cdef void *data_ptr
        if data_array.dtype == numpy.uint8:
            gsd_type = libgsd.GSD_TYPE_UINT8
            data_ptr = __get_ptr_uint8(data_array)
        elif data_array.dtype == numpy.uint16:
            gsd_type = libgsd.GSD_TYPE_UINT16
            data_ptr = __get_ptr_uint16(data_array)
        elif data_array.dtype == numpy.uint32:
            gsd_type = libgsd.GSD_TYPE_UINT32
            data_ptr = __get_ptr_uint32(data_array)
        elif data_array.dtype == numpy.uint64:
            gsd_type = libgsd.GSD_TYPE_UINT64
            data_ptr = __get_ptr_uint64(data_array)
        elif data_array.dtype == numpy.int8:
            gsd_type = libgsd.GSD_TYPE_INT8
            data_ptr = __get_ptr_int8(data_array)
        elif data_array.dtype == numpy.int16:
            gsd_type = libgsd.GSD_TYPE_INT16
            data_ptr = __get_ptr_int16(data_array)
        elif data_array.dtype == numpy.int32:
            gsd_type = libgsd.GSD_TYPE_INT32
            data_ptr = __get_ptr_int32(data_array)
        elif data_array.dtype == numpy.int64:
            gsd_type = libgsd.GSD_TYPE_INT64
            data_ptr = __get_ptr_int64(data_array)
        elif data_array.dtype == numpy.float32:
            gsd_type = libgsd.GSD_TYPE_FLOAT
            data_ptr = __get_ptr_float32(data_array)
        elif data_array.dtype == numpy.float64:
            gsd_type = libgsd.GSD_TYPE_DOUBLE
            data_ptr = __get_ptr_float64(data_array)
        else:
            raise ValueError("invalid type for chunk: " + name)

        logger.debug('write chunk: ' + self.name + ' - ' + name)

        cdef char * c_name
        name_e = name.encode('utf-8')
        c_name = name_e
        with nogil:
            retval = libgsd.gsd_write_chunk(&self.__handle,
                                            c_name,
                                            gsd_type,
                                            N,
                                            M,
                                            0,
                                            data_ptr)

        __raise_on_error(retval, self.name)

    def chunk_exists(self, frame, name):
        """chunk_exists(frame, name)

        Test if a chunk exists.

        Args:
            frame (int): Index of the frame to check
            name (str): Name of the chunk

        Returns:
            bool: True if the chunk exists in the file. False if it does not.

        Example:
            .. ipython:: python

                with gsd.fl.open(name='file.gsd', mode='wb',
                                 application="My application",
                                 schema="My Schema", schema_version=[1,0]) as f:
                    f.write_chunk(name='chunk1',
                                  data=numpy.array([1,2,3,4],
                                                   dtype=numpy.float32))
                    f.write_chunk(name='chunk2',
                                  data=numpy.array([[5,6],[7,8]],
                                                   dtype=numpy.float32))
                    f.end_frame()
                    f.write_chunk(name='chunk1',
                                  data=numpy.array([9,10,11,12],
                                                   dtype=numpy.float32))
                    f.write_chunk(name='chunk2',
                                  data=numpy.array([[13,14],[15,16]],
                                                   dtype=numpy.float32))
                    f.end_frame()

                f = gsd.fl.open(name='file.gsd', mode='rb',
                                application="My application",
                                schema="My Schema", schema_version=[1,0])

                f.chunk_exists(frame=0, name='chunk1')
                f.chunk_exists(frame=0, name='chunk2')
                f.chunk_exists(frame=0, name='chunk3')
                f.chunk_exists(frame=10, name='chunk1')
                f.close()
        """

        cdef const libgsd.gsd_index_entry* index_entry
        cdef char * c_name
        name_e = name.encode('utf-8')
        c_name = name_e
        cdef int64_t c_frame
        c_frame = frame

        logger.debug('chunk exists: ' + self.name + ' - ' + name)

        with nogil:
            index_entry = libgsd.gsd_find_chunk(&self.__handle, c_frame, c_name)

        return index_entry != NULL

    def read_chunk(self, frame, name):
        """read_chunk(frame, name)

        Read a data chunk from the file and return it as a numpy array.

        Args:
            frame (int): Index of the frame to read
            name (str): Name of the chunk

        Returns:
            ``numpy.ndarray[type, ndim=?, mode='c']``: Data read from file.
            ``type`` is determined by the chunk metadata. If the data is
            NxM in the file and M > 1, return a 2D array. If the data is
            Nx1, return a 1D array.

        .. tip::
            Each call invokes a disk read and allocation of a
            new numpy array for storage. To avoid overhead, don't call
            :py:meth:`read_chunk()` on the same chunk repeatedly. Cache the
            arrays instead.

        Example:
            .. ipython:: python

                with gsd.fl.open(name='file.gsd', mode='wb',
                                 application="My application",
                                 schema="My Schema", schema_version=[1,0]) as f:
                    f.write_chunk(name='chunk1',
                                  data=numpy.array([1,2,3,4],
                                                   dtype=numpy.float32))
                    f.write_chunk(name='chunk2',
                                  data=numpy.array([[5,6],[7,8]],
                                                   dtype=numpy.float32))
                    f.end_frame()
                    f.write_chunk(name='chunk1',
                                  data=numpy.array([9,10,11,12],
                                                   dtype=numpy.float32))
                    f.write_chunk(name='chunk2',
                                  data=numpy.array([[13,14],[15,16]],
                                                   dtype=numpy.float32))
                    f.end_frame()

                f = gsd.fl.open(name='file.gsd', mode='rb',
                                application="My application",
                                schema="My Schema", schema_version=[1,0])
                f.read_chunk(frame=0, name='chunk1')
                f.read_chunk(frame=1, name='chunk1')
                @okexcept
                f.read_chunk(frame=2, name='chunk1')
                f.close()
        """

        if not self.__is_open:
            raise ValueError("File is not open")

        cdef const libgsd.gsd_index_entry* index_entry
        cdef char * c_name
        name_e = name.encode('utf-8')
        c_name = name_e
        cdef int64_t c_frame
        c_frame = frame

        with nogil:
            index_entry = libgsd.gsd_find_chunk(&self.__handle, c_frame, c_name)

        if index_entry == NULL:
            raise KeyError("frame " + str(frame) + " / chunk " + name
                           + " not found in: " + self.name)

        cdef libgsd.gsd_type gsd_type
        gsd_type = <libgsd.gsd_type>index_entry.type

        cdef void *data_ptr
        if gsd_type == libgsd.GSD_TYPE_UINT8:
            data_array = numpy.empty(dtype=numpy.uint8,
                                     shape=[index_entry.N, index_entry.M])
        elif gsd_type == libgsd.GSD_TYPE_UINT16:
            data_array = numpy.empty(dtype=numpy.uint16,
                                     shape=[index_entry.N, index_entry.M])
        elif gsd_type == libgsd.GSD_TYPE_UINT32:
            data_array = numpy.empty(dtype=numpy.uint32,
                                     shape=[index_entry.N, index_entry.M])
        elif gsd_type == libgsd.GSD_TYPE_UINT64:
            data_array = numpy.empty(dtype=numpy.uint64,
                                     shape=[index_entry.N, index_entry.M])
        elif gsd_type == libgsd.GSD_TYPE_INT8:
            data_array = numpy.empty(dtype=numpy.int8,
                                     shape=[index_entry.N, index_entry.M])
        elif gsd_type == libgsd.GSD_TYPE_INT16:
            data_array = numpy.empty(dtype=numpy.int16,
                                     shape=[index_entry.N, index_entry.M])
        elif gsd_type == libgsd.GSD_TYPE_INT32:
            data_array = numpy.empty(dtype=numpy.int32,
                                     shape=[index_entry.N, index_entry.M])
        elif gsd_type == libgsd.GSD_TYPE_INT64:
            data_array = numpy.empty(dtype=numpy.int64,
                                     shape=[index_entry.N, index_entry.M])
        elif gsd_type == libgsd.GSD_TYPE_FLOAT:
            data_array = numpy.empty(dtype=numpy.float32,
                                     shape=[index_entry.N, index_entry.M])
        elif gsd_type == libgsd.GSD_TYPE_DOUBLE:
            data_array = numpy.empty(dtype=numpy.float64,
                                     shape=[index_entry.N, index_entry.M])
        else:
            raise ValueError("invalid type for chunk: " + name)

        logger.debug('read chunk: ' + self.name + ' - '
                     + str(frame) + ' - ' + name)

        # only read chunk if we have data
        if index_entry.N != 0 and index_entry.M != 0:
            if gsd_type == libgsd.GSD_TYPE_UINT8:
                data_ptr = __get_ptr_uint8(data_array)
            elif gsd_type == libgsd.GSD_TYPE_UINT16:
                data_ptr = __get_ptr_uint16(data_array)
            elif gsd_type == libgsd.GSD_TYPE_UINT32:
                data_ptr = __get_ptr_uint32(data_array)
            elif gsd_type == libgsd.GSD_TYPE_UINT64:
                data_ptr = __get_ptr_uint64(data_array)
            elif gsd_type == libgsd.GSD_TYPE_INT8:
                data_ptr = __get_ptr_int8(data_array)
            elif gsd_type == libgsd.GSD_TYPE_INT16:
                data_ptr = __get_ptr_int16(data_array)
            elif gsd_type == libgsd.GSD_TYPE_INT32:
                data_ptr = __get_ptr_int32(data_array)
            elif gsd_type == libgsd.GSD_TYPE_INT64:
                data_ptr = __get_ptr_int64(data_array)
            elif gsd_type == libgsd.GSD_TYPE_FLOAT:
                data_ptr = __get_ptr_float32(data_array)
            elif gsd_type == libgsd.GSD_TYPE_DOUBLE:
                data_ptr = __get_ptr_float64(data_array)
            else:
                raise ValueError("invalid type for chunk: " + name)

            with nogil:
                retval = libgsd.gsd_read_chunk(&self.__handle,
                                               data_ptr,
                                               index_entry)

            __raise_on_error(retval, self.name)

        if index_entry.M == 1:
            return data_array.reshape([index_entry.N])
        else:
            return data_array

    def find_matching_chunk_names(self, match):
        """find_matching_chunk_names(match)

        Find all the chunk names in the file that start with the string *match*.

        Args:
            match (str): Start of the chunk name to match

        Returns:
            typing.List[str]: Matching chunk names

        Example:
            .. ipython:: python

                with gsd.fl.open(name='file.gsd', mode='wb',
                                 application="My application",
                                 schema="My Schema", schema_version=[1,0]) as f:
                    f.write_chunk(name='data/chunk1',
                                  data=numpy.array([1,2,3,4],
                                                   dtype=numpy.float32))
                    f.write_chunk(name='data/chunk2',
                                  data=numpy.array([[5,6],[7,8]],
                                                   dtype=numpy.float32))
                    f.write_chunk(name='input/chunk3',
                                  data=numpy.array([9, 10],
                                                   dtype=numpy.float32))
                    f.end_frame()
                    f.write_chunk(name='input/chunk4',
                                  data=numpy.array([11, 12, 13, 14],
                                                   dtype=numpy.float32))
                    f.end_frame()

                f = gsd.fl.open(name='file.gsd', mode='rb',
                                application="My application",
                                schema="My Schema", schema_version=[1,0])

                f.find_matching_chunk_names('')
                f.find_matching_chunk_names('data')
                f.find_matching_chunk_names('input')
                f.find_matching_chunk_names('other')
                f.close()
        """

        if not self.__is_open:
            raise ValueError("File is not open")

        cdef const char * c_found
        cdef char * c_match
        match_e = match.encode('utf-8')
        c_match = match_e

        retval = []

        with nogil:
            c_found = libgsd.gsd_find_matching_chunk_name(&self.__handle,
                                                          c_match,
                                                          NULL)

        while c_found != NULL:
            retval.append(c_found.decode('utf-8'))

            with nogil:
                c_found = libgsd.gsd_find_matching_chunk_name(&self.__handle,
                                                              c_match,
                                                              c_found)

        return retval

    def upgrade(self):
        """upgrade()

        Upgrade a GSD file to the v2 specification in place. The file must be
        open in a writable mode.

        """

        if not self.__is_open:
            raise ValueError("File is not open")

        logger.info('upgrading file: ' + self.name)

        with nogil:
            retval = libgsd.gsd_upgrade(&self.__handle)

        __raise_on_error(retval, self.name)

    def __enter__(self):
        return self

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

    def __reduce__(self):
        """Allows filehandles to be pickled when in read only mode."""
        if self.mode not in ['rb', 'rb+']:
            raise PickleError("Only read only GSDFiles can be pickled.")
        return (GSDFile,
                (self.name, self.mode, self.application,
                    self.schema, self.schema_version),
                )

    property name:
        def __get__(self):
            return self.name

    property mode:
        def __get__(self):
            return self.mode

    property gsd_version:
        def __get__(self):
            cdef uint32_t v = self.__handle.header.gsd_version
            return (v >> 16, v & 0xffff)

    property schema_version:
        def __get__(self):
            cdef uint32_t v = self.__handle.header.schema_version
            return (v >> 16, v & 0xffff)

    property schema:
        def __get__(self):
            return self.__handle.header.schema.decode('utf-8')

    property application:
        def __get__(self):
            return self.__handle.header.application.decode('utf-8')

    property nframes:
        def __get__(self):
            if not self.__is_open:
                raise ValueError("File is not open")

            return libgsd.gsd_get_nframes(&self.__handle)

    def __dealloc__(self):
        if self.__is_open:
            logger.info('closing file: ' + self.name)
            libgsd.gsd_close(&self.__handle)
            self.__is_open = False
