"""Built-in "Scale" action."""

import gi

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

from src import builtin_commands_common
from src import constants
from src import placeholders as placeholders_
from src import setting_additional as setting_additional_
from src import utils
from src.procedure_groups import *

from . import _utils as builtin_actions_utils


__all__ = [
  'ScaleModes',
  'ScaleConditions',
  'scale',
]


class ScaleModes:
  SCALE_MODES = (
    STRETCH,
    KEEP_ADJUST_WIDTH,
    KEEP_ADJUST_HEIGHT,
    FIT,
    FIT_WITH_PADDING,
  ) = (
    'stretch',
    'keep_adjust_width',
    'keep_adjust_height',
    'fit',
    'fit_with_padding',
  )


class ScaleConditions:
  SCALE_CONDITIONS = (
    ALWAYS,
    SMALLER,
    LARGER,
    SMALLER_THAN_CUSTOM,
    LARGER_THAN_CUSTOM,
  ) = (
    'always',
    'smaller',
    'larger',
    'smaller_than_custom',
    'larger_than_custom',
  )


def scale(
      batcher,
      object_to_scale,
      scale_mode,
      new_width,
      new_height,
      interpolation,
      local_origin,
      set_image_resolution,
      image_resolution,
      padding_color,
      padding_position,
      padding_position_custom,
      scale_condition,
      scale_condition_width,
      scale_condition_height,
):
  orig_width_pixels = object_to_scale.get_width()
  if orig_width_pixels == 0:
    orig_width_pixels = 1

  orig_height_pixels = object_to_scale.get_height()
  if orig_height_pixels == 0:
    orig_height_pixels = 1

  if set_image_resolution:
    processed_resolution_x = image_resolution['x'] if image_resolution['x'] > 0 else 1.0
    processed_resolution_y = image_resolution['y'] if image_resolution['y'] > 0 else 1.0

    if isinstance(object_to_scale, Gimp.Image):
      object_to_scale.set_resolution(processed_resolution_x, processed_resolution_y)
    elif isinstance(object_to_scale, Gimp.Item):
      object_to_scale.get_image().set_resolution(processed_resolution_x, processed_resolution_y)

  if new_width['unit'] == '%':
    percent_object_for_width = object_to_scale
  else:
    percent_object_for_width = None

  new_width_pixels = builtin_actions_utils.unit_to_pixels(
    batcher, new_width, 'x', percent_object_for_width, 'width')
  if new_width_pixels <= 0:
    new_width_pixels = 1

  if new_height['unit'] == '%':
    percent_object_for_height = object_to_scale
  else:
    percent_object_for_height = None

  new_height_pixels = builtin_actions_utils.unit_to_pixels(
    batcher, new_height, 'y', percent_object_for_height, 'height')
  if new_height_pixels <= 0:
    new_height_pixels = 1

  can_scale = True

  if scale_condition == ScaleConditions.SMALLER:
    can_scale = orig_width_pixels <= new_width_pixels and orig_height_pixels <= new_height_pixels
  elif scale_condition == ScaleConditions.LARGER:
    can_scale = orig_width_pixels >= new_width_pixels and orig_height_pixels >= new_height_pixels
  elif scale_condition == ScaleConditions.SMALLER_THAN_CUSTOM:
    can_scale = _can_scale(
      batcher,
      orig_width_pixels,
      orig_height_pixels,
      scale_condition_width,
      scale_condition_height,
      lambda orig_width_pixels_, width_pixels_, orig_height_pixels_, height_pixels_: (
        orig_width_pixels_ <= width_pixels_ and orig_height_pixels_ <= height_pixels_)
    )
  elif scale_condition == ScaleConditions.LARGER_THAN_CUSTOM:
    can_scale = _can_scale(
      batcher,
      orig_width_pixels,
      orig_height_pixels,
      scale_condition_width,
      scale_condition_height,
      lambda orig_width_pixels_, width_pixels_, orig_height_pixels_, height_pixels_: (
        orig_width_pixels_ >= width_pixels_ and orig_height_pixels_ >= height_pixels_)
    )

  if not can_scale:
    return

  if scale_mode in [ScaleModes.KEEP_ADJUST_WIDTH, ScaleModes.KEEP_ADJUST_HEIGHT]:
    processed_width_pixels, processed_height_pixels = _get_scale_keep_aspect_ratio_values(
      scale_mode,
      orig_width_pixels,
      orig_height_pixels,
      new_width_pixels,
      new_height_pixels)
  elif scale_mode in [ScaleModes.FIT, ScaleModes.FIT_WITH_PADDING]:
    processed_width_pixels, processed_height_pixels = _get_scale_fit_values(
      orig_width_pixels, orig_height_pixels, new_width_pixels, new_height_pixels)
  else:
    processed_width_pixels = new_width_pixels
    processed_height_pixels = new_height_pixels

  Gimp.context_push()
  Gimp.context_set_interpolation(interpolation)

  if processed_width_pixels == 0:
    processed_width_pixels = 1

  if processed_height_pixels == 0:
    processed_height_pixels = 1

  if isinstance(object_to_scale, Gimp.Image):
    object_to_scale.scale(processed_width_pixels, processed_height_pixels)
  else:
    object_to_scale.scale(processed_width_pixels, processed_height_pixels, local_origin)

  if scale_mode == ScaleModes.FIT_WITH_PADDING:
    _fill_with_padding(
      batcher,
      object_to_scale,
      new_width_pixels,
      new_height_pixels,
      padding_color,
      padding_position,
      padding_position_custom,
    )

  Gimp.context_pop()


