"""Utility functions used within the `builtin_actions` package."""

from typing import Union
import collections
import math
import os

import gi

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

from src import exceptions
from src import file_formats as file_formats_
from src import placeholders as placeholders_
from src import setting as setting_
from src import setting_additional as setting_additional_
from src.pypdb import pdb


__all__ = [
  'EXPORT_NAME_ITEM_STATE',
  'InsertionPositions',
  'UNIT_DEGREE',
  'UNIT_RADIAN',
  'UNITS',
  'AnchorPoints',
  'ANCHOR_POINTS_ITEMS_AND_DISPLAY_NAMES',
  'Positions',
  'get_item_export_name',
  'set_item_export_name',
  'get_item_filepath',
  'unit_to_pixels',
  'angle_to_radians',
  'add_color_layer',
  'get_best_matching_layer_from_image',
]


EXPORT_NAME_ITEM_STATE = 'export_name'


class InsertionPositions:

  INSERTION_POSITIONS = (
    BACKGROUND,
    FOREGROUND,
    TOP,
    BOTTOM,
  ) = 'background', 'foreground', 'top', 'bottom'


class AnchorPoints:
  ANCHOR_POINTS = (
    TOP_LEFT,
    TOP,
    TOP_RIGHT,
    LEFT,
    CENTER,
    RIGHT,
    BOTTOM_LEFT,
    BOTTOM,
    BOTTOM_RIGHT,
  ) = (
    'top_left',
    'top',
    'top_right',
    'left',
    'center',
    'right',
    'bottom_left',
    'bottom',
    'bottom_right',
  )


ANCHOR_POINTS_ITEMS_AND_DISPLAY_NAMES = [
  (AnchorPoints.TOP_LEFT, _('Top left')),
  (AnchorPoints.TOP, _('Top')),
  (AnchorPoints.TOP_RIGHT, _('Top right')),
  (AnchorPoints.LEFT, _('Left')),
  (AnchorPoints.CENTER, _('Center')),
  (AnchorPoints.RIGHT, _('Right')),
  (AnchorPoints.BOTTOM_LEFT, _('Bottom left')),
  (AnchorPoints.BOTTOM, _('Bottom')),
  (AnchorPoints.BOTTOM_RIGHT, _('Bottom right')),
]


class Positions:
  POSITIONS = (
    START,
    CENTER,
    END,
    CUSTOM,
  ) = (
    'start',
    'center',
    'end',
    'custom',
  )


class _AngleUnit:

  def __init__(self, name, display_name, scaling_factor):
    self._name = name
    self._display_name = display_name
    self._scaling_factor = scaling_factor

  @property
  def name(self):
    return self._name

  @property
  def display_name(self):
    return self._display_name

  @property
  def scaling_factor(self):
    return self._scaling_factor


UNIT_DEGREE = _AngleUnit('degree', _('degrees'), 180 / math.pi)
UNIT_RADIAN = _AngleUnit('radian', _('radians'), 1.0)

UNITS = {
  UNIT_DEGREE.name: UNIT_DEGREE,
  UNIT_RADIAN.name: UNIT_RADIAN,
}


def get_item_export_name(item) -> str:
  item_state = item.get_named_state(EXPORT_NAME_ITEM_STATE)
  return item_state['name'] if item_state is not None else item.name


def set_item_export_name(item, name):
  item.get_named_state(EXPORT_NAME_ITEM_STATE)['name'] = name


def get_item_filepath(item, directory: Union[Gio.File, str, None]) -> str:
  """Returns a file path based on the specified directory and the name of
  the item and its parents.

  The file path created has the following format:

    <directory path>/<item path components>/<item name>

  If the directory path is not an absolute path or is ``None``, the
  current working directory is prepended.

  Item path components consist of parents' item names, starting with the
  topmost parent.
  """
  if directory is None:
    dirpath = ''
  elif isinstance(directory, Gio.File):
    if directory.get_path() is not None:
      dirpath = directory.get_path()
    else:
      dirpath = ''
  else:
    dirpath = directory

  dirpath = os.path.abspath(dirpath)

  path_components = [get_item_export_name(parent) for parent in item.parents]
  if path_components:
    dirpath = os.path.join(dirpath, os.path.join(*path_components))

  return os.path.join(dirpath, get_item_export_name(item))


