"""Built-in actions related to adjusting colors."""

import re
import struct

import gi

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

from config import CONFIG
from src import commands
from src import constants
from src import exceptions
from src import setting as setting_
from src import utils_pdb
from src.procedure_groups import *
from src.pypdb import pdb
from src.gui import utils as gui_utils_


__all__ = [
  'brightness_contrast',
  'color_balance',
  'hue_saturation',
  'levels',
  'curves',
  'equalize',
  'white_balance',
  'stretch_contrast',
  'stretch_contrast_hsv',
  'color_enhance',
  'BrightnessContrastFilters',
]


class BrightnessContrastFilters:
  BRIGHTNESS_CONTRAST_FILTERS = (
    GEGL,
    GIMP,
  ) = 'gegl', 'gimp'


_HUE_SATURATION_RANGES = [
  'all',
  'red',
  'yellow',
  'green',
  'cyan',
  'blue',
  'magenta',
]


class CurveData:

  def __init__(self, curve_type=None, channel=None, samples=None, points=None):
    self.curve_type = curve_type
    self.channel = channel
    self.samples = samples
    self.points = points


class LevelsData:

  def __init__(
        self,
        channel=None,
        low_input=0.0,
        high_input=1.0,
        clamp_input=False,
        gamma=1.0,
        low_output=0.0,
        high_output=1.0,
        clamp_output=False,
  ):
    self.channel = channel
    self.low_input = low_input
    self.high_input = high_input
    self.clamp_input = clamp_input
    self.gamma = gamma
    self.low_output = low_output
    self.high_output = high_output
    self.clamp_output = clamp_output

  def has_default_values(self):
    return (
      self.low_input == 0.0
      and self.high_input == 1.0
      and self.gamma == 1.0
      and self.low_output == 0.0
      and self.high_output == 1.0
    )


def brightness_contrast(
      _batcher,
      layer,
      brightness,
      contrast,
      filter_,
      apply_non_destructively,
      blend_mode,
      opacity,
):
  value_range = _MAX_BRIGHTNESS_CONTRAST_VALUE - _MIN_BRIGHTNESS_CONTRAST_VALUE

  if filter_ == BrightnessContrastFilters.GEGL:
    processed_brightness = (brightness / value_range) * 2
    processed_contrast = (contrast / value_range) * 2 + 1.0

    pdb.gegl__brightness_contrast(
      layer,
      contrast=processed_contrast,
      brightness=processed_brightness,
      merge_filter_=not apply_non_destructively,
      blend_mode_=blend_mode,
      opacity_=opacity,
    )
  elif filter_ == BrightnessContrastFilters.GIMP:
    if utils_pdb.get_gimp_version() < (3, 1, 4):
      return

    processed_brightness = (brightness / value_range) * 2
    processed_contrast = (contrast / value_range) * 2

    pdb.gimp__brightness_contrast(
      layer,
      brightness=processed_brightness,
      contrast=processed_contrast,
      merge_filter_=not apply_non_destructively,
      blend_mode_=blend_mode,
      opacity_=opacity,
    )


def color_balance(
      _batcher,
      layer,
      transfer_mode,
      cyan_red_shadows,
      magenta_green_shadows,
      yellow_blue_shadows,
      cyan_red_midtones,
      magenta_green_midtones,
      yellow_blue_midtones,
      cyan_red_highlights,
      magenta_green_highlights,
      yellow_blue_highlights,
      preserve_luminosity,
      apply_non_destructively,
      blend_mode,
      opacity,
):
  if not (cyan_red_highlights == 0.0
          and magenta_green_highlights == 0.0
          and yellow_blue_highlights == 0.0):
    pdb.gimp__color_balance(
      layer,
      range=Gimp.TransferMode.HIGHLIGHTS,
      cyan_red=cyan_red_highlights,
      magenta_green=magenta_green_highlights,
      yellow_blue=yellow_blue_highlights,
      preserve_luminosity=preserve_luminosity,
      merge_filter_=not apply_non_destructively,
      blend_mode_=blend_mode,
      opacity_=opacity,
    )

  if not (cyan_red_midtones == 0.0
          and magenta_green_midtones == 0.0
          and yellow_blue_midtones == 0.0):
    pdb.gimp__color_balance(
      layer,
      range=Gimp.TransferMode.MIDTONES,
      cyan_red=cyan_red_midtones,
      magenta_green=magenta_green_midtones,
      yellow_blue=yellow_blue_midtones,
      preserve_luminosity=preserve_luminosity,
      merge_filter_=not apply_non_destructively,
      blend_mode_=blend_mode,
      opacity_=opacity,
    )

  if not (cyan_red_shadows == 0.0 and magenta_green_shadows == 0.0 and yellow_blue_shadows == 0.0):
    pdb.gimp__color_balance(
      layer,
      range=Gimp.TransferMode.SHADOWS,
      cyan_red=cyan_red_shadows,
      magenta_green=magenta_green_shadows,
      yellow_blue=yellow_blue_shadows,
      preserve_luminosity=preserve_luminosity,
      merge_filter_=not apply_non_destructively,
      blend_mode_=blend_mode,
      opacity_=opacity,
    )