def _can_scale(
      batcher,
      orig_width_pixels,
      orig_height_pixels,
      width_dimension,
      height_dimension,
      predicate,
):
  width_pixels = builtin_actions_utils.unit_to_pixels(batcher, width_dimension, 'x')
  height_pixels = builtin_actions_utils.unit_to_pixels(batcher, height_dimension, 'y')

  return predicate(orig_width_pixels, width_pixels, orig_height_pixels, height_pixels)


def _get_scale_keep_aspect_ratio_values(
      scale_mode,
      orig_width_pixels,
      orig_height_pixels,
      new_width_pixels,
      new_height_pixels,
):
  if scale_mode == ScaleModes.KEEP_ADJUST_WIDTH:
    processed_new_width_pixels = new_width_pixels
    processed_new_height_pixels = round(
      orig_height_pixels * (processed_new_width_pixels / orig_width_pixels))
  elif scale_mode == ScaleModes.KEEP_ADJUST_HEIGHT:
    processed_new_height_pixels = new_height_pixels
    processed_new_width_pixels = round(
      orig_width_pixels * (processed_new_height_pixels / orig_height_pixels))
  else:
    raise ValueError(
      'invalid value for dimension_to_keep; must be "width" or "height"')

  return processed_new_width_pixels, processed_new_height_pixels


def _get_scale_fit_values(
      orig_width_pixels, orig_height_pixels, new_width_pixels, new_height_pixels):
  processed_new_width_pixels = new_width_pixels
  processed_new_height_pixels = round(orig_height_pixels * (new_width_pixels / orig_width_pixels))

  if processed_new_height_pixels > new_height_pixels:
    processed_new_height_pixels = new_height_pixels
    processed_new_width_pixels = round(orig_width_pixels * (new_height_pixels / orig_height_pixels))

  return processed_new_width_pixels, processed_new_height_pixels


