"""Wrapper of ``Gimp.get_pdb()`` to simplify invoking GIMP PDB procedures."""

import abc
import contextlib
import keyword
from typing import Optional

import gi
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
from gi.repository import GObject


try:
  # noinspection PyUnboundLocalVariable
  _
except Exception:
  def _(string):
    return string


__all__ = [
  'pdb',
  'GeglProcedure',
  'GimpPDBProcedure',
  'PDBProcedure',
  'PDBProcedureError',
]


class _PyPDB:

  def __init__(self):
    self._last_status = None
    self._last_error = None

    self._gegl_operations = None
    self._gegl_operations_set = None

    self._proc_cache = {}

  @property
  def last_status(self):
    """Exit status of the last `GimpPDBProcedure` invoked by this class."""
    return self._last_status

  @property
  def last_error(self):
    """Error message of the last `GimpPDBProcedure` invoked by this class."""
    return self._last_error

  def __getattr__(self, name: str) -> 'PDBProcedure':
    canonical_name = self.python_name_to_canonical_name(name)

    if canonical_name in self._proc_cache:
      return self._proc_cache[canonical_name]
    elif name in self._proc_cache:
      return self._proc_cache[name]
    else:
      proc, proc_name = self._create_proc(canonical_name, name)
      self._proc_cache[proc_name] = proc

      return self._proc_cache[proc_name]

  def __getitem__(self, name: str) -> 'PDBProcedure':
    canonical_name = self.python_name_to_canonical_name(name)

    if canonical_name in self._proc_cache:
      return self._proc_cache[canonical_name]
    elif name in self._proc_cache:
      return self._proc_cache[name]
    else:
      proc, proc_name = self._create_proc(canonical_name, name)
      self._proc_cache[proc_name] = proc

      return self._proc_cache[proc_name]

  def __contains__(self, name: Optional[str]) -> bool:
    if name is None:
      return False

    canonical_name = self.python_name_to_canonical_name(name)

    if canonical_name not in self._proc_cache or name not in self._proc_cache:
      try:
        proc, proc_name = self._create_proc(canonical_name, name)
      except AttributeError:
        return False

      self._proc_cache[proc_name] = proc

    return True

  def list_all_gegl_operations(self):
    """Lists all available GEGL operations (filters, layer effects)."""
    self._fill_gegl_operations_and_set_if_empty()

    return self._gegl_operations

  @staticmethod
  def get_duplicate_gegl_operations():
    """Returns all ``'gegl:*'`` operations that have a ``'gimp:*'`` counterpart
    and are thus redundant.
    """
    if (Gimp.MAJOR_VERSION, Gimp.MINOR_VERSION, Gimp.MICRO_VERSION) >= (3, 1, 4):
      return _DUPLICATE_GEGL_OPERATIONS_POST_3_1_4
    else:
      return _DUPLICATE_GEGL_OPERATIONS_PRE_3_1_4

  def _fill_gegl_operations_and_set_if_empty(self):
    if self._gegl_operations is None:
      if (Gimp.MAJOR_VERSION, Gimp.MINOR_VERSION, Gimp.MICRO_VERSION) >= (3, 1, 4):
        self._gegl_operations = self._list_all_gegl_operations_post_3_1_4()
      else:
        self._gegl_operations = self._list_all_gegl_operations_pre_3_1_4()

      self._gegl_operations_set = set(self._gegl_operations)

  @staticmethod
  def _list_all_gegl_operations_post_3_1_4():
    return Gimp.DrawableFilter.operation_get_available()

  @staticmethod
  def _list_all_gegl_operations_pre_3_1_4():
    operation_names = Gegl.list_operations() + list(_GIMP_GEGL_OPERATIONS_PRE_3_1_4)

    processed_operation_names = []

    for name in operation_names:
      categories_str = Gegl.Operation.get_key(name, 'categories')

      if categories_str is not None:
        categories = categories_str.split(':')
        if any(category in _UNUSED_GEGL_OPERATION_CATEGORIES for category in categories):
          continue

      if name in _UNUSED_GEGL_OPERATIONS_PRE_3_1_4:
        continue

      processed_operation_names.append(name)

    return processed_operation_names

  @staticmethod
  def list_all_gimp_pdb_procedures():
    """Lists all available GIMP PDB procedures."""
    return Gimp.get_pdb().query_procedures(*([''] * 8))

  def list_all_procedure_names(self):
    """Lists all available procedures and filters (GEGL operations, layer
    effects).
    """
    return self.list_all_gegl_operations() + self.list_all_gimp_pdb_procedures()

  def remove_from_cache(self, name: str):
    """Removes a `PDBProcedure` instance matching ``name`` from the internal
    cache.

    This method is only ever useful for testing purposes.

    No action is taken if there is no procedure matching ``name`` in the cache.
    """
    proc_name = self.python_name_to_canonical_name(name)

    try:
      del self._proc_cache[proc_name]
    except KeyError:
      pass

  def _create_proc(self, canonical_name, orig_name):
    if self._gimp_pdb_procedure_exists(canonical_name):
      return GimpPDBProcedure(self, canonical_name), canonical_name
    elif self._gegl_operation_exists(orig_name):
      return GeglProcedure(self, orig_name), orig_name
    elif self._gegl_operation_exists(canonical_name):
      return GeglProcedure(self, canonical_name), canonical_name
    else:
      raise AttributeError(f'procedure "{orig_name}" does not exist')

  @staticmethod
  def python_name_to_canonical_name(name):
    return name.replace('__', ':').replace('_', '-')

  @staticmethod
  def canonical_name_to_python_name(name):
    return name.replace('-', '_').replace(':', '__')

  @staticmethod
  def _gimp_pdb_procedure_exists(proc_name):
    return Gimp.is_canonical_identifier(proc_name) and Gimp.get_pdb().procedure_exists(proc_name)

  def _gegl_operation_exists(self, proc_name):
    self._fill_gegl_operations_and_set_if_empty()

    return proc_name in self._gegl_operations_set


