"""Placeholder objects replaced with real GIMP objects during batch processing.

The placeholder objects are defined in the `PLACEHOLDERS` dictionary.
"""

import inspect
from typing import Callable, Dict, Iterable, List, Optional, Union, Type

from gi.repository import GObject

from src import exceptions
from src import setting as setting_
from src import utils
from src import utils_pdb
from src.gui import placeholders as gui_placeholders


class Placeholder:
  
  def __init__(
        self,
        name: str,
        display_name: str,
        replacement_func: Callable,
        arguments: Optional[Iterable[Dict]] = None,
  ):
    self._name = name
    self._display_name = display_name
    self._replacement_func = replacement_func
    self._arguments = list(arguments) if arguments is not None else []

    self._settings = None

  @property
  def name(self) -> str:
    return self._name
  
  @property
  def display_name(self) -> str:
    return self._display_name

  @property
  def arguments(self) -> List[Dict]:
    return self._arguments

  def create_settings_from_arguments(self) -> setting_.Group:
    settings = setting_.Group(name='arguments')
    settings.add(self._arguments)

    return settings

  def replace_args(self, *args, **kwargs):
    return self._replacement_func(*args, **kwargs)


def get_current_image(_setting, batcher):
  return batcher.current_image


def get_current_layer(_setting, batcher):
  return batcher.current_layer


def get_current_layer_for_array(setting, batcher):
  return (get_current_layer(setting, batcher),)


def get_background_layer(_setting, batcher):
  return _get_adjacent_layer(batcher, 1, _('No layer below current layer (no background).'))


def get_background_layer_for_array(setting, batcher):
  return (get_background_layer(setting, batcher),)


def get_foreground_layer(_setting, batcher):
  return _get_adjacent_layer(batcher, -1, _('No layer above current layer (no foreground).'))


def get_foreground_layer_for_array(setting, batcher):
  return (get_foreground_layer(setting, batcher),)


def get_layer_at_position(_setting, batcher, position: int = 1):
  current_image_layers = batcher.current_image.get_layers()

  if not current_image_layers:
    raise exceptions.SkipCommand(_('Image has no layers.'))
  elif position > len(current_image_layers):
    raise exceptions.SkipCommand(_('There is no layer at position {}.').format(position))
  elif position < 1:
    position = 1

  return current_image_layers[position - 1]


def get_layer_at_position_for_array(setting, batcher, **kwargs):
  return (get_layer_at_position(setting, batcher, **kwargs),)


def get_none_object(_setting, _batcher):
  return None


def get_all_top_level_layers(_setting, batcher):
  return batcher.current_image.get_layers()


def get_value_for_unsupported_parameter(setting, _batcher):
  return getattr(setting, 'default_param_value', None)


def _get_adjacent_layer(batcher, adjacent_position_increment, skip_message):
  image = batcher.current_image
  layer = batcher.current_layer

  if layer.get_parent() is None:
    children = image.get_layers()
  else:
    children = layer.get_parent().get_children()

  adjacent_layer_position = image.get_item_position(layer) + adjacent_position_increment

  if adjacent_layer_position < 0 or adjacent_layer_position >= len(children):
    raise exceptions.SkipCommand(skip_message)
  else:
    adjacent_layer = children[adjacent_layer_position]

    # This is necessary for some commands relying on selected layers.
    image.set_selected_layers([adjacent_layer])

    return adjacent_layer


def _get_previous_enabled_actions(batcher, current_command):
  previous_enabled_actions = []

  for action in batcher.actions:
    if action == current_command:
      return previous_enabled_actions

    if action['enabled'].value:
      previous_enabled_actions.append(action)

  return previous_enabled_actions


_PLACEHOLDERS_LIST = [
  ('current_image', _('Current Image'), get_current_image),
  ('current_layer', _('Current Layer'), get_current_layer),
  ('current_layer_for_array', _('Current Layer'), get_current_layer_for_array),
  ('background_layer', _('Layer Below (Background)'), get_background_layer),
  ('background_layer_for_array', _('Layer Below (Background)'), get_background_layer_for_array),
  ('foreground_layer', _('Layer Above (Foreground)'), get_foreground_layer),
  ('foreground_layer_for_array', _('Layer Above (Foreground)'), get_foreground_layer_for_array),
  (
    'layer_at_position',
    _('Layer at Position'),
    get_layer_at_position,
    [
      {
        'type': 'int',
        'name': 'position',
        'default_value': 1,
        'min_value': 1,
      }
    ],
  ),
  (
    'layer_at_position_for_array',
    _('Layer at Position'),
    get_layer_at_position_for_array,
    [
      {
        'type': 'int',
        'name': 'position',
        'default_value': 1,
        'min_value': 1,
      }
    ],
  ),
  ('all_top_level_layers', _('All Layers'), get_all_top_level_layers),
  ('none', _('None'), get_none_object),
  ('unsupported_parameter', '', get_value_for_unsupported_parameter),
]


