# Source code for pyvips.voperation

    import logging

    import pyvips
    from pyvips import ffi, vips_lib, Error, _to_bytes, _to_string, GValue, \
        type_map, type_from_name, nickname_find, at_least_libvips

    logger = logging.getLogger(__name__)

    # values for VipsArgumentFlags
    _REQUIRED = 1
    _CONSTRUCT = 2
    _SET_ONCE = 4
    _SET_ALWAYS = 8
    _INPUT = 16
    _OUTPUT = 32
    _DEPRECATED = 64
    _MODIFY = 128

    # for VipsOperationFlags
    _OPERATION_NOCACHE = 4
    _OPERATION_DEPRECATED = 8

    class Introspect(object):
        """Build introspection data for operations.

        Make an operation, introspect it, and build a structure representing
        everything we know about it.

        """
        __slots__ = ('description', 'flags', 'details',
                     'required_input', 'optional_input',
                     'required_output', 'optional_output',
                     'doc_optional_input', 'doc_optional_output',
                     'member_x', 'method_args')

        def __init__(self, operation_name):
            op = Operation.new_from_name(operation_name)

            self.description = op.get_description()
            self.flags = vips_lib.vips_operation_get_flags(op.pointer)

            # build a list of constructor arg [name, flags] pairs in arg order
            arguments = []

            def add_args(name, flags):
                if (flags & _CONSTRUCT) != 0:
                    # libvips uses '-' to separate parts of arg names, but we
                    # need '_' for Python
                    name = name.replace('-', '_')
                    arguments.append([name, flags])

            if at_least_libvips(8, 7):
                p_names = ffi.new('char**[1]')
                p_flags = ffi.new('int*[1]')
                p_n_args = ffi.new('int[1]')
                result = vips_lib.vips_object_get_args(op.vobject,
                                                       p_names, p_flags, p_n_args)
                if result != 0:
                    raise Error('unable to get arguments from operation')
                p_names = p_names[0]
                p_flags = p_flags[0]
                n_args = p_n_args[0]

                for i in range(0, n_args):
                    add_args(_to_string(p_names[i]), p_flags[i])
            else:
                def add_construct(self, pspec, argument_class,
                                  argument_instance, a, b):
                    add_args(_to_string(pspec.name), argument_class.flags)
                    return ffi.NULL

                cb = ffi.callback('VipsArgumentMapFn', add_construct)
                vips_lib.vips_argument_map(op.vobject, cb, ffi.NULL, ffi.NULL)

            # logger.debug('arguments = %s', self.arguments)

            # build a hash from arg name to detailed arg information
            self.details = {}
            for name, flags in arguments:
                self.details[name] = {
                    "name": name,
                    "flags": flags,
                    "blurb": op.get_blurb(name),
                    "type": op.get_typeof(name)
                }

            # lists of arg names by category
            self.required_input = []
            self.optional_input = []
            self.required_output = []
            self.optional_output = []

            # same, but with deprecated args filtered out ... this is the set we
            # show in documentation
            self.doc_optional_input = []
            self.doc_optional_output = []

            for name, flags in arguments:
                if ((flags & _INPUT) != 0 and
                        (flags & _REQUIRED) != 0 and
                        (flags & _DEPRECATED) == 0):
                    self.required_input.append(name)

                    # required inputs which we MODIFY are also required outputs
                    if (flags & _MODIFY) != 0:
                        self.required_output.append(name)

                if ((flags & _OUTPUT) != 0 and
                        (flags & _REQUIRED) != 0 and
                        (flags & _DEPRECATED) == 0):
                    self.required_output.append(name)

                # deprecated optional args get on to the main arg lists, but are
                # filtered from the documented set
                if ((flags & _INPUT) != 0 and
                        (flags & _REQUIRED) == 0):
                    self.optional_input.append(name)

                    if (flags & _DEPRECATED) == 0:
                        self.doc_optional_input.append(name)

                if ((flags & _OUTPUT) != 0 and
                        (flags & _REQUIRED) == 0):
                    self.optional_output.append(name)

                    if (flags & _DEPRECATED) == 0:
                        self.doc_optional_output.append(name)

            # find the first required input image arg, if any ... that will be self
            self.member_x = None
            for name in self.required_input:
                details = self.details[name]
                if details['type'] == GValue.image_type:
                    self.member_x = name
                    break

            # method args are required args, but without the image they are a
            # method on
            if self.member_x is not None:
                self.method_args = list(self.required_input)
                self.method_args.remove(self.member_x)
            else:
                self.method_args = self.required_input

        # a hash mapping operation names to introspection data
        _introspect_cache = {}

        @classmethod
        def get(cls, operation_name):
            if operation_name not in cls._introspect_cache:
                cls._introspect_cache[operation_name] = Introspect(operation_name)

            return cls._introspect_cache[operation_name]

    # search an array with a predicate, recursing into subarrays as we see them
    # used to find the match_image for an operation
    def _find_inside(pred, thing):
        if pred(thing):
            return thing

        if isinstance(thing, list) or isinstance(thing, tuple):
            for x in thing:
                result = _find_inside(pred, x)

                if result is not None:
                    return result

        return None

    class Operation(pyvips.VipsObject):
        """Call libvips operations.

        This class wraps the libvips VipsOperation class.

        """
        __slots__ = ()

        # cache nickname -> docstring here
        _docstring_cache = {}

        def __init__(self, pointer):
            # logger.debug('Operation.__init__: pointer = %s', pointer)
            super(Operation, self).__init__(pointer)

        @staticmethod
        def new_from_name(operation_name):
            vop = vips_lib.vips_operation_new(_to_bytes(operation_name))
            if vop == ffi.NULL:
                raise Error(f'no such operation {operation_name}')
            return Operation(vop)

        def set(self, name, flags, match_image, value):
            # if the object wants an image and we have a constant, _imageize it
            #
            # if the object wants an image array, _imageize any constants in the
            # array
            if match_image:
                gtype = self.get_typeof(name)

                if gtype == pyvips.GValue.image_type:
                    value = pyvips.Image._imageize(match_image, value)
                elif gtype == pyvips.GValue.array_image_type:
                    value = [pyvips.Image._imageize(match_image, x)
                             for x in value]

            # MODIFY args need to be copied before they are set
            if (flags & _MODIFY) != 0:
                # logger.debug('copying MODIFY arg %s', name)
                # make sure we have a unique copy
                value = value.copy().copy_memory()

            super(Operation, self).set(name, value)

        @staticmethod
        def call(operation_name, *args, **kwargs):
            """Call a libvips operation.

            Use this method to call any libvips operation. For example::

                black_image = pyvips.Operation.call('black', 10, 10)

            See the Introduction for notes on how this works.

            """

            logger.debug('VipsOperation.call: operation_name = %s', operation_name)
            # logger.debug('VipsOperation.call: args = %s, kwargs =%s',
            #              args, kwargs)

            intro = Introspect.get(operation_name)

            if len(intro.required_input) != len(args):
                raise Error(f'{operation_name} needs {len(intro.required_input)} '
                            f'arguments, but {len(args)} given')

            op = Operation.new_from_name(operation_name)

            # set any string options before any args so they can't be
            # overridden
            string_options = kwargs.pop('string_options', '')
            if not op.set_string(string_options):
                raise Error(f'unable to call {operation_name}')

            # the first image argument is the thing we expand constants to
            # match ... look inside tables for images, since we may be passing
            # an array of images as a single param
            match_image = _find_inside(lambda x: isinstance(x, pyvips.Image),
                                       args)

            logger.debug('VipsOperation.call: match_image = %s', match_image)

            # collect a list of all input references here
            # we can't use a set because set elements are unique under "==", and
            # Python checks memoryview equality with hash functions, not pointer
            # equality
            references = []

            # does a list contain an element using pointer equality
            # we can't use "in" since that uses "==", which means hash equality
            def contains(array, x):
                for y in array:
                    if id(x) == id(y):
                        return True
                return False

            def add_reference(x):
                if isinstance(x, pyvips.Image):
                    for i in x._references:
                        if not contains(references, i):
                            references.append(i)
                return False

            # set required input args
            for name, value in zip(intro.required_input, args):
                _find_inside(add_reference, value)
                op.set(name, intro.details[name]['flags'], match_image, value)

            # set any optional args
            for name in kwargs:
                if (name not in intro.optional_input and
                        name not in intro.optional_output):
                    raise Error(f'{operation_name} does not support optional '
                                f'argument {name}')

                value = kwargs[name]
                details = intro.details[name]

                if (details['flags'] & _DEPRECATED) != 0:
                    logger.info('%s argument %s is deprecated',
                                operation_name, name)

                _find_inside(add_reference, value)
                op.set(name, details['flags'], match_image, value)

            # build operation
            vop = vips_lib.vips_cache_operation_build(op.pointer)
            if vop == ffi.NULL:
                vips_lib.vips_object_unref_outputs(op.vobject)
                raise Error(f'unable to call {operation_name}')
            op = Operation(vop)

            # attach all input refs to output x
            def set_reference(x):
                if isinstance(x, pyvips.Image):
                    x._references.extend(references)
                return False

            # fetch required output args (plus modified input images)
            result = []
            for name in intro.required_output:
                value = op.get(name)
                _find_inside(set_reference, value)
                result.append(value)

            # fetch optional output args
            opts = {}
            for name in intro.optional_output:
                if name in kwargs:
                    value = op.get(name)
                    _find_inside(set_reference, value)
                    opts[name] = value

            if len(opts) > 0:
                result.append(opts)

            vips_lib.vips_object_unref_outputs(op.vobject)

            if len(result) == 0:
                result = None
            elif len(result) == 1:
                result = result[0]

            logger.debug('VipsOperation.call: result = %s', result)

            return result

        @staticmethod
        def generate_docstring(operation_name):
            """Make a google-style docstring.

            This is used to generate help() output.

            """

            # we cache these to save regeneration
            if operation_name in Operation._docstring_cache:
                return Operation._docstring_cache[operation_name]

            intro = Introspect.get(operation_name)
            if (intro.flags & _OPERATION_DEPRECATED) != 0:
                raise Error('No such operator.',
                            f'operator "{operation_name}" is deprecated')

            result = intro.description[0].upper() + intro.description[1:] + '.\n\n'
            result += 'Example:\n'

            result += '   ' + ', '.join(intro.required_output) + ' = '
            if intro.member_x is not None:
                result += intro.member_x + '.' + operation_name + '('
            else:
                result += 'pyvips.Image.' + operation_name + '('

            args = []
            args += intro.method_args
            args += [x + '=' + GValue.gtype_to_python(intro.details[x]['type'])
                     for x in intro.doc_optional_input]
            args += [x + '=bool'
                     for x in intro.doc_optional_output]
            result += ", ".join(args) + ')\n'

            def argstr(name):
                details = intro.details[name]
                return (u'    {0} ({1}): {2}\n'.
                        format(name,
                               GValue.gtype_to_python(details['type']),
                               details['blurb']))

            result += '\nReturns:\n'
            for name in intro.required_output:
                result += argstr(name)

            result += '\nArgs:\n'
            if intro.member_x is not None:
                result += argstr(intro.member_x)
            for name in intro.method_args:
                result += argstr(name)

            if len(intro.doc_optional_input) > 0:
                result += '\nKeyword args:\n'
                for name in intro.doc_optional_input:
                    result += argstr(name)

            if len(intro.doc_optional_output) > 0:
                result += '\nOther Parameters:\n'
                for name in intro.doc_optional_output:
                    result += argstr(name)

            result += '\nRaises:\n    :class:`.Error`\n'

            # add to cache to save building again
            Operation._docstring_cache[operation_name] = result

            return result

        @staticmethod
        def generate_sphinx(operation_name):
            """Make a sphinx-style docstring.

            This is used to generate the off-line docs.

            """

            intro = Introspect.get(operation_name)
            if (intro.flags & _OPERATION_DEPRECATED) != 0:
                raise Error('No such operator.',
                            f'operator "{operation_name}" is deprecated')

            # drop "revalidate" flag from buffer loaders and operations marked
            # "nocache" as they are already uncached
            doc_optional_input = [name for name in intro.doc_optional_input if
                                  name != 'revalidate' or
                                  ('_buffer' not in operation_name and
                                   (intro.flags & _OPERATION_NOCACHE) == 0)]

            if intro.member_x is not None:
                result = '.. method:: '
            else:
                result = '.. staticmethod:: '
            args = []
            args += intro.method_args
            args += [x + '=' + GValue.gtype_to_python(intro.details[x]['type'])
                     for x in doc_optional_input]
            args += [x + '=bool'
                     for x in intro.doc_optional_output]
            result += operation_name + '(' + ", ".join(args) + ')\n\n'

            result += intro.description[0].upper() + \
                intro.description[1:] + '.\n\n'

            result += 'Example:\n'
            result += '    '
            if len(intro.required_output) > 0:
                result += ', '.join(intro.required_output) + ' = '
            if intro.member_x is not None:
                result += intro.member_x + "." + operation_name + '('
            else:
                result += 'pyvips.Image.' + operation_name + '('
            args = []
            args += intro.method_args
            args += [x + '=' + GValue.gtype_to_python(intro.details[x]['type'])
                     for x in doc_optional_input]
            result += ', '.join(args)
            result += ')\n\n'

            for name in intro.method_args + doc_optional_input:
                details = intro.details[name]
                result += f':param {name}: {details["blurb"]}\n'
                result += (f':type {name}: '
                           f'{GValue.gtype_to_python(details["type"])}\n')
            for name in intro.doc_optional_output:
                result += (f':param {name}: '
                           f'enable output: {intro.details[name]["blurb"]}\n')
                result += f':type {name}: bool\n'

            output_types = [GValue.gtype_to_python(intro.details[name]['type'])
                            for name in intro.required_output]
            if len(output_types) == 1:
                output_type = output_types[0]
            else:
                output_type = 'list[' + ', '.join(output_types) + ']'

            if len(intro.doc_optional_output) > 0:
                output_types += ['Dict[str, mixed]']
                output_type += ' or list[' + ', '.join(output_types) + ']'

            result += ':rtype: ' + output_type + '\n'
            result += ':raises Error:\n'

            return result

        @staticmethod
        def generate_sphinx_all():
            """Generate sphinx documentation.

            This generates a .rst file for all auto-generated image methods. Use it
            to regenerate the docs with something like::

                $ python -c \
    "import pyvips; pyvips.Operation.generate_sphinx_all()" > x

            And copy-paste the file contents into doc/vimage.rst in the appropriate
            place.

            """

            # these names are aliased
            alias = ["crop"]
            alias_gtypes = {}
            for name in alias:
                gtype = pyvips.type_find("VipsOperation", name)
                alias_gtypes[gtype] = name

            # all names we can generate docstrings for
            all_names = []

            def add_name(gtype, a, b):
                if gtype in alias_gtypes:
                    name = alias_gtypes[gtype]
                else:
                    name = nickname_find(gtype)

                try:
                    Operation.generate_sphinx(name)
                    all_names.append(name)
                except Error:
                    pass

                type_map(gtype, add_name)

                return ffi.NULL

            type_map(type_from_name('VipsOperation'), add_name)

            all_names.sort()

            # remove operations we have to wrap by hand
            exclude = ['scale', 'ifthenelse', 'bandjoin', 'bandrank']
            all_names = [x for x in all_names if x not in exclude]

            # Output summary table
            print('.. class:: Image')
            print('   :noindex:\n')
            print('   .. rubric:: Methods\n')
            print('   .. autosummary::')
            print('      :nosignatures:\n')
            for name in all_names:
                print(f'      ~{name}')
            print()

            # Output docs
            print()
            for name in all_names:
                docstr = Operation.generate_sphinx(name)
                docstr = docstr.replace('\n', '\n      ')
                print('   ' + docstr)

    def cache_set_max(mx):
        """Set the maximum number of operations libvips will cache."""
        vips_lib.vips_cache_set_max(mx)

    def cache_set_max_mem(mx):
        """Limit the operation cache by memory use."""
        vips_lib.vips_cache_set_max_mem(mx)

    def cache_set_max_files(mx):
        """Limit the operation cache by number of open files."""
        vips_lib.vips_cache_set_max_files(mx)

    def cache_set_trace(trace):
        """Turn on libvips cache tracing."""
        vips_lib.vips_cache_set_trace(trace)

    def cache_get_max():
        """Get the maximum number of operations libvips will cache."""
        return vips_lib.vips_cache_get_max()

    def cache_get_size():
        """Get the current size of the operations cache."""
        return vips_lib.vips_cache_get_size()

    def cache_get_max_mem():
        """Get the operation cache limit by memory use."""
        return vips_lib.vips_cache_get_max_mem()

    def cache_get_max_files():
        """Get the operation cache limit by number of open files."""
        return vips_lib.vips_cache_get_max_files()

    def block_untrusted_set(state):
        """Set the block state for all untrusted operations."""
        if at_least_libvips(8, 13):
            vips_lib.vips_block_untrusted_set(state)

    def operation_block_set(name, state):
        """Set the block state for a named operation."""
        if at_least_libvips(8, 13):
            vips_lib.vips_operation_block_set(_to_bytes(name), state)

    __all__ = [
        'Introspect',
        'Operation',
        'cache_set_max',
        'cache_set_max_mem',
        'cache_set_max_files',
        'cache_set_trace',
        'cache_get_max',
        'cache_get_max_mem',
        'cache_get_max_files',
        'cache_get_size',
        'block_untrusted_set',
        'operation_block_set'
    ]