def hue_saturation(
      _batcher,
      layer,
      **kwargs,
):
  range_nicks_and_values = [
    ('all', Gimp.HueRange.ALL),
    ('red', Gimp.HueRange.RED),
    ('yellow', Gimp.HueRange.YELLOW),
    ('green', Gimp.HueRange.GREEN),
    ('cyan', Gimp.HueRange.CYAN),
    ('blue', Gimp.HueRange.BLUE),
    ('magenta', Gimp.HueRange.MAGENTA),
  ]

  for range_nick, range_value in reversed(range_nicks_and_values):
    hue = kwargs[f'hue_{range_nick}']
    saturation = kwargs[f'saturation_{range_nick}']
    lightness = kwargs[f'lightness_{range_nick}']

    if not (hue == 0.0 and saturation == 0.0 and lightness == 0.0):
      pdb.gimp__hue_saturation(
        layer,
        range=range_value,
        hue=hue,
        saturation=saturation,
        lightness=lightness,
        overlap=kwargs['overlap'],
        merge_filter_=not kwargs['apply_non_destructively'],
        blend_mode_=kwargs['blend_mode'],
        opacity_=kwargs['opacity'],
      )


def levels(
      _batcher,
      layer,
      preset_file,
      apply_non_destructively,
      blend_mode,
      opacity,
):
  image = layer.get_image()

  if (image.get_base_type() == Gimp.ImageBaseType.INDEXED
      and preset_file is not None and preset_file.get_path() is not None):
    raise ValueError('Levels cannot be applied to indexed images')

  _apply_levels_curves(
    layer,
    preset_file,
    _parse_gimp_levels_preset,
    _parse_photoshop_levels_preset,
    _apply_levels,
    apply_non_destructively,
    blend_mode,
    opacity,
  )


def curves(
      _batcher,
      layer,
      preset_file,
      apply_non_destructively,
      blend_mode,
      opacity,
):
  _apply_levels_curves(
    layer,
    preset_file,
    _parse_gimp_curves_preset,
    _parse_photoshop_curves_preset,
    _apply_curves,
    apply_non_destructively,
    blend_mode,
    opacity,
  )


def _apply_levels_curves(
      layer,
      preset_file,
      _parse_gimp_preset,
      _parse_photoshop_preset,
      _apply_func,
      apply_non_destructively,
      blend_mode,
      opacity,
):
  if preset_file is None or preset_file.get_path() is None:
    raise exceptions.SkipCommand(_('Preset file not specified.'))

  try:
    with open(preset_file.get_path(), 'r', encoding=constants.TEXT_FILE_ENCODING) as f:
      preset_data = f.readlines()
  except Exception:
    file_successfully_read = False
  else:
    file_successfully_read = True

  trc = None
  curve_data = None

  if file_successfully_read:
    try:
      trc, curve_data = _parse_gimp_preset(preset_data)
    except Exception as e:
      raise ValueError(_FAILED_TO_READ_DATA_MESSAGE) from e
  else:
    with open(preset_file.get_path(), 'rb') as f:
      try:
        trc, curve_data = _parse_photoshop_preset(f)
      except Exception as e:
        raise ValueError(_FAILED_TO_READ_DATA_MESSAGE) from e

  if utils_pdb.get_gimp_version() < (3, 2) and trc != _TRC_TYPES['linear']:
    raise ValueError(
      _('Only presets with the linear mode are supported for GIMP versions earlier than 3.2.'
        ' Upgrade to GIMP 3.2 or later if you need to use presets with modes other than linear.'))

  if trc is not None and curve_data is not None:
    _apply_func(
      layer,
      trc,
      curve_data,
      apply_non_destructively,
      blend_mode,
      opacity,
    )


def _apply_levels(
      layer,
      trc,
      levels_data,
      apply_non_destructively,
      blend_mode,
      opacity,
):
  for levels_data_for_channel in levels_data.values():
    if not levels_data_for_channel.has_default_values():
      if utils_pdb.get_gimp_version() >= (3, 2):
        pdb.gimp__levels(
          layer,
          trc=trc,
          channel=levels_data_for_channel.channel,
          low_input=levels_data_for_channel.low_input,
          high_input=levels_data_for_channel.high_input,
          clamp_input=levels_data_for_channel.clamp_input,
          gamma=levels_data_for_channel.gamma,
          low_output=levels_data_for_channel.low_output,
          high_output=levels_data_for_channel.high_output,
          clamp_output=levels_data_for_channel.clamp_output,
          merge_filter_=not apply_non_destructively,
          blend_mode_=blend_mode,
          opacity_=opacity,
        )
      else:
        layer.levels(
          levels_data_for_channel.channel,
          levels_data_for_channel.low_input,
          levels_data_for_channel.high_input,
          levels_data_for_channel.clamp_input,
          levels_data_for_channel.gamma,
          levels_data_for_channel.low_output,
          levels_data_for_channel.high_output,
          levels_data_for_channel.clamp_output,
        )


def _apply_curves(
      layer,
      trc,
      curve_data,
      apply_non_destructively,
      blend_mode,
      opacity,
):
  for curve_data_for_channel in curve_data.values():
    if curve_data_for_channel.channel is None:
      continue

    if utils_pdb.get_gimp_version() >= (3, 2):
      curve = Gimp.Curve.new()

      if curve_data_for_channel.curve_type is not None:
        curve.set_curve_type(Gimp.CurveType.FREE)

      max_x = len(curve_data_for_channel.samples) - 1
      for index, sample in enumerate(curve_data_for_channel.samples):
        curve.set_sample(index / max_x, sample)

      pdb.gimp__curves(
        layer,
        trc=trc,
        channel=curve_data_for_channel.channel,
        curve=curve,
        merge_filter_=not apply_non_destructively,
        blend_mode_=blend_mode,
        opacity_=opacity,
      )
    else:
      if curve_data_for_channel.samples is not None:
        layer.curves_explicit(curve_data_for_channel.channel, curve_data_for_channel.samples)
      elif curve_data_for_channel.points is not None:
        layer.curves_spline(curve_data_for_channel.channel, curve_data_for_channel.points)
      else:
        raise ValueError('failed to obtain curve points from file')