def _fill_with_padding(
      batcher,
      gimp_object,
      new_width_pixels,
      new_height_pixels,
      padding_color,
      padding_position,
      padding_position_custom,
):
  object_width = gimp_object.get_width()
  object_height = gimp_object.get_height()

  if isinstance(gimp_object, Gimp.Image):
    drawable_with_padding = batcher.current_layer
    image_of_drawable_with_padding = gimp_object
  else:
    drawable_with_padding = gimp_object
    image_of_drawable_with_padding = gimp_object.get_image()

  if new_width_pixels > object_width:
    offset_x = 0
    if padding_position == builtin_actions_utils.Positions.START:
      offset_x = 0
    elif padding_position == builtin_actions_utils.Positions.CENTER:
      offset_x = round((new_width_pixels - object_width) / 2)
    elif padding_position == builtin_actions_utils.Positions.END:
      offset_x = new_width_pixels - object_width
    elif padding_position == builtin_actions_utils.Positions.CUSTOM:
      offset_x = builtin_actions_utils.unit_to_pixels(batcher, padding_position_custom, 'x')
      offset_x = min(offset_x, new_width_pixels - object_width)

    offset_y = 0
  else:
    offset_y = 0
    if padding_position == builtin_actions_utils.Positions.START:
      offset_y = 0
    elif padding_position == builtin_actions_utils.Positions.CENTER:
      offset_y = round((new_height_pixels - object_height) / 2)
    elif padding_position == builtin_actions_utils.Positions.END:
      offset_y = new_height_pixels - object_height
    elif padding_position == builtin_actions_utils.Positions.CUSTOM:
      offset_y = builtin_actions_utils.unit_to_pixels(batcher, padding_position_custom, 'y')
      offset_y = min(offset_y, new_height_pixels - object_height)

    offset_x = 0

  if isinstance(gimp_object, Gimp.Image):
    color_layer_offset_x = 0
    color_layer_offset_y = 0

    new_object_offset_x = offset_x
    new_object_offset_y = offset_y

    if isinstance(gimp_object, Gimp.TextLayer):
      gimp_object.resize(new_width_pixels, new_height_pixels)
    else:
      gimp_object.resize(new_width_pixels, new_height_pixels, offset_x, offset_y)
  else:
    drawable_with_padding_offsets = gimp_object.get_offsets()

    color_layer_offset_x = drawable_with_padding_offsets.offset_x
    color_layer_offset_y = drawable_with_padding_offsets.offset_y

    new_object_offset_x = drawable_with_padding_offsets.offset_x + offset_x
    new_object_offset_y = drawable_with_padding_offsets.offset_y + offset_y

    gimp_object.set_offsets(new_object_offset_x, new_object_offset_y)

  builtin_actions_utils.add_color_layer(
    padding_color,
    image_of_drawable_with_padding,
    drawable_with_padding,
    color_layer_offset_x,
    color_layer_offset_y,
    new_width_pixels,
    new_height_pixels,
    new_object_offset_x,
    new_object_offset_y,
    object_width,
    object_height,
  )


def _on_after_add_scale_action(_actions, action, _orig_action_dict, _settings):
  _set_visible_for_local_origin(
    action['arguments/object_to_scale'],
    action['arguments/local_origin'],
  )

  action['arguments/object_to_scale'].connect_event(
    'value-changed',
    _set_visible_for_local_origin,
    action['arguments/local_origin'])

  _set_sensitive_for_dimensions_given_scale_mode(
    action['arguments/scale_mode'],
    action['arguments/new_width'],
    action['arguments/new_height'],
  )

  action['arguments/scale_mode'].connect_event(
    'value-changed',
    _set_sensitive_for_dimensions_given_scale_mode,
    action['arguments/new_width'],
    action['arguments/new_height'],
  )

  action['arguments/padding_position'].connect_event(
    'value-changed',
    _set_visible_for_padding_custom_position,
    action['arguments/padding_position_custom'],
  )

  action['arguments/padding_position'].connect_event(
    'gui-visible-changed',
    _set_visible_for_padding_custom_position,
    action['arguments/padding_position_custom'],
  )

  _set_visible_for_padding_color_and_position(
    action['arguments/scale_mode'],
    action['arguments/padding_color'],
    action['arguments/padding_position'],
  )

  action['arguments/scale_mode'].connect_event(
    'value-changed',
    _set_visible_for_padding_color_and_position,
    action['arguments/padding_color'],
    action['arguments/padding_position'],
  )

  action['arguments/image_resolution'].connect_event(
    'after-set-gui',
    _set_left_margin_for_resolution,
  )

  _set_visible_for_resolution(
    action['arguments/set_image_resolution'],
    action['arguments/image_resolution'],
  )

  action['arguments/set_image_resolution'].connect_event(
    'value-changed',
    _set_visible_for_resolution,
    action['arguments/image_resolution'],
  )

  _set_visible_for_scale_condition_arguments(
    action['arguments/scale_condition'],
    action['arguments/scale_condition_width'],
    action['arguments/scale_condition_height'],
  )

  action['arguments/scale_condition'].connect_event(
    'value-changed',
    _set_visible_for_scale_condition_arguments,
    action['arguments/scale_condition_width'],
    action['arguments/scale_condition_height'],
  )

  builtin_commands_common.set_up_display_name_change_for_command(
    _set_display_name_for_scale,
    action['arguments/scale_mode'],
    action,
    [
      action['arguments/new_width'],
      action['arguments/new_height'],
    ],
    set_display_name_immediately=False,
  )

  action['arguments/new_width'].connect_event(
    'value-changed',
    _set_display_name_for_scale_via_new_width,
    action['arguments/scale_mode'],
    action,
    action['arguments/new_height'],
  )

  action['arguments/new_height'].connect_event(
    'value-changed',
    _set_display_name_for_scale_via_new_height,
    action['arguments/scale_mode'],
    action,
    action['arguments/new_width'],
  )