PLACEHOLDERS = {args[0]: Placeholder(*args) for args in _PLACEHOLDERS_LIST}
"""Mapping of a placeholder name to a placeholder object.

The following placeholder objects are defined:

* ``PLACEHOLDERS['current_image']``: The image currently being processed.

* ``PLACEHOLDERS['current_layer']``: The layer currently being processed in the
  current image. This placeholder is used for commands containing
  `Gimp.Layer`, `Gimp.Drawable` or `Gimp.Item` parameters.

* ``PLACEHOLDERS['current_layer_for_array']``: The layer currently being
  processed in the current image. This placeholder is used for commands
  containing the `Gimp.CoreObjectArray` parameter whose object type is
  `Gimp.Layer`, `Gimp.Drawable` or `Gimp.Item`.

* ``PLACEHOLDERS['background_layer']``: The layer positioned immediately after
  the currently processed layer.

* ``PLACEHOLDERS['background_layer_for_array']``: The layer positioned 
  immediately after the currently processed layer. This placeholder is used for 
  commands containing the `Gimp.CoreObjectArray` parameter whose object 
  type is `Gimp.Layer`, `Gimp.Drawable` or `Gimp.Item`.

* ``PLACEHOLDERS['foreground_layer']``: The layer positioned immediately before
  the currently processed layer.

* ``PLACEHOLDERS['foreground_layer_for_array']``: The layer positioned 
  immediately before the currently processed layer. This placeholder is used 
  for commands containing the `Gimp.CoreObjectArray` parameter whose 
  object type is `Gimp.Layer`, `Gimp.Drawable` or `Gimp.Item`.

* ``PLACEHOLDERS['layer_at_position']``: The layer at the specified position.

* ``PLACEHOLDERS['layer_at_position_for_array']``: The layer at the specified 
  position. This placeholder is used for commands containing the 
  `Gimp.CoreObjectArray` parameter whose object type is `Gimp.Layer`, 
  `Gimp.Drawable` or `Gimp.Item`.

* ``PLACEHOLDERS['all_top_level_layers']``: All layers in the currently 
  processed image. This placeholder is used for commands containing the 
  `Gimp.CoreObjectArray` parameter whose object type is `Gimp.Layer`, 
  `Gimp.Drawable` or `Gimp.Item`.

* ``PLACEHOLDERS['unsupported_parameter']``: Indicates that the parameter
  is not supported by this plug-in and cannot be modified.
"""


ATTRIBUTES = {
  'width': _('Width'),
  'height': _('Height'),
  'x_offset': _('X-offset'),
  'y_offset': _('Y-offset'),
}
"""Attributes that can be obtained from objects substituted for placeholders.

Not all attributes can be obtained from all objects. `PLACEHOLDER_ATTRIBUTE_MAP`
specifies the allowed objects per a group of attributes.
"""


ALL_IMAGE_PLACEHOLDERS = (
  'current_image',
)

ALL_LAYER_PLACEHOLDERS = (
  'current_layer',
  'background_layer',
  'foreground_layer',
  'layer_at_position',
)

ALL_LAYER_ARRAY_PLACEHOLDERS = (
  'current_layer_for_array',
  'background_layer_for_array',
  'foreground_layer_for_array',
  'layer_at_position_for_array',
  'all_top_level_layers',
)

PLACEHOLDER_ATTRIBUTE_MAP = {
  ALL_IMAGE_PLACEHOLDERS: ('width', 'height'),
  ALL_LAYER_PLACEHOLDERS: ('width', 'height', 'x_offset', 'y_offset'),
}
"""Mapping of placeholders to applicable attributes."""