def _parse_gimp_levels_preset(data):
  # We create empty levels data with a fixed order of channels. When levels are
  # applied as filters, they are appended. A filter using the VALUE channel
  # must be appended after other channels so that we obtain a result identical
  # to the Levels tool in GIMP. Hence, the VALUE channel is created here as the
  # last channel.
  levels_data = {
    channel: LevelsData()
    for channel in reversed(_HISTOGRAM_CHANNELS.values())
  }

  trc = None
  clamp_input = None
  clamp_output = None

  current_channel = None

  for line in data:
    if trc is None:
      trc = _get_trc_from_str(_parse_entry(line, 'trc'))

    if clamp_input is None:
      clamp_input = _parse_clamp_value(line, 'clamp-input')

    if clamp_output is None:
      clamp_output = _parse_clamp_value(line, 'clamp-output')

    parsed_channel = _parse_entry(line, 'channel')
    if parsed_channel is not None:
      current_channel = _get_channel_from_str(parsed_channel)
      levels_data[current_channel].channel = current_channel
      levels_data[current_channel].clamp_input = clamp_input
      levels_data[current_channel].clamp_output = clamp_output

    for entry in [
      'low-input',
      'high-input',
      'gamma',
      'low-output',
      'high-output',
    ]:
      parsed_entry = _parse_entry(line, entry)
      if parsed_entry is not None:
        setattr(levels_data[current_channel], entry.replace('-', '_'), float(parsed_entry))

  if trc is None:
    trc = _TRC_TYPES['linear']

  return trc, levels_data


def _parse_photoshop_levels_preset(file):
  _version = struct.unpack('>H', file.read(2))

  # We ignore other channels provided in the .alv file as there is no support
  # for them in GIMP at the time of writing this comment.
  levels_data = {}
  for channel in _HISTOGRAM_CHANNELS.values():
    levels_parameters = _read_levels_parameters(file)
    levels_data[channel] = LevelsData(
      channel=channel,
      **levels_parameters,
      clamp_input=True,
      clamp_output=True,
    )

  return _TRC_TYPES['linear'], levels_data


def _read_levels_parameters(file):
  return {
    # The range for low_input is 0..253:
    # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1057086
    'low_input': struct.unpack('>H', file.read(2))[0] / 253.0,
    'high_input': struct.unpack('>H', file.read(2))[0] / 255.0,
    'low_output': struct.unpack('>H', file.read(2))[0] / 255.0,
    'high_output': struct.unpack('>H', file.read(2))[0] / 255.0,
    'gamma': struct.unpack('>H', file.read(2))[0] / 100.0,
  }


def _parse_gimp_curves_preset(data):
  # We create empty curve data with a fixed order of channels. When curves are
  # applied as filters, they are appended. A filter using the VALUE channel
  # must be appended after other channels so that we obtain a result identical
  # to the Curves tool in GIMP. Hence, the VALUE channel is created here as the
  # last channel.
  curve_data = {
    channel: CurveData()
    for channel in reversed(_HISTOGRAM_CHANNELS.values())
  }
  trc = None
  current_channel = None
  current_curve_type_str = None

  for line in data:
    if trc is None:
      trc = _get_trc_from_str(_parse_entry(line, 'trc'))

    parsed_channel = _parse_entry(line, 'channel')
    if parsed_channel is not None:
      current_channel = _get_channel_from_str(parsed_channel)

    match = re.match(r'\s*\(curve-type +(.*?)\)+', line)
    if match and current_channel is not None:
      current_curve_type_str = match.group(1)
      if utils_pdb.get_gimp_version() >= (3, 2):
        curve_data[current_channel].curve_type = _get_curve_type_from_str(current_curve_type_str)

    match = re.match(r'\s*\(points +(.*?)\)+', line)
    if match and current_channel is not None:
      points = match.group(1)

      keep_channel_intact = (
        (re.match(r' *0 *', points) or re.match(r' *4 +0 +0 +1 +1 *', points))
        and current_curve_type_str != 'free')

      if not keep_channel_intact:
        curve_data[current_channel].channel = current_channel

    match = re.match(r'\s*\(samples +[0-9]+ +(.*?)\)+', line)
    if match and current_channel in curve_data:
      samples = _parse_samples_or_points(match.group(1))
      if samples is not None:
        curve_data[current_channel].samples = samples

  if trc is None:
    trc = _TRC_TYPES['linear']

  return trc, curve_data


def _parse_entry(line, name):
  match = re.match(r'\s*\(' + re.escape(name) + r' +(.*?)\)+', line)
  if match:
    return match.group(1)
  else:
    return None


def _parse_clamp_value(line, name):
  clamp_value_str = _parse_entry(line, name)
  if clamp_value_str is not None:
    return clamp_value_str.lower() == 'yes'
  else:
    return None


def _get_channel_from_str(channel_str):
  return _HISTOGRAM_CHANNELS.get(channel_str, None)


def _get_curve_type_from_str(curve_type_str):
  return _CURVE_TYPES.get(curve_type_str, None)


def _get_trc_from_str(trc_str):
  return _TRC_TYPES.get(trc_str, None)


def _parse_samples_or_points(str_):
  number_list_str = re.split(r' +', str_)

  try:
    return [float(number) for number in number_list_str if number]
  except ValueError:
    return None


