import logging
import numbers

import pyvips
from pyvips import ffi, vips_lib, gobject_lib, \
    glib_lib, Error, _to_bytes, _to_string, type_name, type_from_name, \
    at_least_libvips

logger = logging.getLogger(__name__)


class GValue(object):

    """Wrap GValue in a Python class.

    This class wraps :class:`.GValue` in a convenient interface. You can use
    instances of this class to get and set :class:`.GObject` properties.

    On construction, :class:`.GValue` is all zero (empty). You can pass it to
    a get function to have it filled by :class:`.GObject`, or use init to
    set a type, set to set a value, then use it to set an object property.

    GValue lifetime is managed automatically.

    """
    __slots__ = ('pointer', 'gvalue')

    # look up some common gtypes at init for speed
    gbool_type = type_from_name('gboolean')
    gint_type = type_from_name('gint')
    guint64_type = type_from_name('guint64')
    gdouble_type = type_from_name('gdouble')
    gstr_type = type_from_name('gchararray')
    genum_type = type_from_name('GEnum')
    gflags_type = type_from_name('GFlags')
    gobject_type = type_from_name('GObject')
    image_type = type_from_name('VipsImage')
    array_int_type = type_from_name('VipsArrayInt')
    array_double_type = type_from_name('VipsArrayDouble')
    array_image_type = type_from_name('VipsArrayImage')
    refstr_type = type_from_name('VipsRefString')
    blob_type = type_from_name('VipsBlob')
    source_type = type_from_name('VipsSource')
    target_type = type_from_name('VipsTarget')

    pyvips.vips_lib.vips_band_format_get_type()
    format_type = type_from_name('VipsBandFormat')

    if at_least_libvips(8, 6):
        pyvips.vips_lib.vips_blend_mode_get_type()
    blend_mode_type = type_from_name('VipsBlendMode')

    # map a gtype to the name of the corresponding Python type
    _gtype_to_python = {
        gbool_type: 'bool',
        gint_type: 'int',
        guint64_type: 'long',  # Note: int and long are unified in Python 3
        gdouble_type: 'float',
        gstr_type: 'str',
        refstr_type: 'str',
        genum_type: 'str',
        gflags_type: 'int',
        gobject_type: 'GObject',
        image_type: 'Image',
        array_int_type: 'list[int]',
        array_double_type: 'list[float]',
        array_image_type: 'list[Image]',
        blob_type: 'str',
        source_type: 'Source',
        target_type: 'Target',
    }

    @staticmethod
    def gtype_to_python(gtype):
        """Map a gtype to the name of the Python type we use to represent it.

        """

        fundamental = gobject_lib.g_type_fundamental(gtype)

        # enums can be strings or class members ... we want to generate a union
        # type
        if fundamental == GValue.genum_type:
            name = type_name(gtype)
            if name.startswith('Vips'):
                name = name[4:]
            return f"Union[str, {name}]"
        if gtype in GValue._gtype_to_python:
            return GValue._gtype_to_python[gtype]
        if fundamental in GValue._gtype_to_python:
            return GValue._gtype_to_python[fundamental]
        return '<unknown type>'

    @staticmethod
    def to_enum(gtype, value):
        """Turn a string into an enum value ready to be passed into libvips.

        """

        if isinstance(value, str):
            enum_value = vips_lib.vips_enum_from_nick(b'pyvips', gtype,
                                                      _to_bytes(value))
            if enum_value < 0:
                raise Error('no value {0} in gtype {1} ({2})'.
                            format(value, type_name(gtype), gtype))
        else:
            enum_value = value

        return enum_value

    @staticmethod
    def from_enum(gtype, enum_value):
        """Turn an int back into an enum string.

        """

        pointer = vips_lib.vips_enum_nick(gtype, enum_value)
        if pointer == ffi.NULL:
            raise Error('value not in enum')

        return _to_string(pointer)

    @staticmethod
    def to_flag(gtype, value):
        """Turn a string into a flag value ready to be passed into libvips.

        """

        if isinstance(value, str):
            flag_value = vips_lib.vips_flags_from_nick(b'pyvips', gtype,
                                                       _to_bytes(value))
            if flag_value < 0:
                raise Error('no value {0} in gtype {1} ({2})'.
                            format(value, type_name(gtype), gtype))
        else:
            flag_value = value

        return flag_value

    def __init__(self):
        # allocate memory for the gvalue which will be freed on GC
        self.pointer = ffi.new('GValue *')
        # logger.debug('GValue.__init__: pointer = %s', self.pointer)

        # and tag it to be unset on GC as well
        self.gvalue = ffi.gc(self.pointer, gobject_lib.g_value_unset)
        # logger.debug('GValue.__init__: gvalue = %s', self.gvalue)

    def set_type(self, gtype):
        """Set the type of a GValue.

        GValues have a set type, fixed at creation time. Use set_type to set
        the type of a GValue before assigning to it.

        GTypes are 32 or 64-bit integers (depending on the platform). See
        type_find.

        """

        gobject_lib.g_value_init(self.gvalue, gtype)

    def set(self, value):
        """Set a GValue.

        The value is converted to the type of the GValue, if possible, and
        assigned.

        """

        # logger.debug('GValue.set: value = %s', value)

        gtype = self.gvalue.g_type
        fundamental = gobject_lib.g_type_fundamental(gtype)

        if gtype == GValue.gbool_type:
            gobject_lib.g_value_set_boolean(self.gvalue, value)
        elif gtype == GValue.gint_type:
            gobject_lib.g_value_set_int(self.gvalue, int(value))
        elif gtype == GValue.guint64_type:
            gobject_lib.g_value_set_uint64(self.gvalue, value)
        elif gtype == GValue.gdouble_type:
            gobject_lib.g_value_set_double(self.gvalue, value)
        elif fundamental == GValue.genum_type:
            gobject_lib.g_value_set_enum(self.gvalue,
                                         GValue.to_enum(gtype, value))
        elif fundamental == GValue.gflags_type:
            gobject_lib.g_value_set_flags(self.gvalue,
                                          GValue.to_flag(gtype, value))
        elif gtype == GValue.gstr_type:
            gobject_lib.g_value_set_string(self.gvalue, _to_bytes(value))
        elif gtype == GValue.refstr_type:
            vips_lib.vips_value_set_ref_string(self.gvalue, _to_bytes(value))
        elif fundamental == GValue.gobject_type:
            gobject_lib.g_value_set_object(self.gvalue, value.pointer)
        elif gtype == GValue.array_int_type:
            if isinstance(value, numbers.Number):
                value = [value]

            array = ffi.new('int[]', value)
            vips_lib.vips_value_set_array_int(self.gvalue, array, len(value))
        elif gtype == GValue.array_double_type:
            if isinstance(value, numbers.Number):
                value = [value]

            array = ffi.new('double[]', value)
            vips_lib.vips_value_set_array_double(self.gvalue, array,
                                                 len(value))
        elif gtype == GValue.array_image_type:
            if isinstance(value, pyvips.Image):
                value = [value]

            vips_lib.vips_value_set_array_image(self.gvalue, len(value))
            array = vips_lib.vips_value_get_array_image(self.gvalue, ffi.NULL)
            for i, image in enumerate(value):
                gobject_lib.g_object_ref(image.pointer)
                array[i] = image.pointer
        elif gtype == GValue.blob_type:
            # we need to set the blob to a copy of the string that vips_lib
            # can own
            memory = glib_lib.g_malloc(len(value))
            ffi.memmove(memory, value, len(value))

            # In API mode, we use set_blob_free in a backwards compatible way.
            # For pre-8.6 libvipses, in ABI mode, we declare the type of the
            # free func in set_blob incorrectly so that we can pass g_free
            # at runtime without triggering an exception.
            if pyvips.API_mode or at_least_libvips(8, 6):
                vips_lib.vips_value_set_blob_free(self.gvalue,
                                                  memory, len(value))
            else:
                vips_lib.vips_value_set_blob(self.gvalue,
                                             glib_lib.g_free,
                                             memory, len(value))
        else:
            raise Error('unsupported gtype for set {0}, fundamental {1}'.
                        format(type_name(gtype), type_name(fundamental)))

    def get(self):
        """Get the contents of a GValue.

        The contents of the GValue are read out as a Python type.
        """

        # logger.debug('GValue.get: self = %s', self)

        gtype = self.gvalue.g_type
        fundamental = gobject_lib.g_type_fundamental(gtype)

        result = None

        if gtype == GValue.gbool_type:
            result = bool(gobject_lib.g_value_get_boolean(self.gvalue))
        elif gtype == GValue.gint_type:
            result = gobject_lib.g_value_get_int(self.gvalue)
        elif gtype == GValue.guint64_type:
            result = gobject_lib.g_value_get_uint64(self.gvalue)
        elif gtype == GValue.gdouble_type:
            result = gobject_lib.g_value_get_double(self.gvalue)
        elif fundamental == GValue.genum_type:
            return GValue.from_enum(gtype,
                                    gobject_lib.g_value_get_enum(self.gvalue))
        elif fundamental == GValue.gflags_type:
            result = gobject_lib.g_value_get_flags(self.gvalue)
        elif gtype == GValue.gstr_type:
            pointer = gobject_lib.g_value_get_string(self.gvalue)

            if pointer != ffi.NULL:
                result = _to_string(pointer)
        elif gtype == GValue.refstr_type:
            psize = ffi.new('size_t *')
            pointer = vips_lib.vips_value_get_ref_string(self.gvalue, psize)

            # psize[0] will be number of bytes in string, but just assume it's
            # NULL-terminated
            result = _to_string(pointer)
        elif gtype == GValue.image_type:
            # g_value_get_object() will not add a ref ... that is
            # held by the gvalue
            go = gobject_lib.g_value_get_object(self.gvalue)
            vi = ffi.cast('VipsImage *', go)

            # we want a ref that will last with the life of the vimage:
            # this ref is matched by the unref that's attached to finalize
            # by Image()
            gobject_lib.g_object_ref(vi)

            result = pyvips.Image(vi)
        elif gtype == GValue.array_int_type:
            pint = ffi.new('int *')
            array = vips_lib.vips_value_get_array_int(self.gvalue, pint)

            result = []
            for i in range(0, pint[0]):
                result.append(array[i])
        elif gtype == GValue.array_double_type:
            pint = ffi.new('int *')
            array = vips_lib.vips_value_get_array_double(self.gvalue, pint)

            result = []
            for i in range(0, pint[0]):
                result.append(array[i])
        elif gtype == GValue.array_image_type:
            pint = ffi.new('int *')
            array = vips_lib.vips_value_get_array_image(self.gvalue, pint)

            result = []
            for i in range(0, pint[0]):
                vi = array[i]
                gobject_lib.g_object_ref(vi)
                image = pyvips.Image(vi)
                result.append(image)
        elif gtype == GValue.blob_type:
            psize = ffi.new('size_t *')
            array = vips_lib.vips_value_get_blob(self.gvalue, psize)
            buf = ffi.cast('char*', array)

            result = ffi.unpack(buf, psize[0])
        else:
            raise Error('unsupported gtype for get {0}'.
                        format(type_name(gtype)))

        return result


__all__ = ['GValue']