def _set_visible_for_local_origin(object_to_scale_setting, local_origin_setting):
  local_origin_setting.gui.set_visible(object_to_scale_setting.value != 'current_image')


def _set_sensitive_for_dimensions_given_scale_mode(
      scale_mode_setting,
      new_width_setting,
      new_height_setting,
):
  adjust_width = scale_mode_setting.value == ScaleModes.KEEP_ADJUST_WIDTH
  adjust_height = scale_mode_setting.value == ScaleModes.KEEP_ADJUST_HEIGHT

  new_width_setting.gui.set_sensitive(not adjust_height)
  new_height_setting.gui.set_sensitive(not adjust_width)


def _set_visible_for_padding_custom_position(
      padding_position_setting,
      padding_position_custom_setting,
):
  padding_position_custom_setting.gui.set_visible(
    padding_position_setting.gui.get_visible()
    and padding_position_setting.value == builtin_actions_utils.Positions.CUSTOM)


def _set_visible_for_padding_color_and_position(
      scale_mode_setting,
      padding_color_setting,
      padding_position_setting,
):
  padding_color_setting.gui.set_visible(
    scale_mode_setting.value == ScaleModes.FIT_WITH_PADDING)
  padding_position_setting.gui.set_visible(
    scale_mode_setting.value == ScaleModes.FIT_WITH_PADDING)


def _set_left_margin_for_resolution(image_resolution_setting):
  if not image_resolution_setting.gui.is_null():
    image_resolution_setting.gui.widget.set_margin_start(constants.RELATED_WIDGETS_LEFT_MARGIN)


def _set_visible_for_resolution(set_image_resolution_setting, image_resolution_setting):
  image_resolution_setting.gui.set_visible(set_image_resolution_setting.value)


def _set_visible_for_scale_condition_arguments(
      scale_condition_setting,
      scale_condition_width_setting,
      scale_condition_height_setting,
):
  is_visible = scale_condition_setting.value in [
    ScaleConditions.SMALLER_THAN_CUSTOM, ScaleConditions.LARGER_THAN_CUSTOM]

  scale_condition_width_setting.gui.set_visible(is_visible)
  scale_condition_height_setting.gui.set_visible(is_visible)


def _set_display_name_for_scale(
      scale_mode_setting,
      action,
      new_width_setting,
      new_height_setting,
):
  width_dimension = new_width_setting.value
  if width_dimension['unit'] == 'px':
    width_value = int(width_dimension['pixel_value'])
    width_unit = ' px'
  elif width_dimension['unit'] in [
      '%', setting_additional_.DimensionSetting.CUSTOM_PERCENT_SYMBOL]:
    width_value = round(width_dimension['percent_value'])
    width_unit = '%'
  else:
    width_value = round(width_dimension['other_value'], 2)
    width_unit = ' ' + width_dimension['unit']

  height_dimension = new_height_setting.value
  if height_dimension['unit'] == 'px':
    height_value = int(height_dimension['pixel_value'])
    height_unit = ' px'
  elif height_dimension['unit'] in [
      '%', setting_additional_.DimensionSetting.CUSTOM_PERCENT_SYMBOL]:
    height_value = round(height_dimension['percent_value'])
    height_unit = '%'
  else:
    height_value = round(height_dimension['other_value'], 2)
    height_unit = ' ' + height_dimension['unit']

  if scale_mode_setting.value == ScaleModes.STRETCH:
    action['display_name'].set_value(
      _('Scale, {}{}×{}{}').format(width_value, width_unit, height_value, height_unit))
  elif scale_mode_setting.value == ScaleModes.KEEP_ADJUST_WIDTH:
    action['display_name'].set_value(
      _('Scale, Width {}{}').format(width_value, width_unit))
  elif scale_mode_setting.value == ScaleModes.KEEP_ADJUST_HEIGHT:
    action['display_name'].set_value(
      _('Scale, Height {}{}').format(height_value, height_unit))
  elif scale_mode_setting.value == ScaleModes.FIT:
    action['display_name'].set_value(
      _('Scale, Fit Within {}{}×{}{}').format(width_value, width_unit, height_value, height_unit))
  elif scale_mode_setting.value == ScaleModes.FIT_WITH_PADDING:
    action['display_name'].set_value(
      _('Scale, Canvas {}{}×{}{}').format(
        width_value, width_unit, height_value, height_unit))
  else:
    action['display_name'].set_value(_('Scale'))