def _parse_photoshop_curves_preset(file):
  # The code is based on: https://gist.github.com/fish2000/5641c3697fa4407fcfd59099575d6938

  version, n_curves = struct.unpack('>HH', file.read(4))

  if version not in [1, 4]:
    raise ValueError('version for the Photoshop curves format (.acv) must be 1 or 4')

  points_per_channel = []
  for i in range(n_curves):
    points_per_channel.append(_read_points(file))

  curve_data = {}
  for channel, points in zip(_HISTOGRAM_CHANNELS.values(), points_per_channel):
    curve_data[channel] = CurveData(channel=channel, points=points)

  return _TRC_TYPES['linear'], curve_data


def _read_points(file):
  curve_points = []
  n_points_in_curve, = struct.unpack('>H', file.read(2))

  for index in range(n_points_in_curve):
    output_value, input_value = struct.unpack('>HH', file.read(4))
    curve_points.append(input_value / 255.0)
    curve_points.append(output_value / 255.0)

  return curve_points


def equalize(_batcher, layer, mask_only):
  layer.equalize(mask_only)


def white_balance(_batcher, layer):
  layer.levels_stretch()


def stretch_contrast(
      _batcher,
      layer,
      keep_colors,
      perceptual,
      apply_non_destructively,
      blend_mode,
      opacity,
):
  pdb.gegl__stretch_contrast(
    layer,
    keep_colors=keep_colors,
    perceptual=perceptual,
    merge_filter_=not apply_non_destructively,
    blend_mode_=blend_mode,
    opacity_=opacity,
  )


def stretch_contrast_hsv(_batcher, layer):
  pdb.gegl__stretch_contrast_hsv(layer, merge_filter_=True)


def color_enhance(_batcher, layer):
  pdb.gegl__color_enhance(layer, merge_filter_=True)


def _on_after_add_brightness_contrast_action(_actions, action, _orig_action_dict, _settings):
  _hide_filter_for_gimp_lower_than_3_1_4(action)
  _hide_filter_arguments_for_gimp_lower_than_3_2(action)


def _on_after_add_color_balance_action(_actions, action, _orig_action_dict, _settings):
  action.add([
    {
      'type': 'generic',
      'name': 'load_preset_preprocessor',
      'default_value': _preprocess_loaded_preset_data_for_color_balance,
      'tags': ['ignore_reset', 'ignore_load', 'ignore_save'],
    },
    {
      'type': 'generic',
      'name': 'save_preset_preprocessor',
      'default_value': _preprocess_color_balance_data_before_preset_save,
      'tags': ['ignore_reset', 'ignore_load', 'ignore_save'],
    },
  ])

  _set_visible_for_transfer_mode_settings(
    action['arguments/transfer_mode'],
    action['arguments'],
  )

  action['arguments/transfer_mode'].connect_event(
    'value-changed',
    _set_visible_for_transfer_mode_settings,
    action['arguments'],
  )


def _preprocess_loaded_preset_data_for_color_balance(action, parsed_data):
  current_transfer_mode = None
  warning_displayed = False

  preprocessed_parsed_data = []

  for name, arguments in parsed_data:
    if name == 'range' and len(arguments) >= 1:
      current_transfer_mode = arguments[0].lower()
      continue

    if name == 'gimp-mode':
      processed_name = 'blend_mode'

      blend_mode_str = arguments[0].lower() if arguments else 'replace'

      if not warning_displayed:
        warning_displayed = _warn_on_non_default_blend_mode_and_opacity(
          # The `blend_mode` setting is hidden under More options.
          action['arguments/opacity'],
          blend_mode_str != 'replace',
        )
    elif name == 'gimp-opacity':
      processed_name = 'opacity'

      if not warning_displayed:
        try:
          opacity_value = float(arguments[0])
        except Exception:
          pass
        else:
          warning_displayed = _warn_on_non_default_blend_mode_and_opacity(
            action['arguments/opacity'],
            opacity_value < 100.0,
          )
    else:
      processed_name = name.replace('-', '_')

    if processed_name in ['cyan_red', 'magenta_green', 'yellow_blue']:
      preprocessed_parsed_data.append((f'{processed_name}_{current_transfer_mode}', arguments))
    else:
      preprocessed_parsed_data.append((processed_name, arguments))

  return preprocessed_parsed_data


def _preprocess_color_balance_data_before_preset_save(_action, data):
  processed_data = []

  for name, arguments_str in data:
    processed_name = name

    if processed_name in ['transfer_mode', 'apply_non_destructively']:
      continue

    if processed_name == 'blend_mode':
      processed_data.append(('gimp-mode', arguments_str))
      continue

    if processed_name == 'opacity':
      processed_data.append(('gimp-opacity', arguments_str))
      continue

    if processed_name == 'cyan_red_shadows':
      processed_data.append(('range', 'shadows'))
    elif processed_name == 'cyan_red_midtones':
      processed_data.append(('range', 'midtones'))
    elif processed_name == 'cyan_red_highlights':
      processed_data.append(('range', 'highlights'))

    processed_name = re.sub(r'^(cyan_red|magenta_green|yellow_blue).*', r'\1', processed_name)

    processed_name = processed_name.replace('_', '-')

    processed_data.append((processed_name, arguments_str))

  return processed_data


