# This file is part of h5py, a Python interface to the HDF5 library.
#
# http://www.h5py.org
#
# Copyright 2008-2013 Andrew Collette and contributors
#
# License:  Standard 3-clause BSD; see "license.txt" for full license terms
#           and contributor agreement.

"""
    Attributes testing module

    Covers all operations which access the .attrs property, with the
    exception of data read/write and type conversion.  Those operations
    are tested by module test_attrs_data.
"""

import numpy as np

from collections.abc import MutableMapping

from .common import TestCase, ut

import h5py
from h5py import File
from h5py import h5a,  h5t
from h5py import AttributeManager


class BaseAttrs(TestCase):

    def setUp(self):
        self.f = File(self.mktemp(), 'w')

    def tearDown(self):
        if self.f:
            self.f.close()

class TestRepr(TestCase):

    """ Feature: AttributeManager provide a helpful
        __repr__ string
    """

    def test_repr(self):
        grp = self.f.create_group('grp')
        grp.attrs.create('att', 1)
        self.assertIsInstance(repr(grp.attrs), str)
        grp.id.close()
        self.assertIsInstance(repr(grp.attrs), str)


class TestAccess(BaseAttrs):

    """
        Feature: Attribute creation/retrieval via special methods
    """

    def test_create(self):
        """ Attribute creation by direct assignment """
        self.f.attrs['a'] = 4.0
        self.assertEqual(list(self.f.attrs.keys()), ['a'])
        self.assertEqual(self.f.attrs['a'], 4.0)

    def test_create_2(self):
        """ Attribute creation by create() method """
        self.f.attrs.create('a', 4.0)
        self.assertEqual(list(self.f.attrs.keys()), ['a'])
        self.assertEqual(self.f.attrs['a'], 4.0)

    def test_modify(self):
        """ Attributes are modified by direct assignment"""
        self.f.attrs['a'] = 3
        self.assertEqual(list(self.f.attrs.keys()), ['a'])
        self.assertEqual(self.f.attrs['a'], 3)
        self.f.attrs['a'] = 4
        self.assertEqual(list(self.f.attrs.keys()), ['a'])
        self.assertEqual(self.f.attrs['a'], 4)

    def test_modify_2(self):
        """ Attributes are modified by modify() method """
        self.f.attrs.modify('a',3)
        self.assertEqual(list(self.f.attrs.keys()), ['a'])
        self.assertEqual(self.f.attrs['a'], 3)

        self.f.attrs.modify('a', 4)
        self.assertEqual(list(self.f.attrs.keys()), ['a'])
        self.assertEqual(self.f.attrs['a'], 4)

        # If the attribute doesn't exist, create new
        self.f.attrs.modify('b', 5)
        self.assertEqual(list(self.f.attrs.keys()), ['a', 'b'])
        self.assertEqual(self.f.attrs['a'], 4)
        self.assertEqual(self.f.attrs['b'], 5)

        # Shape of new value is incompatible with the previous
        new_value = np.arange(5)
        with self.assertRaises(TypeError):
            self.f.attrs.modify('b', new_value)

    def test_overwrite(self):
        """ Attributes are silently overwritten """
        self.f.attrs['a'] = 4.0
        self.f.attrs['a'] = 5.0
        self.assertEqual(self.f.attrs['a'], 5.0)

    def test_rank(self):
        """ Attribute rank is preserved """
        self.f.attrs['a'] = (4.0, 5.0)
        self.assertEqual(self.f.attrs['a'].shape, (2,))
        self.assertArrayEqual(self.f.attrs['a'], np.array((4.0,5.0)))

    def test_single(self):
        """ Attributes of shape (1,) don't become scalars """
        self.f.attrs['a'] = np.ones((1,))
        out = self.f.attrs['a']
        self.assertEqual(out.shape, (1,))
        self.assertEqual(out[()], 1)

    def test_access_exc(self):
        """ Attempt to access missing item raises KeyError """
        with self.assertRaises(KeyError):
            self.f.attrs['a']

    def test_get_id(self):
        self.f.attrs['a'] = 4.0
        aid = self.f.attrs.get_id('a')
        assert isinstance(aid, h5a.AttrID)

        with self.assertRaises(KeyError):
            self.f.attrs.get_id('b')

class TestDelete(BaseAttrs):

    """
        Feature: Deletion of attributes using __delitem__
    """

    def test_delete(self):
        """ Deletion via "del" """
        self.f.attrs['a'] = 4.0
        self.assertIn('a', self.f.attrs)
        del self.f.attrs['a']
        self.assertNotIn('a', self.f.attrs)

    def test_delete_exc(self):
        """ Attempt to delete missing item raises KeyError """
        with self.assertRaises(KeyError):
            del self.f.attrs['a']