class PDBProcedure(metaclass=abc.ABCMeta):

  def __init__(self, pypdb_instance, name):
    self._pypdb_instance = pypdb_instance
    self._name = name

  @abc.abstractmethod
  def __call__(self, **kwargs):
    """Calls the procedure.

    All procedure arguments must be specified as keyword arguments (unless
    defined otherwise in subclasses).

    If any keyword argument is omitted, the default value is used for that
    argument as defined in the procedure.

    All underscore characters  (``_``) in argument names are automatically
    replaced by ``-``.

    Arguments whose names match a Python keyword (``if``, ``lambda``,
    etc.) can be specified by appending a ``_``, e.g. ``lambda_=<value>``.
    """
    pass

  @staticmethod
  def _process_arg_name(arg_name):
    """Transforms the given Python argument name to the name of a property
    defined for this procedure.

    For example, ``'run_mode'`` is transformed to ``'run-mode'``.

    Argument names matching a Python keyword can be specified with a trailing
    ``_``, e.g. ``lambda_``. This is transformed such that the trailing `_``
    is removed, e.g. ``lambda``.
    """
    processed_arg_name = arg_name.replace('_', '-')

    # This allows passing arguments with a trailing '_' to avoid name clashes
    # with Python keywords.
    if processed_arg_name.endswith('-') and keyword.iskeyword(processed_arg_name[:-1]):
      processed_arg_name = processed_arg_name[:-1]

    return processed_arg_name

  @property
  @abc.abstractmethod
  def arguments(self):
    pass

  @property
  @abc.abstractmethod
  def raw_arguments(self):
    """List of arguments as returned via the GIMP API, without additional
    potential arguments added by this class.
    """
    pass

  @property
  @abc.abstractmethod
  def aux_arguments(self):
    pass

  @property
  @abc.abstractmethod
  def return_values(self):
    pass

  @property
  @abc.abstractmethod
  def authors(self):
    pass

  @property
  @abc.abstractmethod
  def blurb(self):
    pass

  @property
  @abc.abstractmethod
  def copyright(self):
    pass

  @property
  @abc.abstractmethod
  def date(self):
    pass

  @property
  @abc.abstractmethod
  def help(self):
    pass

  @property
  @abc.abstractmethod
  def menu_label(self):
    pass

  @property
  @abc.abstractmethod
  def menu_paths(self):
    pass

  @property
  def name(self):
    """The procedure name.

    The name can be used to access a `PDBProcedure` instance as `pdb[<name>]`.
    """
    return self._name

  @property
  def must_be_merged(self):
    """Returns ``True`` if this procedure must always be applied destructively,
    ``False`` otherwise.
    """
    return False

  @abc.abstractmethod
  def create_config(self):
    """Creates a procedure config filled with default values."""
    pass