def unit_to_pixels(
      batcher,
      dimension,
      resolution_axis,
      percent_object=None,
      percent_property_name=None,
):
  """Converts the value of a `setting_additional.DimensionSetting` to pixels.

  ``resolution_axis`` is either ``'x'`` or ``'y'`` and represents the image
  resolution along the x- or y-axis, respectively. The resolution is used to
  convert units other than pixels or percentages (e.g. inches) to pixels. The
  image to obtain resolution from is the currently processed image
  (`batcher.current_image`).

  If ``percent_object`` is not ``None``, it replaces the percentage object
  specified in ``dimension['percent_object']``. This must be an actual object
  rather than a placeholder. If ``percent_object`` is not ``None``, then
  ``percent_property_name`` must also be specified, which is one of the
  accepted values as in ``dimension['percent_property']``.
  """
  if dimension['unit'] in ['%', setting_additional_.DimensionSetting.CUSTOM_PERCENT_SYMBOL]:
    if percent_object is None:
      if isinstance(dimension['percent_object'], dict):
        percent_object_name = dimension['percent_object']['name']
        percent_object_kwargs = {
          key: value for key, value in dimension['percent_object'].items() if key != 'name'}
      else:
        percent_object_name = dimension['percent_object']
        percent_object_kwargs = {}

      placeholder_object = placeholders_.PLACEHOLDERS[percent_object_name]
      gimp_object = placeholder_object.replace_args(None, batcher, **percent_object_kwargs)

      percent_property_name = _get_percent_property_value(
        dimension['percent_property'], percent_object_name)
    else:
      gimp_object = percent_object

    if percent_property_name == 'width':
      gimp_object_dimension = gimp_object.get_width()
    elif percent_property_name == 'height':
      gimp_object_dimension = gimp_object.get_height()
    elif percent_property_name == 'x_offset':
      if isinstance(gimp_object, Gimp.Image):
        gimp_object_dimension = 0
      else:
        gimp_object_dimension = gimp_object.get_offsets().offset_x
    elif percent_property_name == 'y_offset':
      if isinstance(gimp_object, Gimp.Image):
        gimp_object_dimension = 0
      else:
        gimp_object_dimension = gimp_object.get_offsets().offset_y
    else:
      raise ValueError(f'unrecognized percent property: {percent_property_name}')

    pixels = (dimension['percent_value'] / 100) * gimp_object_dimension
  elif dimension['unit'] == 'px':
    pixels = dimension['pixel_value']
  else:
    image_resolution = batcher.current_image.get_resolution()
    if resolution_axis == 'x':
      image_resolution_for_axis = image_resolution.xresolution
    elif resolution_axis == 'y':
      image_resolution_for_axis = image_resolution.yresolution
    else:
      raise ValueError(f'unrecognized value for resolution_axis: {resolution_axis}')

    unit = setting_.UnitSetting.raw_data_to_unit(dimension['unit'])

    if unit is not None:
      factor = unit.get_factor()
    else:
      factor = 1.0

    pixels = dimension['other_value'] / factor * image_resolution_for_axis

  int_pixels = round(pixels)

  return int_pixels


def _get_percent_property_value(percent_property, percent_object):
  """Returns the property (e.g. width, X-offset) for the current value of
  ``'percent_object'`` within the `DimensionSetting` value's
  ``percent_property`` entry.
  """
  for key in percent_property:
    if percent_object in key:
      return percent_property[key]

  return None


def angle_to_radians(angle):
  scaling_factor = UNITS[angle['unit']].scaling_factor

  if scaling_factor != 0.0:
    return angle['value'] / scaling_factor
  else:
    return angle['value']


def add_color_layer(
      padding_color,
      image,
      drawable,
      color_layer_offset_x,
      color_layer_offset_y,
      color_layer_width,
      color_layer_height,
      selection_x,
      selection_y,
      selection_width,
      selection_height,
):
  Gimp.context_push()
  Gimp.context_set_foreground(setting_.ColorSetting.get_value_as_color(padding_color))
  Gimp.context_set_opacity(
    setting_.ColorSetting.get_value_as_color(padding_color).get_rgba().alpha * 100)

  channel = pdb.gimp_selection_save(image=image)
  image.select_rectangle(
    Gimp.ChannelOps.REPLACE,
    selection_x,
    selection_y,
    selection_width,
    selection_height,
  )

  pdb.gimp_selection_invert(image=image)

  selection_is_non_empty = pdb.gimp_selection_bounds(image=image)[0]
  if selection_is_non_empty:
    color_layer = Gimp.Layer.new(
      image,
      drawable.get_name(),
      color_layer_width,
      color_layer_height,
      Gimp.ImageType.RGBA_IMAGE,
      100.0,
      Gimp.LayerMode.NORMAL,
    )
    color_layer.set_offsets(color_layer_offset_x, color_layer_offset_y)
    image.insert_layer(
      color_layer,
      drawable.get_parent(),
      image.get_item_position(drawable) + 1,
    )

    color_layer.edit_fill(Gimp.FillType.FOREGROUND)

    image.merge_down(drawable, Gimp.MergeType.EXPAND_AS_NECESSARY)

  image.select_item(Gimp.ChannelOps.REPLACE, channel)
  image.remove_channel(channel)

  Gimp.context_pop()


def get_best_matching_layer_from_image(batcher, image):
  if image == batcher.current_image:
    if batcher.current_layer.is_valid():
      return batcher.current_layer

  selected_layers = image.get_selected_layers()
  if selected_layers:
    return image.get_selected_layers()[0]
  else:
    layers = image.get_layers()
    if layers:
      return layers[0]
    else:
      # Rather than returning no layer, we skip the current action. An image
      # having no layers points to a problem outside the action.
      raise exceptions.SkipCommand(_('The image has no layers.'))


class _FileExtension:
  """Class holding properties for a file extension."""

  def __init__(self):
    self.is_valid = True
    """If ``True``, the file extension is valid and can be used in filenames
    for file export procedures.
    """
    self.processed_count = 0
    """Number of items with the file extension that have been exported."""


class FileExtensionProperties:
  """Mapping of file extensions from `file_formats.FILE_FORMATS` to
  `_FileExtension` instances.

  File extension as a key is always converted to lowercase.
  """
  def __init__(self, import_or_export):
    if import_or_export not in ['import', 'export']:
      raise ValueError('invalid value for import_or_export; must be either "import" or "export"')

    self._import_or_export = import_or_export

    self._properties = collections.defaultdict(_FileExtension)

    for file_format in file_formats_.FILE_FORMATS:
      if self._import_or_export == 'import' and not file_format.has_import_proc():
        continue

      if self._import_or_export == 'export' and not file_format.has_export_proc():
        continue

      # This ensures that the file format dialog will be displayed only once per
      # file format if multiple file extensions for the same format are used
      # (e.g. 'jpg', 'jpeg' or 'jpe' for the JPEG format).
      extension_properties = _FileExtension()
      for file_extension in file_format.file_extensions:
        self._properties[file_extension.lower()] = extension_properties

  def __getitem__(self, key):
    return self._properties[key.lower()]
