"""This module defines the funtions byref_at(cobj, offset)
and cast_field(struct, fieldname, fieldtype).
"""

from ctypes import *


def _calc_offset():
    # Internal helper function that calculates where the object
    # returned by a byref() call stores the pointer.

    # The definition of PyCArgObject in C code (that is the type of
    # object that a byref() call returns):
    class PyCArgObject(Structure):
        class value(Union):
            _fields_ = [
                ("c", c_char),
                ("h", c_short),
                ("i", c_int),
                ("l", c_long),
                ("q", c_longlong),
                ("d", c_double),
                ("f", c_float),
                ("p", c_void_p),
            ]

        #
        # Thanks to Lenard Lindstrom for this tip:
        # sizeof(PyObject_HEAD) is the same as object.__basicsize__.
        #
        _fields_ = [
            ("PyObject_HEAD", c_byte * object.__basicsize__),
            ("pffi_type", c_void_p),
            ("tag", c_char),
            ("value", value),
            ("obj", c_void_p),
            ("size", c_int),
        ]

        _anonymous_ = ["value"]

    # additional checks to make sure that everything works as expected

    if sizeof(PyCArgObject) != type(byref(c_int())).__basicsize__:
        raise RuntimeError("sizeof(PyCArgObject) invalid")

    obj = c_int()
    ref = byref(obj)

    argobj = PyCArgObject.from_address(id(ref))

    if argobj.obj != id(obj) or argobj.p != addressof(obj) or argobj.tag != "P":
        raise RuntimeError("PyCArgObject field definitions incorrect")

    return PyCArgObject.p.offset  # offset of the pointer field


################################################################
#
# byref_at
#
def byref_at(
    obj,
    offset,
    _byref=byref,
    _c_void_p_from_address=c_void_p.from_address,
    _byref_pointer_offset=_calc_offset(),
):
    """byref_at(cobj, offset) behaves similar this C code:

        (((char *)&obj) + offset)

    In other words, the returned 'pointer' points to the address of
    'cobj' + 'offset'.  'offset' is in units of bytes.
    """
    ref = _byref(obj)
    # Change the pointer field in the created byref object by adding
    # 'offset' to it:
    _c_void_p_from_address(id(ref) + _byref_pointer_offset).value += offset
    return ref


################################################################
#
# cast_field
#
def cast_field(
    struct,
    fieldname,
    fieldtype,
    offset=0,
    _POINTER=POINTER,
    _byref_at=byref_at,
    _byref=byref,
    _divmod=divmod,
    _sizeof=sizeof,
):
    """cast_field(struct, fieldname, fieldtype)

    Return the contents of a struct field as it it were of type
    'fieldtype'.
    """
    fieldoffset = getattr(type(struct), fieldname).offset
    return cast(_byref_at(struct, fieldoffset), _POINTER(fieldtype))[0]


__all__ = ["byref_at", "cast_field"]