class GimpPDBProcedure(PDBProcedure):

  def __init__(self, pypdb_instance, name):
    self._proc = Gimp.get_pdb().lookup_procedure(name)

    super().__init__(pypdb_instance, name)

  def __call__(self, *args, **kwargs):
    """Calls a GIMP PDB procedure.

    All arguments must be specified as keyword arguments.

    Return values from the procedure are returned as a tuple of values. If the
    procedure does not define any return value, ``None`` is returned.
    """
    if args:
      raise TypeError('positional arguments are not allowed; specify keyword arguments instead')

    config = self._create_config_for_call(**kwargs)

    result = self._proc.run(config)

    if result is None:
      return None

    result_list = [result.index(i) for i in range(result.length())]

    last_status = None
    last_error = None

    if len(result_list) > 0:
      if isinstance(result_list[0], Gimp.PDBStatusType):
        last_status = result_list.pop(0)

    if len(result_list) > 0:
      if last_status not in [Gimp.PDBStatusType.SUCCESS, Gimp.PDBStatusType.PASS_THROUGH]:
        last_error = result_list.pop(0)

    self._pypdb_instance._last_status = last_status
    self._pypdb_instance._last_error = last_error

    if (last_status is not None
        and last_status not in [Gimp.PDBStatusType.SUCCESS, Gimp.PDBStatusType.PASS_THROUGH]):
      raise PDBProcedureError(last_error, last_status)

    if result_list:
      if len(result_list) == 1:
        return result_list[0]
      else:
        return result_list
    else:
      return None

  @property
  def proc(self):
    """`Gimp.Procedure` instance wrapped by this class."""
    return self._proc

  @property
  def arguments(self):
    return self._proc.get_arguments()

  @property
  def raw_arguments(self):
    return self._proc.get_arguments()

  @property
  def aux_arguments(self):
    return self._proc.get_aux_arguments()

  @property
  def return_values(self):
    return self._proc.get_return_values()

  @property
  def authors(self):
    return self._proc.get_authors()

  @property
  def blurb(self):
    return self._proc.get_blurb()

  @property
  def copyright(self):
    return self._proc.get_copyright()

  @property
  def date(self):
    return self._proc.get_date()

  @property
  def help(self):
    return self._proc.get_help()

  @property
  def menu_label(self):
    return self._proc.get_menu_label()

  @property
  def menu_paths(self):
    return self._proc.get_menu_paths()

  def create_config(self):
    return self._proc.create_config()

  def _create_config_for_call(self, **proc_kwargs):
    config = self.create_config()

    args = self.arguments

    args_and_names = {arg.name: arg for arg in args}

    for arg_name, arg_value in proc_kwargs.items():
      processed_arg_name = self._process_arg_name(arg_name)

      try:
        arg = args_and_names[processed_arg_name]
      except KeyError:
        raise PDBProcedureError(
          f'argument "{processed_arg_name}" does not exist',
          Gimp.PDBStatusType.CALLING_ERROR)

      arg_type_name = arg.value_type.name
      config_set_property = _get_set_property_func_for_gimp_pdb_procedure(arg_type_name, config)
      config_set_property(processed_arg_name, arg_value)

    return config