def _set_visible_for_transfer_mode_settings(transfer_mode_setting, arguments):
  arguments['cyan_red_shadows'].gui.set_visible(
    transfer_mode_setting.value == Gimp.TransferMode.SHADOWS)
  arguments['magenta_green_shadows'].gui.set_visible(
    transfer_mode_setting.value == Gimp.TransferMode.SHADOWS)
  arguments['yellow_blue_shadows'].gui.set_visible(
    transfer_mode_setting.value == Gimp.TransferMode.SHADOWS)

  arguments['cyan_red_midtones'].gui.set_visible(
    transfer_mode_setting.value == Gimp.TransferMode.MIDTONES)
  arguments['magenta_green_midtones'].gui.set_visible(
    transfer_mode_setting.value == Gimp.TransferMode.MIDTONES)
  arguments['yellow_blue_midtones'].gui.set_visible(
    transfer_mode_setting.value == Gimp.TransferMode.MIDTONES)

  arguments['cyan_red_highlights'].gui.set_visible(
    transfer_mode_setting.value == Gimp.TransferMode.HIGHLIGHTS)
  arguments['magenta_green_highlights'].gui.set_visible(
    transfer_mode_setting.value == Gimp.TransferMode.HIGHLIGHTS)
  arguments['yellow_blue_highlights'].gui.set_visible(
    transfer_mode_setting.value == Gimp.TransferMode.HIGHLIGHTS)


def _on_after_add_hue_saturation_action(_actions, action, _orig_action_dict, _settings):
  action.add([
    {
      'type': 'generic',
      'name': 'load_preset_preprocessor',
      'default_value': _preprocess_loaded_preset_data_for_hue_saturation,
      'tags': ['ignore_reset', 'ignore_load', 'ignore_save'],
    },
    {
      'type': 'generic',
      'name': 'save_preset_preprocessor',
      'default_value': _preprocess_hue_saturation_data_before_preset_save,
      'tags': ['ignore_reset', 'ignore_load', 'ignore_save'],
    },
  ])

  _set_visible_for_range_settings(
    action['arguments/range'],
    action['arguments'],
  )

  action['arguments/range'].connect_event(
    'value-changed',
    _set_visible_for_range_settings,
    action['arguments'],
  )


def _preprocess_loaded_preset_data_for_hue_saturation(action, parsed_data):
  current_range = None
  warning_displayed = False

  preprocessed_parsed_data = []

  for name, arguments in parsed_data:
    if name == 'range' and len(arguments) >= 1:
      current_range = arguments[0].lower()
      continue

    if name == 'gimp-mode':
      processed_name = 'blend_mode'

      blend_mode_str = arguments[0].lower() if arguments else 'replace'

      if not warning_displayed:
        warning_displayed = _warn_on_non_default_blend_mode_and_opacity(
          # The `blend_mode` setting is hidden under More options.
          action['arguments/opacity'],
          blend_mode_str != 'replace',
        )
    elif name == 'gimp-opacity':
      processed_name = 'opacity'

      if not warning_displayed:
        try:
          opacity_value = float(arguments[0])
        except Exception:
          pass
        else:
          warning_displayed = _warn_on_non_default_blend_mode_and_opacity(
            action['arguments/opacity'],
            opacity_value < 100.0,
          )
    else:
      processed_name = name.replace('-', '_')

    if processed_name in ['hue', 'saturation', 'lightness']:
      preprocessed_parsed_data.append((f'{processed_name}_{current_range}', arguments))
    else:
      preprocessed_parsed_data.append((processed_name, arguments))

  return preprocessed_parsed_data


def _preprocess_hue_saturation_data_before_preset_save(_action, data):
  processed_data = []

  for name, arguments_str in data:
    processed_name = name

    if processed_name in ['range', 'apply_non_destructively']:
      continue

    if processed_name == 'blend_mode':
      processed_data.append(('gimp-mode', arguments_str))
      continue

    if processed_name == 'opacity':
      processed_data.append(('gimp-opacity', arguments_str))
      continue

    for range_ in _HUE_SATURATION_RANGES:
      if processed_name.startswith('hue') and processed_name.endswith(range_):
        processed_data.append(('range', range_))

    processed_name = re.sub(r'^(hue|saturation|lightness).*', r'\1', processed_name)

    processed_name = processed_name.replace('_', '-')

    processed_data.append((processed_name, arguments_str))

  return processed_data


def _set_visible_for_range_settings(range_setting, arguments):
  range_nicks_and_values = [
    ('all', Gimp.HueRange.ALL),
    ('red', Gimp.HueRange.RED),
    ('yellow', Gimp.HueRange.YELLOW),
    ('green', Gimp.HueRange.GREEN),
    ('cyan', Gimp.HueRange.CYAN),
    ('blue', Gimp.HueRange.BLUE),
    ('magenta', Gimp.HueRange.MAGENTA),
  ]

  for nick, value in range_nicks_and_values:
    arguments[f'hue_{nick}'].gui.set_visible(range_setting.value == value)
    arguments[f'saturation_{nick}'].gui.set_visible(range_setting.value == value)
    arguments[f'lightness_{nick}'].gui.set_visible(range_setting.value == value)


def _warn_on_non_default_blend_mode_and_opacity(setting_for_popover, predicate):
  if CONFIG.RUN_MODE == Gimp.RunMode.INTERACTIVE and predicate:
    if not isinstance(setting_for_popover.gui, setting_.NullPresenter):
      gui_utils_.display_popover(
        setting_for_popover.gui.widget,
        _('Due to a technical limitation, non-default blend mode or opacity values below 100'
          ' may not produce the same results as the corresponding tool in GIMP.'),
        icon_name=GimpUi.ICON_DIALOG_WARNING,
        max_width_chars=35,
      )

      return True

  return False


def _on_after_add_levels_action(_actions, action, _orig_action_dict, _settings):
  _hide_filter_arguments_for_gimp_lower_than_3_2(action)


def _on_after_add_curves_action(_actions, action, _orig_action_dict, _settings):
  _hide_filter_arguments_for_gimp_lower_than_3_2(action)


def _hide_filter_for_gimp_lower_than_3_1_4(action):
  if utils_pdb.get_gimp_version() < (3, 1, 4):
    action['arguments/filter_'].gui.set_visible(False)