def _set_display_name_for_scale_via_new_width(
      new_width_setting,
      scale_mode_setting,
      action,
      new_height_setting,
):
  _set_display_name_for_scale(
    scale_mode_setting,
    action,
    new_width_setting,
    new_height_setting,
  )


def _set_display_name_for_scale_via_new_height(
      new_height_setting,
      scale_mode_setting,
      action,
      new_width_setting,
):
  _set_display_name_for_scale(
    scale_mode_setting,
    action,
    new_width_setting,
    new_height_setting,
  )


SCALE_FOR_IMAGES_DICT = {
  'name': 'scale_for_images',
  'function': scale,
  'display_name': _('Scale'),
  'menu_path': _('Resize and Transform'),
  'display_options_on_create': True,
  'additional_tags': [CONVERT_GROUP, EDIT_AND_SAVE_IMAGES_GROUP, EXPORT_IMAGES_GROUP],
  'arguments': [
    {
      'type': 'placeholder_image_or_layer',
      'name': 'object_to_scale',
      'default_value': 'current_image',
      'display_name': _('Apply to (image or layer):'),
    },
    {
      'type': 'choice',
      'name': 'scale_mode',
      'default_value': ScaleModes.STRETCH,
      'items': [
        (ScaleModes.STRETCH, _('Stretch')),
        (ScaleModes.KEEP_ADJUST_WIDTH, _('Match width (keep aspect ratio)')),
        (ScaleModes.KEEP_ADJUST_HEIGHT, _('Match height (keep aspect ratio)')),
        (ScaleModes.FIT, _('Fit within bounds')),
        (ScaleModes.FIT_WITH_PADDING, _('Fit to canvas')),
      ],
      'display_name': _('How to scale'),
    },
    {
      'type': 'dimension',
      'name': 'new_width',
      'default_value': {
        'pixel_value': 100.0,
        'percent_value': 100.0,
        'other_value': 1.0,
        'unit': '%',
        'percent_object': 'current_image',
        'percent_property': {
          placeholders_.ALL_IMAGE_PLACEHOLDERS: 'width',
          placeholders_.ALL_LAYER_PLACEHOLDERS: 'width',
        },
      },
      'min_value': 0.0,
      'display_name': _('Width'),
      'show_percent': True,
    },
    {
      'type': 'dimension',
      'name': 'new_height',
      'default_value': {
        'pixel_value': 100.0,
        'percent_value': 100.0,
        'other_value': 1.0,
        'unit': '%',
        'percent_object': 'current_image',
        'percent_property': {
          placeholders_.ALL_IMAGE_PLACEHOLDERS: 'height',
          placeholders_.ALL_LAYER_PLACEHOLDERS: 'height',
        },
      },
      'min_value': 0.0,
      'display_name': _('Height'),
      'show_percent': True,
    },
    {
      'type': 'enum',
      'enum_type': Gimp.InterpolationType,
      'name': 'interpolation',
      'default_value': Gimp.InterpolationType.NONE,
      'display_name': _('Interpolation'),
    },
    {
      'type': 'bool',
      'name': 'local_origin',
      'default_value': False,
      'display_name': _('Use local origin'),
    },
    {
      'type': 'bool',
      'name': 'set_image_resolution',
      'default_value': False,
      'display_name': _('Set image resolution in DPI'),
    },
    {
      'type': 'coordinates',
      'name': 'image_resolution',
      'default_value': {
        'x': 72.0,
        'y': 72.0,
      },
      'min_x': 0.0,
      'min_y': 0.0,
      'gui_kwargs': {
        'show_display_name': False,
      },
      'gui_type_kwargs': {
        # FOR TRANSLATORS: "X" represents the X-axis.
        'label_x': _('X'),
        # FOR TRANSLATORS: "Y" represents the Y-axis.
        'label_y': _('Y'),
      },
    },
    {
      'type': 'color',
      'name': 'padding_color',
      'default_value': [0.0, 0.0, 0.0, 0.0],
      'display_name': _('Padding color'),
    },
    {
      'type': 'choice',
      'name': 'padding_position',
      'default_value': builtin_actions_utils.Positions.CENTER,
      'items': [
        (builtin_actions_utils.Positions.START, _('Start')),
        (builtin_actions_utils.Positions.CENTER, _('Center')),
        (builtin_actions_utils.Positions.END, _('End')),
        (builtin_actions_utils.Positions.CUSTOM, _('Custom')),
      ],
      'display_name': _('Position'),
    },
    {
      'type': 'dimension',
      'name': 'padding_position_custom',
      'default_value': {
        'pixel_value': 0.0,
        'percent_value': 0.0,
        'other_value': 0.0,
        'unit': 'px',
        'percent_object': 'current_image',
        'percent_property': {
          placeholders_.ALL_IMAGE_PLACEHOLDERS: 'width',
          placeholders_.ALL_LAYER_PLACEHOLDERS: 'width',
        },
      },
      'min_value': 0.0,
      'display_name': _('Custom start position'),
    },
    {
      'type': 'choice',
      'name': 'scale_condition',
      'default_value': ScaleConditions.ALWAYS,
      'items': [
        (ScaleConditions.ALWAYS, _('Always')),
        (ScaleConditions.SMALLER, _('Smaller than new dimensions')),
        (ScaleConditions.LARGER, _('Larger than new dimensions')),
        (ScaleConditions.SMALLER_THAN_CUSTOM, _('Smaller than...')),
        (ScaleConditions.LARGER_THAN_CUSTOM, _('Larger than...')),
      ],
      'display_name': _('When to scale'),
    },
    {
      'type': 'dimension',
      'name': 'scale_condition_width',
      'default_value': {
        'pixel_value': 1920.0,
        'unit': 'px',
        'percent_object': 'current_image',
        'percent_property': {
          placeholders_.ALL_IMAGE_PLACEHOLDERS: 'width',
          placeholders_.ALL_LAYER_PLACEHOLDERS: 'width',
        },
      },
      'min_value': 0.0,
      'display_name': _('Custom width'),
    },
    {
      'type': 'dimension',
      'name': 'scale_condition_height',
      'default_value': {
        'pixel_value': 1080.0,
        'unit': 'px',
        'percent_object': 'current_image',
        'percent_property': {
          placeholders_.ALL_IMAGE_PLACEHOLDERS: 'height',
          placeholders_.ALL_LAYER_PLACEHOLDERS: 'height',
        },
      },
      'min_value': 0.0,
      'display_name': _('Custom height'),
    },
  ],
  'after_add_handler': _on_after_add_scale_action,
}

SCALE_FOR_LAYERS_DICT = utils.semi_deep_copy(SCALE_FOR_IMAGES_DICT)
SCALE_FOR_LAYERS_DICT.update({
  'name': 'scale_for_layers',
  'additional_tags': [EXPORT_LAYERS_GROUP, EDIT_LAYERS_GROUP],
})
SCALE_FOR_LAYERS_DICT['arguments'][0]['default_value'] = 'current_layer'

_SCALE_DIMENSION_ARGUMENT_INDEXES = [
  index for index, dict_ in enumerate(SCALE_FOR_LAYERS_DICT['arguments'])
  if dict_['type'] == 'dimension']
for index in _SCALE_DIMENSION_ARGUMENT_INDEXES:
  SCALE_FOR_LAYERS_DICT['arguments'][index]['default_value']['percent_object'] = 'current_layer'