class GeglProcedure(PDBProcedure):

  _GIMP_GEGL_OPERATIONS_PROPERTIES = None
  _GIMP_GEGL_OPERATIONS_DETAILS = None

  def __init__(self, pypdb_instance, name):
    self._filter_properties = self._get_filter_properties(name)
    self._must_be_merged = self._get_must_be_merged(name)

    self._drawable_param = Gimp.param_spec_drawable(
      'drawable-', _('Drawable'), _('Drawable'), False, GObject.ParamFlags.READWRITE)
    self._blend_mode_param = GObject.param_spec_enum(
      'blend-mode-',
      _('Blend mode'),
      _('Blend mode'),
      Gimp.LayerMode.__gtype__,
      Gimp.LayerMode.REPLACE,
      GObject.ParamFlags.READWRITE,
    )
    self._opacity_param = GObject.param_spec_double(
      'opacity-', _('Opacity'), _('Opacity'), 0.0, 1.0, 1.0, GObject.ParamFlags.READWRITE)
    self._merge_filter_param = GObject.param_spec_boolean(
      'merge-filter-', _('Merge filter'), _('Merge filter'), False, GObject.ParamFlags.READWRITE)
    self._visible_param = GObject.param_spec_boolean(
      'visible-', _('Visible'), _('Visible'), True, GObject.ParamFlags.READWRITE)
    self._filter_name_param = GObject.param_spec_string(
      'name-', _('Filter name'), _('Filter name'), '', GObject.ParamFlags.READWRITE)

    self._details = self._get_details(name)
    self._properties = {prop.name: prop for prop in self._get_properties()}

    super().__init__(pypdb_instance, name)

  def __call__(self, *args, **kwargs):
    """Applies a layer effect (drawable filter, GEGL operation) on the specified
    drawable.

    Beside arguments defined by the GEGL operation, you may specify the
    following additional keyword arguments, all of which have a trailing ``_``:

    * ``drawable_`` - The ``Gimp.Drawable`` instance to apply the layer effect
      to. This argument may be specified as the first and the only positional
      argument.
    *  ``merge_filter_`` - If ``False`` (default), the layer effect is applied
      non-destructively and the filter is returned. If ``True``, the layer
      effect is immediately merged into the drawable and ``None`` is returned.
    * ``blend_mode_`` - The `Gimp.LayerMode` to apply to the layer effect.
    * ``opacity_`` - The opacity of the layer effect.
    * ``visible_`` - If ``True`` (default), the layer effect is visible. If
      ``False``, the layer effect is not visible.
    * ``name_`` - A custom name for the layer effect. If an empty string, the
      default name of the layer effect is assigned.

    All arguments must be specified as keyword arguments, except the
    ``drawable_`` argument, which may be specified as the first and the only
    positional argument.
    """
    processed_kwargs = {self._process_arg_name(name): value for name, value in kwargs.items()}

    if 'drawable-' in processed_kwargs:
      drawable = processed_kwargs.pop('drawable-')
    else:
      if not args or not isinstance(args[0], Gimp.Drawable):
        raise PDBProcedureError(
          ('if the "drawable_" parameter is not specified as a keyword argument, it must be'
           ' specified as the first and only positional argument'),
          Gimp.PDBStatusType.CALLING_ERROR)

      drawable = args[0]

    blend_mode = processed_kwargs.pop('blend-mode-', self._blend_mode_param.default_value)
    opacity = processed_kwargs.pop('opacity-', self._opacity_param.default_value)
    merge_filter = processed_kwargs.pop('merge-filter-', self._merge_filter_param.default_value)
    visible = processed_kwargs.pop('visible-', self._visible_param.default_value)
    name = processed_kwargs.pop('name-', self._filter_name_param.default_value)

    drawable_filter = Gimp.DrawableFilter.new(drawable, self.name, name)
    drawable_filter.set_blend_mode(blend_mode)
    drawable_filter.set_opacity(opacity)
    drawable_filter.set_visible(visible)

    config = drawable_filter.get_config()

    properties_from_config = {prop.name: prop for prop in config.list_properties()}

    for arg_name, arg_value in processed_kwargs.items():
      if arg_name not in self._properties:
        raise PDBProcedureError(
          f'argument "{arg_name}" does not exist or is not supported',
          Gimp.PDBStatusType.CALLING_ERROR)

      # Silently skip properties not supported in GIMP as the procedure
      # may still finish successfully.
      if arg_name not in properties_from_config:
        continue

      # While GIMP internally transforms GEGL enum values to `Gimp.Choice`
      # values (https://gitlab.gnome.org/GNOME/gimp/-/merge_requests/2008),
      # this is not the case for GIMP < 3.1.4, or when
      # `Gimp.DrawableFilter.operation_get_pspecs()` returns ``None`` for some
      # parameters. We therefore explicitly convert enums to choices in that
      # case.
      if (properties_from_config[arg_name].__gtype__ == Gimp.ParamChoice.__gtype__
          and isinstance(arg_value, GObject.GEnum)):
        processed_value = arg_value.value_nick
      else:
        processed_value = arg_value

      config.set_property(arg_name, processed_value)

    drawable_filter.update()

    if merge_filter:
      drawable.merge_filter(drawable_filter)

      return None
    else:
      drawable.append_filter(drawable_filter)

      return drawable_filter

  @property
  def arguments(self):
    return list(self._properties.values())

  @property
  def raw_arguments(self):
    return list(self._filter_properties)

  @property
  def aux_arguments(self):
    return []

  @property
  def return_values(self):
    return []

  @property
  def authors(self):
    return ''

  @property
  def blurb(self):
    return self._details['description']

  @property
  def copyright(self):
    return ''

  @property
  def date(self):
    return ''

  @property
  def help(self):
    return ''

  @property
  def menu_label(self):
    return self._details['title']

  @property
  def menu_paths(self):
    return []

  @property
  def must_be_merged(self):
    return self._must_be_merged

  def create_config(self):
    """Creates a procedure config filled with default values.

    Note that this method is quite slow as it requires creating a temporary
    filter, which in turn requires creating a temporary empty layer and image.
    """
    with self._create_temp_layer() as temp_layer:
      drawable_filter = Gimp.DrawableFilter.new(temp_layer, self.name, '')
      return drawable_filter.get_config()

  def _get_filter_properties(self, name):
    if (Gimp.MAJOR_VERSION, Gimp.MINOR_VERSION, Gimp.MICRO_VERSION) >= (3, 1, 4):
      properties_array = Gimp.DrawableFilter.operation_get_pspecs(name)

      properties = [properties_array.index(index) for index in range(properties_array.length())]

      # Use `Gegl.Operation.list_properties()` as a fallback in case
      # `Gimp.DrawableFilter.operation_get_pspecs()` returns ``None`` for
      # some parameters for some reason.
      indexes_of_none_properties = [index for index, prop in enumerate(properties) if prop is None]
      if indexes_of_none_properties:
        properties_from_gegl = Gegl.Operation.list_properties(name)
        if properties_from_gegl:
          for index in indexes_of_none_properties:
            properties[index] = properties_from_gegl[index]

      return properties
    else:
      if name in _GIMP_GEGL_OPERATIONS_SET_PRE_3_1_4:
        self._fill_gimp_gegl_operations_attributes_pre_3_1_4()

        return self._GIMP_GEGL_OPERATIONS_PROPERTIES[name]
      else:
        return Gegl.Operation.list_properties(name)

  @staticmethod
  def _get_must_be_merged(name):
    if name in _GIMP_GEGL_OPERATIONS_THAT_MUST_BE_MERGED:
      return True

    node = Gegl.Node()
    node.set_property('operation', name)

    return node.has_pad('aux')

  def _get_details(self, name):
    if (Gimp.MAJOR_VERSION, Gimp.MINOR_VERSION, Gimp.MICRO_VERSION) >= (3, 1, 4):
      return self._get_details_post_3_1_4(name)
    else:
      if name in _GIMP_GEGL_OPERATIONS_SET_PRE_3_1_4:
        self._fill_gimp_gegl_operations_attributes_pre_3_1_4()

        return self._GIMP_GEGL_OPERATIONS_DETAILS[name]
      else:
        return self._get_details_pre_3_1_4(name)

  @staticmethod
  def _get_details_post_3_1_4(name):
    success, names, values = Gimp.DrawableFilter.operation_get_details(name)

    if success:
      title = values.index(names.index('title'))
      description = values.index(names.index('description'))

      return {
        'title': title if title is not None else '',
        'description': description if description is not None else '',
      }
    else:
      return {
        'title': '',
        'description': '',
      }

  @staticmethod
  def _get_details_pre_3_1_4(name):
    keys = set(Gegl.Operation.list_keys(name))

    return {
      'title': Gegl.Operation.get_key(name, 'title') if 'title' in keys else '',
      'description': Gegl.Operation.get_key(name, 'description') if 'description' in keys else '',
    }

  @classmethod
  def _fill_gimp_gegl_operations_attributes_pre_3_1_4(cls):
    if cls._GIMP_GEGL_OPERATIONS_PROPERTIES is None:
      cls._GIMP_GEGL_OPERATIONS_PROPERTIES = {}
      cls._GIMP_GEGL_OPERATIONS_DETAILS = {}

      # HACK: The only reliable way to obtain information about `gimp:*`
      # filter parameters seems to be to create `Gimp.DrawableFilter`
      # instances for each filter, for which we need to create an image with
      # an empty layer.
      with cls._create_temp_layer() as temp_layer:
        for operation_name in _GIMP_GEGL_OPERATIONS_PRE_3_1_4:
          drawable_filter = Gimp.DrawableFilter.new(temp_layer, operation_name, '')

          config = drawable_filter.get_config()
          cls._GIMP_GEGL_OPERATIONS_PROPERTIES[operation_name] = config.list_properties()
          cls._GIMP_GEGL_OPERATIONS_DETAILS[operation_name] = {
            'title': drawable_filter.get_name(),
            'description': '',
          }

          drawable_filter.delete()

  @classmethod
  @contextlib.contextmanager
  def _create_temp_layer(cls):
    temp_image = None

    try:
      temp_image = Gimp.Image.new(2, 2, Gimp.ImageBaseType.RGB)

      empty_layer = Gimp.Layer.new(
        temp_image,
        'layer',
        temp_image.get_width(),
        temp_image.get_height(),
        Gimp.ImageType.RGBA_IMAGE,
        100.0,
        Gimp.LayerMode.NORMAL,
      )
      temp_image.insert_layer(empty_layer, None, 0)

      yield empty_layer
    finally:
      if temp_image is not None and temp_image.is_valid():
        temp_image.delete()

  def _get_properties(self):
    return [
      self._drawable_param,
      *self._filter_properties,
      self._blend_mode_param,
      self._opacity_param,
      self._merge_filter_param,
      self._visible_param,
      self._filter_name_param,
    ]