def _hide_filter_arguments_for_gimp_lower_than_3_2(action):
  if utils_pdb.get_gimp_version() < (3, 2):
    action['arguments/apply_non_destructively'].gui.set_visible(False)
    action['arguments/blend_mode'].gui.set_visible(False)
    action['arguments/opacity'].gui.set_visible(False)


_MIN_BRIGHTNESS_CONTRAST_VALUE = -127
_MAX_BRIGHTNESS_CONTRAST_VALUE = 127

_FAILED_TO_READ_DATA_MESSAGE = 'Failed to obtain data from file. File may be corrupt.'

_HISTOGRAM_CHANNELS = {
  'value': Gimp.HistogramChannel.VALUE,
  'red': Gimp.HistogramChannel.RED,
  'green': Gimp.HistogramChannel.GREEN,
  'blue': Gimp.HistogramChannel.BLUE,
  'alpha': Gimp.HistogramChannel.ALPHA,
}

if utils_pdb.get_gimp_version() >= (3, 2):
  _CURVE_TYPES = {
    'smooth': Gimp.CurveType.SMOOTH,
    'free': Gimp.CurveType.FREE,
  }
else:
  _CURVE_TYPES = {
    'smooth': 0,
    'free': 1,
  }

if utils_pdb.get_gimp_version() >= (3, 2):
  _TRC_TYPES = {
    'linear': Gimp.TRCType.LINEAR,
    'non-linear': Gimp.TRCType.NON_LINEAR,
    'perceptual': Gimp.TRCType.PERCEPTUAL,
  }
else:
  _TRC_TYPES = {
    'linear': 0,
    'non-linear': 1,
    'perceptual': 2,
  }

BRIGHTNESS_CONTRAST_DICT = {
  'name': 'brightness_contrast',
  'function': brightness_contrast,
  'display_name': _('Brightness-Contrast'),
  'menu_path': _('Colors'),
  'display_options_on_create': True,
  'additional_tags': ALL_PROCEDURE_GROUPS,
  'arguments': [
    {
      'type': 'placeholder_layer',
      'name': 'layer',
      'display_name': _('Layer'),
    },
    {
      'type': 'int',
      'name': 'brightness',
      'default_value': 0,
      'display_name': _('Brightness'),
      'min_value': _MIN_BRIGHTNESS_CONTRAST_VALUE,
      'max_value': _MAX_BRIGHTNESS_CONTRAST_VALUE,
    },
    {
      'type': 'int',
      'name': 'contrast',
      'default_value': 0,
      'display_name': _('Contrast'),
      'min_value': _MIN_BRIGHTNESS_CONTRAST_VALUE,
      'max_value': _MAX_BRIGHTNESS_CONTRAST_VALUE,
    },
    {
      'type': 'choice',
      'name': 'filter_',
      'default_value': (
        BrightnessContrastFilters.GEGL
        if utils_pdb.get_gimp_version() < (3, 1, 4)
        else BrightnessContrastFilters.GIMP),
      'items': [
        (BrightnessContrastFilters.GEGL, _('GEGL')),
        (BrightnessContrastFilters.GIMP, _('GIMP')),
      ],
      'display_name': _('Filter'),
      'tags': [commands.MORE_OPTIONS_TAG],
    },
    {
      'type': 'bool',
      'name': 'apply_non_destructively',
      'default_value': True,
      'display_name': _('Apply non-destructively'),
    },
    {
      'type': 'enum',
      'name': 'blend_mode',
      'enum_type': Gimp.LayerMode,
      'default_value': Gimp.LayerMode.REPLACE,
      'display_name': _('Blend mode'),
      'tags': [commands.MORE_OPTIONS_TAG],
    },
    {
      'type': 'double',
      'name': 'opacity',
      'default_value': 1.0,
      'min_value': 0.0,
      'max_value': 1.0,
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
      'display_name': _('Opacity'),
    },
  ],
  'after_add_handler': _on_after_add_brightness_contrast_action,
}