class TestUnicode(BaseAttrs):

    """
        Feature: Attributes can be accessed via Unicode or byte strings
    """

    def test_ascii(self):
        """ Access via pure-ASCII byte string """
        self.f.attrs[b"ascii"] = 42
        out = self.f.attrs[b"ascii"]
        self.assertEqual(out, 42)

    def test_raw(self):
        """ Access via non-ASCII byte string """
        name = b"non-ascii\xfe"
        self.f.attrs[name] = 42
        out = self.f.attrs[name]
        self.assertEqual(out, 42)

    def test_unicode(self):
        """ Access via Unicode string with non-ascii characters """
        name = "Omega" + chr(0x03A9)
        self.f.attrs[name] = 42
        out = self.f.attrs[name]
        self.assertEqual(out, 42)


class TestCreate(BaseAttrs):

    """
        Options for explicit attribute creation
    """

    def test_named(self):
        """ Attributes created from named types link to the source type object
        """
        self.f['type'] = np.dtype('u8')
        self.f.attrs.create('x', 42, dtype=self.f['type'])
        self.assertEqual(self.f.attrs['x'], 42)
        aid = h5a.open(self.f.id, b'x')
        htype = aid.get_type()
        htype2 = self.f['type'].id
        self.assertEqual(htype, htype2)
        self.assertTrue(htype.committed())

    def test_empty(self):
        # https://github.com/h5py/h5py/issues/1540
        """ Create attribute with h5py.Empty value
        """
        self.f.attrs.create('empty', h5py.Empty('f'))
        self.assertEqual(self.f.attrs['empty'], h5py.Empty('f'))

        self.f.attrs.create('empty', h5py.Empty(None))
        self.assertEqual(self.f.attrs['empty'], h5py.Empty(None))

class TestMutableMapping(BaseAttrs):
    '''Tests if the registration of AttributeManager as a MutableMapping
    behaves as expected
    '''
    def test_resolution(self):
        assert issubclass(AttributeManager, MutableMapping)
        assert isinstance(self.f.attrs, MutableMapping)

    def test_validity(self):
        '''
        Test that the required functions are implemented.
        '''
        AttributeManager.__getitem__
        AttributeManager.__setitem__
        AttributeManager.__delitem__
        AttributeManager.__iter__
        AttributeManager.__len__

class TestVlen(BaseAttrs):
    def test_vlen(self):
        a = np.array([np.arange(3), np.arange(4)],
            dtype=h5t.vlen_dtype(int))
        self.f.attrs['a'] = a
        self.assertArrayEqual(self.f.attrs['a'][0], a[0])

    def test_vlen_s1(self):
        dt = h5py.vlen_dtype(np.dtype('S1'))
        a = np.empty((1,), dtype=dt)
        a[0] = np.array([b'a', b'b'], dtype='S1')

        self.f.attrs.create('test', a)
        self.assertArrayEqual(self.f.attrs['test'][0], a[0])


class TestTrackOrder(BaseAttrs):
    def fill_attrs(self, track_order):
        attrs = self.f.create_group('test', track_order=track_order).attrs
        for i in range(100):
            attrs[str(i)] = i
        return attrs

    @ut.skipUnless(h5py.version.hdf5_version_tuple >= (1, 10, 6), 'HDF5 1.10.6 required')
    # https://forum.hdfgroup.org/t/bug-h5arename-fails-unexpectedly/4881
    def test_track_order(self):
        attrs = self.fill_attrs(track_order=True)  # creation order
        self.assertEqual(list(attrs),
                         [str(i) for i in range(100)])

    def test_no_track_order(self):
        attrs = self.fill_attrs(track_order=False)  # name alphanumeric
        self.assertEqual(list(attrs),
                         sorted([str(i) for i in range(100)]))

    def fill_attrs2(self, track_order):
        group = self.f.create_group('test', track_order=track_order)
        for i in range(12):
            group.attrs[str(i)] = i
        return group

    @ut.skipUnless(h5py.version.hdf5_version_tuple >= (1, 10, 6), 'HDF5 1.10.6 required')
    def test_track_order_overwrite_delete(self):
        # issue 1385
        group = self.fill_attrs2(track_order=True)  # creation order
        self.assertEqual(group.attrs["11"], 11)
        # overwrite attribute
        group.attrs['11'] = 42.0
        self.assertEqual(group.attrs["11"], 42.0)
        # delete attribute
        self.assertIn('10', group.attrs)
        del group.attrs['10']
        self.assertNotIn('10', group.attrs)


class TestDatatype(BaseAttrs):

    def test_datatype(self):
        self.f['foo'] = np.dtype('f')
        dt = self.f['foo']
        self.assertEqual(list(dt.attrs.keys()), [])
        dt.attrs.create('a', 4.0)
        self.assertEqual(list(dt.attrs.keys()), ['a'])
        self.assertEqual(list(dt.attrs.values()), [4.0])

def test_python_int_uint64(writable_file):
    f = writable_file
    data = [np.iinfo(np.int64).max, np.iinfo(np.int64).max + 1]

    # Check creating a new attribute
    f.attrs.create('a', data, dtype=np.uint64)
    assert f.attrs['a'].dtype == np.dtype(np.uint64)
    np.testing.assert_array_equal(f.attrs['a'], np.array(data, dtype=np.uint64))

    # Check modifying an existing attribute
    f.attrs.modify('a', data)
    np.testing.assert_array_equal(f.attrs['a'], np.array(data, dtype=np.uint64))