def _get_set_property_func_for_gimp_pdb_procedure(arg_type_name, config):
  if arg_type_name == 'GimpCoreObjectArray':
    return config.set_core_object_array
  elif arg_type_name == 'GimpColorArray':
    return config.set_color_array
  else:
    return config.set_property


class PDBProcedureError(Exception):

  def __init__(self, message, status):
    super().__init__(message)

    self.message = message
    self.status = status

  def __str__(self):
    return str(self.message)


# Taken from:
# https://gitlab.gnome.org/GNOME/gimp/-/blob/GIMP_3_2_0_RC2/app/gegl/gimp-gegl-utils.c
_UNUSED_GEGL_OPERATION_CATEGORIES = frozenset([
  'compositors',
  'core',
  'debug',
  'display',
  'hidden',
  'input',
  'output',
  'programming',
  'transform',
  'video',
])


# Taken from:
# https://gitlab.gnome.org/GNOME/gimp/-/blob/GIMP_3_2_0_RC2/app/gegl/gimp-gegl-utils.c
_UNUSED_GEGL_OPERATIONS_PRE_3_1_4 = frozenset([
  'gegl:color',
  'gegl:contrast-curve',
  'gegl:convert-format',
  'gegl:ditto',
  'gegl:fill-path',
  'gegl:hstack',
  'gegl:introspect',
  'gegl:json:dropshadow2',
  'gegl:json:grey2',
  'gegl:layer',
  'gegl:lcms-from-profile',
  'gegl:linear-gradient',
  'gegl:map-absolute',
  'gegl:map-relative',
  'gegl:matting-global',
  'gegl:matting-levin',
  'gegl:opacity',
  'gegl:pack',
  'gegl:path',
  'gegl:radial-gradient',
  'gegl:rectangle',
  'gegl:seamless-clone',
  'gegl:text',
  'gegl:tile',
  'gegl:unpremul',
  'gegl:vector-stroke',
])