COLOR_BALANCE_DICT = {
  'name': 'color_balance',
  'function': color_balance,
  'display_name': _('Color Balance'),
  'menu_path': _('Colors'),
  'description': _(
    'Applies the Colors → Color Balance tool. You may load a preset file saved with this tool.'
  ),
  'display_options_on_create': True,
  'additional_tags': [*ALL_PROCEDURE_GROUPS, commands.CAN_MANAGE_PRESETS_TAG],
  'arguments': [
    {
      'type': 'placeholder_layer',
      'name': 'layer',
      'display_name': _('Layer'),
    },
    {
      'type': 'enum',
      'name': 'transfer_mode',
      'enum_type': Gimp.TransferMode,
      'default_value': Gimp.TransferMode.SHADOWS,
      'display_name': _('Range'),
    },
    {
      'type': 'double',
      'name': 'cyan_red_shadows',
      'default_value': 0.0,
      'min_value': -1.0,
      'max_value': 1.0,
      'display_name': _('Cyan-red'),
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
    },
    {
      'type': 'double',
      'name': 'magenta_green_shadows',
      'default_value': 0.0,
      'min_value': -1.0,
      'max_value': 1.0,
      'display_name': _('Magenta-green'),
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
    },
    {
      'type': 'double',
      'name': 'yellow_blue_shadows',
      'default_value': 0.0,
      'min_value': -1.0,
      'max_value': 1.0,
      'display_name': _('Yellow-blue'),
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
    },
    {
      'type': 'double',
      'name': 'cyan_red_midtones',
      'default_value': 0.0,
      'min_value': -1.0,
      'max_value': 1.0,
      'display_name': _('Cyan-red'),
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
    },
    {
      'type': 'double',
      'name': 'magenta_green_midtones',
      'default_value': 0.0,
      'min_value': -1.0,
      'max_value': 1.0,
      'display_name': _('Magenta-green'),
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
    },
    {
      'type': 'double',
      'name': 'yellow_blue_midtones',
      'default_value': 0.0,
      'min_value': -1.0,
      'max_value': 1.0,
      'display_name': _('Yellow-blue'),
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
    },
    {
      'type': 'double',
      'name': 'cyan_red_highlights',
      'default_value': 0.0,
      'min_value': -1.0,
      'max_value': 1.0,
      'display_name': _('Cyan-red'),
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
    },
    {
      'type': 'double',
      'name': 'magenta_green_highlights',
      'default_value': 0.0,
      'min_value': -1.0,
      'max_value': 1.0,
      'display_name': _('Magenta-green'),
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
    },
    {
      'type': 'double',
      'name': 'yellow_blue_highlights',
      'default_value': 0.0,
      'min_value': -1.0,
      'max_value': 1.0,
      'display_name': _('Yellow-blue'),
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
    },
    {
      'type': 'bool',
      'name': 'preserve_luminosity',
      'default_value': True,
      'display_name': _('Preserve luminosity'),
    },
    {
      'type': 'bool',
      'name': 'apply_non_destructively',
      'default_value': True,
      'display_name': _('Apply non-destructively'),
    },
    {
      'type': 'enum',
      'name': 'blend_mode',
      'enum_type': Gimp.LayerMode,
      'default_value': Gimp.LayerMode.REPLACE,
      'display_name': _('Blend mode'),
      'tags': [commands.MORE_OPTIONS_TAG],
    },
    {
      'type': 'double',
      'name': 'opacity',
      'default_value': 1.0,
      'min_value': 0.0,
      'max_value': 1.0,
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
      'display_name': _('Opacity'),
    },
  ],
  'available': lambda _command_dict: utils_pdb.get_gimp_version() >= (3, 1, 4),
  'after_add_handler': _on_after_add_color_balance_action,
}

HUE_SATURATION_DICT = {
  'name': 'hue_saturation',
  'function': hue_saturation,
  'display_name': _('Hue-Saturation'),
  'menu_path': _('Colors'),
  'description': _(
    'Applies the Colors → Hue-Saturation tool. You may load a preset file saved with this tool.'
  ),
  'display_options_on_create': True,
  'additional_tags': [*ALL_PROCEDURE_GROUPS, commands.CAN_MANAGE_PRESETS_TAG],
  'arguments': [
    {
      'type': 'placeholder_layer',
      'name': 'layer',
      'display_name': _('Layer'),
    },
    {
      'type': 'enum',
      'name': 'range',
      'enum_type': Gimp.HueRange,
      'default_value': Gimp.HueRange.ALL,
      'display_name': _('Range'),
    },
    *[
      {
        'type': 'double',
        'name': f'{arg}_{range_}',
        'default_value': 0.0,
        'min_value': -1.0,
        'max_value': 1.0,
        'display_name': display_name,
        'gui_type_kwargs': {
          'factor': 180.0 if arg == 'hue' else 100.0,
          'digits': 1,
        },
      }
      for range_ in _HUE_SATURATION_RANGES
      for arg, display_name in [
        ('hue', _('Hue')), ('saturation', _('Saturation')), ('lightness', _('Lightness'))]
    ],
    {
      'type': 'double',
      'name': 'overlap',
      'default_value': 0.0,
      'min_value': 0.0,
      'max_value': 1.0,
      'display_name': _('Overlap'),
    },
    {
      'type': 'bool',
      'name': 'apply_non_destructively',
      'default_value': True,
      'display_name': _('Apply non-destructively'),
    },
    {
      'type': 'enum',
      'name': 'blend_mode',
      'enum_type': Gimp.LayerMode,
      'default_value': Gimp.LayerMode.REPLACE,
      'display_name': _('Blend mode'),
      'tags': [commands.MORE_OPTIONS_TAG],
    },
    {
      'type': 'double',
      'name': 'opacity',
      'default_value': 1.0,
      'min_value': 0.0,
      'max_value': 1.0,
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
      'display_name': _('Opacity'),
    },
  ],
  'available': lambda _command_dict: utils_pdb.get_gimp_version() >= (3, 1, 4),
  'after_add_handler': _on_after_add_hue_saturation_action,
}

LEVELS_DICT = {
  'name': 'levels',
  'function': levels,
  'display_name': _('Levels'),
  'menu_path': _('Colors'),
  'description': _(
    'Applies levels using a preset file saved in GIMP or Photoshop (.alv file).'
    '\n\nTo obtain a preset file in GIMP, go to Colors → Levels and'
    ' <a href="https://docs.gimp.org/en/gimp-colors-menu.html#colors-common-features">'
    'export the settings</a>.'
  ),
  'display_options_on_create': True,
  'additional_tags': ALL_PROCEDURE_GROUPS,
  'arguments': [
    {
      'type': 'placeholder_layer',
      'name': 'layer',
      'display_name': _('Layer'),
    },
    {
      'type': 'file',
      'name': 'preset_file',
      'default_value': None,
      'action': Gimp.FileChooserAction.OPEN,
      'display_name': _('Preset file'),
      'none_ok': True,
    },
    {
      'type': 'bool',
      'name': 'apply_non_destructively',
      'default_value': True,
      'display_name': _('Apply non-destructively'),
    },
    {
      'type': 'enum',
      'name': 'blend_mode',
      'enum_type': Gimp.LayerMode,
      'default_value': Gimp.LayerMode.REPLACE,
      'display_name': _('Blend mode'),
      'tags': [commands.MORE_OPTIONS_TAG],
    },
    {
      'type': 'double',
      'name': 'opacity',
      'default_value': 1.0,
      'min_value': 0.0,
      'max_value': 1.0,
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
      'display_name': _('Opacity'),
    },
  ],
  'after_add_handler': _on_after_add_levels_action,
}