class PlaceholderSetting(setting_.Setting):
   
  _ALLOWED_GUI_TYPES = [gui_placeholders.PlaceholdersComboBoxPresenter]

  _DEFAULT_PLACEHOLDERS = []

  def __init__(self, name, **kwargs):
    self._placeholder_names = list(self._DEFAULT_PLACEHOLDERS)

    self._process_kwargs_before_parent_init(kwargs)

    super().__init__(name, **kwargs)

  def _process_kwargs_before_parent_init(self, kwargs):
    parent_class_parameters = inspect.signature(super().__init__).parameters

    # This ensures that extra arguments not specified in
    # `setting.Setting.__init__` are not passed there.
    keys_to_delete = []
    for key, value in kwargs.items():
      if key not in parent_class_parameters:
        utils.create_read_only_property(self, key, value)
        keys_to_delete.append(key)

    for key in keys_to_delete:
      del kwargs[key]

  def _get_pdb_type(self, pdb_type):
    # This avoids errors when creating placeholder settings. Placeholders
    # cannot be registered to the PDB anyway.
    return None

  def get_placeholder_names(self) -> List[str]:
    """Returns a list of names of placeholders applicable for this setting
    class.
    """
    return list(self._placeholder_names)

  def get_placeholders(self) -> List[Placeholder]:
    """Returns a list of placeholder objects applicable for this setting class.
    """
    return [
      placeholder for placeholder_name, placeholder in PLACEHOLDERS.items()
      if placeholder_name in self._placeholder_names
    ]

  def _validate(self, value):
    if isinstance(value, dict):
      if 'name' not in value or value['name'] not in self._placeholder_names:
        return 'invalid placeholder', 'invalid_value'
    else:
      if value not in self._placeholder_names:
        return 'invalid placeholder', 'invalid_value'


class PlaceholderGimpObjectSetting(PlaceholderSetting):

  def __init__(self, name, none_ok: bool = False, **kwargs):
    self._none_ok = none_ok

    super().__init__(name, **kwargs)

    if none_ok:
      self._placeholder_names.append('none')


class PlaceholderImageOrLayerSetting(PlaceholderGimpObjectSetting):
  
  _DEFAULT_DEFAULT_VALUE = 'current_image'
  _DEFAULT_PLACEHOLDERS = (
    *ALL_IMAGE_PLACEHOLDERS,
    *ALL_LAYER_PLACEHOLDERS,
  )


class PlaceholderImageSetting(PlaceholderGimpObjectSetting):

  _DEFAULT_DEFAULT_VALUE = 'current_image'
  _DEFAULT_PLACEHOLDERS = ALL_IMAGE_PLACEHOLDERS


class PlaceholderDrawableSetting(PlaceholderGimpObjectSetting):
  
  _DEFAULT_DEFAULT_VALUE = 'current_layer'
  _DEFAULT_PLACEHOLDERS = ALL_LAYER_PLACEHOLDERS


class PlaceholderLayerSetting(PlaceholderGimpObjectSetting):
  
  _DEFAULT_DEFAULT_VALUE = 'current_layer'
  _DEFAULT_PLACEHOLDERS = ALL_LAYER_PLACEHOLDERS


class PlaceholderLayerWithoutCurrentLayerSetting(PlaceholderGimpObjectSetting):

  _DEFAULT_DEFAULT_VALUE = 'foreground_layer'
  _DEFAULT_PLACEHOLDERS = ('background_layer', 'foreground_layer', 'layer_at_position')


class PlaceholderItemSetting(PlaceholderGimpObjectSetting):
  
  _DEFAULT_DEFAULT_VALUE = 'current_layer'
  _DEFAULT_PLACEHOLDERS = ALL_LAYER_PLACEHOLDERS


if utils_pdb.get_gimp_version() >= (3, 1, 4):
  class PlaceholderRasterizableSetting(PlaceholderGimpObjectSetting):

    _DEFAULT_DEFAULT_VALUE = 'current_layer'
    _DEFAULT_PLACEHOLDERS = ALL_LAYER_PLACEHOLDERS


class PlaceholderArraySetting(PlaceholderSetting):

  def __init__(self, name, element_type, **kwargs):
    super().__init__(name, **kwargs)

    self._element_type = setting_.process_setting_type(element_type)

  @property
  def element_type(self) -> Type[setting_.Setting]:
    return self._element_type

  def to_dict(self):
    settings_dict = super().to_dict()

    settings_dict['element_type'] = setting_.SETTING_TYPES[self._element_type]

    return settings_dict


class PlaceholderDrawableArraySetting(PlaceholderArraySetting):

  _DEFAULT_DEFAULT_VALUE = 'current_layer_for_array'
  _DEFAULT_PLACEHOLDERS = ALL_LAYER_ARRAY_PLACEHOLDERS


class PlaceholderLayerArraySetting(PlaceholderArraySetting):

  _DEFAULT_DEFAULT_VALUE = 'current_layer_for_array'
  _DEFAULT_PLACEHOLDERS = ALL_LAYER_ARRAY_PLACEHOLDERS


class PlaceholderItemArraySetting(PlaceholderArraySetting):

  _DEFAULT_DEFAULT_VALUE = 'current_layer_for_array'
  _DEFAULT_PLACEHOLDERS = ALL_LAYER_ARRAY_PLACEHOLDERS