# The 'gimp:*' counterparts of these filters can achieve the same effect. They
# are kept to maintain backwards compatibility with GIMP < 3.1.4 or because
# they are not blocklisted by GIMP.
_DUPLICATE_GEGL_OPERATIONS_PRE_3_1_4 = (
  'gegl:gray',
  'gegl:posterize',
  'gegl:threshold',
  'gegl:wavelet-blur',
)


# The 'gimp:*' counterparts of these filters can achieve the same effect. They
# are kept to maintain backwards compatibility with GIMP < 3.1.4 or because
# they are not blocklisted by GIMP.
_DUPLICATE_GEGL_OPERATIONS_POST_3_1_4 = (
  'gegl:brightness-contrast',
  'gegl:levels',
)


# This is not an exhaustive list as many 'gimp:*' operations cause GIMP to
# crash on some platforms, have unsupported parameter types or otherwise have
# no effect.
_GIMP_GEGL_OPERATIONS_PRE_3_1_4 = (
  'gimp:border',
  'gimp:colorize',
  'gimp:compose-crop',
  'gimp:desaturate',
  'gimp:flood',
  'gimp:grow',
  'gimp:posterize',
  'gimp:scalar-multiply',
  'gimp:semi-flatten',
  'gimp:set-alpha',
  'gimp:shrink',
  'gimp:threshold',
  'gimp:threshold-alpha',
)