CURVES_DICT = {
  'name': 'curves',
  'function': curves,
  'display_name': _('Curves'),
  'menu_path': _('Colors'),
  'description': _(
    'Applies curves using a preset file saved in GIMP or Photoshop (.acv file).'
    '\n\nTo obtain a preset file in GIMP, go to Colors → Curves and'
    ' <a href="https://docs.gimp.org/en/gimp-colors-menu.html#colors-common-features">'
    'export the settings</a>.'
  ),
  'display_options_on_create': True,
  'additional_tags': ALL_PROCEDURE_GROUPS,
  'arguments': [
    {
      'type': 'placeholder_layer',
      'name': 'layer',
      'display_name': _('Layer'),
    },
    {
      'type': 'file',
      'name': 'preset_file',
      'default_value': None,
      'action': Gimp.FileChooserAction.OPEN,
      'display_name': _('Preset file'),
      'none_ok': True,
    },
    {
      'type': 'bool',
      'name': 'apply_non_destructively',
      'default_value': True,
      'display_name': _('Apply non-destructively'),
    },
    {
      'type': 'enum',
      'name': 'blend_mode',
      'enum_type': Gimp.LayerMode,
      'default_value': Gimp.LayerMode.REPLACE,
      'display_name': _('Blend mode'),
      'tags': [commands.MORE_OPTIONS_TAG],
    },
    {
      'type': 'double',
      'name': 'opacity',
      'default_value': 1.0,
      'min_value': 0.0,
      'max_value': 1.0,
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
      'display_name': _('Opacity'),
    },
  ],
  'after_add_handler': _on_after_add_curves_action,
}

EQUALIZE_DICT = {
  'name': 'equalize',
  'function': equalize,
  'display_name': _('Equalize'),
  'menu_path': '{}/{}'.format(_('Colors'), _('Auto')),
  'display_options_on_create': False,
  'additional_tags': ALL_PROCEDURE_GROUPS,
  'arguments': [
    {
      'type': 'placeholder_layer',
      'name': 'layer',
      'display_name': _('Layer'),
    },
    {
      'type': 'bool',
      'name': 'mask_only',
      'default_value': False,
      'display_name': _('Mask only'),
    },
  ],
}

WHITE_BALANCE_DICT = {
  'name': 'white_balance',
  'function': white_balance,
  'display_name': _('White Balance'),
  'menu_path': '{}/{}'.format(_('Colors'), _('Auto')),
  'display_options_on_create': False,
  'additional_tags': ALL_PROCEDURE_GROUPS,
  'arguments': [
    {
      'type': 'placeholder_layer',
      'name': 'layer',
      'display_name': _('Layer'),
    },
  ],
}

STRETCH_CONTRAST_DICT = {
  'name': 'stretch_contrast',
  'function': stretch_contrast,
  'display_name': _('Stretch Contrast'),
  'menu_path': '{}/{}'.format(_('Colors'), _('Auto')),
  'display_options_on_create': True,
  'additional_tags': ALL_PROCEDURE_GROUPS,
  'arguments': [
    {
      'type': 'placeholder_layer',
      'name': 'layer',
      'display_name': _('Layer'),
    },
    {
      'type': 'bool',
      'name': 'keep_colors',
      'default_value': True,
      'display_name': _('Keep colors'),
    },
    {
      'type': 'bool',
      'name': 'perceptual',
      'default_value': False,
      'display_name': _('Non-linear components'),
    },
    {
      'type': 'bool',
      'name': 'apply_non_destructively',
      'default_value': True,
      'display_name': _('Apply non-destructively'),
    },
    {
      'type': 'enum',
      'name': 'blend_mode',
      'enum_type': Gimp.LayerMode,
      'default_value': Gimp.LayerMode.REPLACE,
      'display_name': _('Blend mode'),
      'tags': [commands.MORE_OPTIONS_TAG],
    },
    {
      'type': 'double',
      'name': 'opacity',
      'default_value': 1.0,
      'min_value': 0.0,
      'max_value': 1.0,
      'gui_type_kwargs': {
        'factor': 100.0,
        'digits': 1,
      },
      'display_name': _('Opacity'),
    },
  ],
}

STRETCH_CONTRAST_HSV_DICT = {
  'name': 'stretch_contrast_hsv',
  'function': stretch_contrast_hsv,
  'display_name': _('Stretch Contrast HSV'),
  'menu_path': '{}/{}'.format(_('Colors'), _('Auto')),
  'display_options_on_create': False,
  'additional_tags': ALL_PROCEDURE_GROUPS,
  'arguments': [
    {
      'type': 'placeholder_layer',
      'name': 'layer',
      'display_name': _('Layer'),
    },
  ],
}

COLOR_ENHANCE_DICT = {
  'name': 'color_enhance',
  'function': color_enhance,
  'display_name': _('Color Enhance'),
  'menu_path': '{}/{}'.format(_('Colors'), _('Auto')),
  'display_options_on_create': False,
  'additional_tags': ALL_PROCEDURE_GROUPS,
  'arguments': [
    {
      'type': 'placeholder_layer',
      'name': 'layer',
      'display_name': _('Layer'),
    },
  ],
}