class PlaceholderUnsupportedParameterSetting(PlaceholderSetting):

  _DEFAULT_DEFAULT_VALUE = 'unsupported_parameter'
  _ALLOWED_GUI_TYPES = [gui_placeholders.UnsupportedParameterPresenter]
  _DEFAULT_PLACEHOLDERS = (
    'unsupported_parameter',
  )

  def __init__(self, name, default_param_value=None, **kwargs):
    self._default_param_value = default_param_value

    super().__init__(name, **kwargs)

  @property
  def default_param_value(self):
    return self._default_param_value


def get_replaced_value(setting: PlaceholderSetting, batcher: 'src.core.Batcher'):
  """Returns a valid value replacing the placeholder value.

  ``setting`` is the placeholder setting whose ``value`` property is replaced.

  ``batcher`` is a `core.Batcher` instance holding data that may be used
  depending on the subclass of ``setting``.

  `KeyError` is raised if the placeholder value is not one of the keys in
  `PLACEHOLDERS`.
  """
  try:
    if isinstance(setting.value, dict):
      placeholder = PLACEHOLDERS[setting.value['name']]
    else:
      placeholder = PLACEHOLDERS[setting.value]
  except KeyError:
    raise ValueError(f'invalid placeholder value "{setting.value}"')
  else:
    if isinstance(setting.value, dict):
      kwargs = {key: value for key, value in setting.value.items() if key != 'name'}
    else:
      kwargs = {}

    return placeholder.replace_args(setting, batcher, **kwargs)


def get_placeholder_type_name_from_pdb_type(
      pdb_type: Union[GObject.GType, Type[GObject.GObject]],
      pdb_param_info: Optional[GObject.ParamSpec] = None,
) -> Union[str, None]:
  """Returns the name of a `setting.Setting` subclass representing a
  placeholder from the given GIMP PDB parameter type.

  Args:
    pdb_type:
      A `GObject.GObject` subclass or a `GObject.GType` instance representing a
      GIMP PDB parameter.
    pdb_param_info:
      Object representing PDB parameter information, obtainable via
      `pypdb.PDBProcedure.arguments`. This is used to infer the element type
      for an object array argument (images, layers, etc.).

  Returns:
    String as a human-readable name of a `setting.Setting` subclass
    representing a placeholder if ``pdb_type`` matches an identifier, or
    ``None`` otherwise.
  """
  processed_pdb_type = pdb_type

  if hasattr(pdb_type, '__gtype__'):
    processed_pdb_type = pdb_type.__gtype__

  try:
    pdb_type_name = processed_pdb_type.name
  except AttributeError:
    return None

  if pdb_type_name == 'GimpCoreObjectArray' and pdb_param_info is not None:
    _array_type, setting_dict = (
      setting_.get_array_setting_type_from_gimp_core_object_array(pdb_param_info))
    key = (pdb_type_name, setting_dict['element_type'])
  else:
    key = pdb_type_name

  try:
    placeholder_type_name = _PDB_TYPES_TO_PLACEHOLDER_TYPE_NAMES[key]
  except (KeyError, TypeError):
    return None
  else:
    return placeholder_type_name


_PDB_TYPES_TO_PLACEHOLDER_TYPE_NAMES = {
  'GimpImage': setting_.SETTING_TYPES[PlaceholderImageSetting],
  'GimpItem': setting_.SETTING_TYPES[PlaceholderItemSetting],
  'GimpDrawable': setting_.SETTING_TYPES[PlaceholderDrawableSetting],
  'GimpLayer': setting_.SETTING_TYPES[PlaceholderLayerSetting],
  'GimpTextLayer': setting_.SETTING_TYPES[PlaceholderLayerSetting],
  ('GimpCoreObjectArray', setting_.LayerSetting): (
    setting_.SETTING_TYPES[PlaceholderLayerArraySetting]),
  ('GimpCoreObjectArray', setting_.DrawableSetting): (
    setting_.SETTING_TYPES[PlaceholderDrawableArraySetting]),
  ('GimpCoreObjectArray', setting_.ItemSetting): (
    setting_.SETTING_TYPES[PlaceholderItemArraySetting]),
}

if utils_pdb.get_gimp_version() >= (3, 1, 4):
  # noinspection PyUnboundLocalVariable
  _PDB_TYPES_TO_PLACEHOLDER_TYPE_NAMES.update({
    'GimpRasterizable': setting_.SETTING_TYPES[PlaceholderRasterizableSetting],
    'GimpVectorLayer': setting_.SETTING_TYPES[PlaceholderLayerSetting],
    'GimpLinkLayer': setting_.SETTING_TYPES[PlaceholderLayerSetting],
  })