_GIMP_GEGL_OPERATIONS_SET_PRE_3_1_4 = set(_GIMP_GEGL_OPERATIONS_PRE_3_1_4)


# Checking for the presence of the 'aux' pad in `gimp:*` operations does not
# appear to work and yields console warnings. We therefore list these
# explicitly.
_GIMP_GEGL_OPERATIONS_THAT_MUST_BE_MERGED = (
  'gimp:addition-legacy',
  'gimp:anti-erase',
  'gimp:behind',
  'gimp:burn-legacy',
  'gimp:compose-crop',
  'gimp:darken-only-legacy',
  'gimp:difference-legacy',
  'gimp:divide-legacy',
  'gimp:dodge-legacy',
  'gimp:erase',
  'gimp:grain-extract-legacy',
  'gimp:grain-merge-legacy',
  'gimp:hardlight-legacy',
  'gimp:hsl-color-legacy',
  'gimp:hsv-hue-legacy',
  'gimp:hsv-saturation-legacy',
  'gimp:hsv-value-legacy',
  'gimp:layer-mode',
  'gimp:lighten-only-legacy',
  'gimp:mask-components',
  'gimp:merge',
  'gimp:multiply-legacy',
  'gimp:normal',
  'gimp:overwrite',
  'gimp:pass-through',
  'gimp:replace',
  'gimp:screen-legacy',
  'gimp:set-alpha',
  'gimp:softlight-legacy',
  'gimp:split',
  'gimp:subtract-legacy',
)


pdb = _PyPDB()
