"""A wrapper for dictionaries."""
from __future__ import annotations

from typing import Any, Dict, Optional, Union

from ee import _arg_types
from ee import _utils
from ee import apifunction
from ee import computedobject
from ee import ee_array
from ee import ee_list
from ee import ee_number
from ee import ee_string
from ee import geometry
from ee import image

# bool, float, and int are automatically converted to ee.String for keys.
_EeKeyType = Union[bool, float, int, str, computedobject.ComputedObject]
# TODO: Make a better type for a list of keys.
_EeKeyListType = Any
# TODO: Make a better type for a list of strings.
#   Or is this the same as _EeKeyListType?
_StringListType = Union[Any, computedobject.ComputedObject]


class Dictionary(computedobject.ComputedObject):
  """An object to represent dictionaries."""

  _dictionary: Optional[Dict[Any, Any]]

  _initialized = False

  # Tell pytype to not complain about dynamic attributes.
  _HAS_DYNAMIC_ATTRIBUTES = True

  def __init__(self, arg: Optional[_arg_types.Dictionary] = None):
    """Construct a dictionary.

    Args:
      arg: This constructor accepts the following args:
        1) Another dictionary.
        2) A list of key/value pairs.
        3) A null or no argument (producing an empty dictionary)
    """
    self.initialize()

    if isinstance(arg, dict):
      super().__init__(None, None)
      self._dictionary = arg
    else:
      self._dictionary = None
      if self.is_func_returning_same(arg):
        # If it's a call that's already returning a Dictionary, just cast.
        assert isinstance(arg, computedobject.ComputedObject)
        super().__init__(arg.func, arg.args, arg.varName)
      else:
        # Delegate everything else to the server-side constructor.
        super().__init__(apifunction.ApiFunction(self.name()), {'input': arg})

  @classmethod
  def initialize(cls) -> None:
    """Imports API functions to this class."""
    if not cls._initialized:
      apifunction.ApiFunction.importApi(cls, cls.name(), cls.name())
      cls._initialized = True

  @classmethod
  def reset(cls) -> None:
    """Removes imported API functions from this class."""
    apifunction.ApiFunction.clearApi(cls)
    cls._initialized = False

  @staticmethod
  def name() -> str:
    return 'Dictionary'

  @_utils.accept_opt_prefix('opt_encoder')
  def encode(self, encoder=None):
    if self._dictionary is not None:
      return encoder(self._dictionary)
    else:
      return super().encode(encoder)

  @_utils.accept_opt_prefix('opt_encoder')
  def encode_cloud_value(self, encoder=None):
    if self._dictionary is not None:
      return {'valueReference': encoder(self._dictionary)}
    else:
      return super().encode_cloud_value(encoder)

  def combine(
      self,
      second: _arg_types.Dictionary,
      overwrite: Optional[_arg_types.Bool] = None,
  ) -> Dictionary:
    """Combines two dictionaries.

    In the case of duplicate key names, the output will contain the value of the
    second dictionary unless overwrite is false. Null values in both
    dictionaries are ignored / removed.

    Args:
      second: The other dictionary to merge in.
      overwrite: If true, this keeps the value of the original dictionary.
        Defaults to true.

    Returns:
      An ee.Dictionary.
    """

    return apifunction.ApiFunction.call_(
        self.name() + '.combine', self, second, overwrite
    )

  def contains(self, key: _arg_types.String) -> computedobject.ComputedObject:
    """Returns true if the dictionary contains the given key.

    Args:
      key: A string to look for in the dictionary.

    Returns:
      An ee.Boolean.
    """

    return apifunction.ApiFunction.call_(self.name() + '.contains', self, key)

  # TODO: keys should be a _StringListType.
  @staticmethod
  def fromLists(keys: _arg_types.List, values: _arg_types.List) -> Dictionary:
    """Returns a dictionary from two parallel lists of keys and values.

    Args:
      keys: A list of keys.
      values: A list of values.
    """

    return apifunction.ApiFunction.call_('Dictionary.fromLists', keys, values)

  def get(
      self,
      key: _EeKeyType,
      # pylint: disable-next=invalid-name
      defaultValue: Optional[_arg_types.Any] = None,
  ) -> computedobject.ComputedObject:
    """Extracts a named value from a dictionary.

    If the dictionary does not contain the given key, then defaultValue is
    returned, unless it is null.

    Args:
      key: A string to look for in the dictionary.
      defaultValue: The value to return if the key is not found.

    Returns:
      Returns an ee.ComputedObject.
    """

    return apifunction.ApiFunction.call_(
        self.name() + '.get', self, key, defaultValue
    )

  def getArray(self, key: _EeKeyType) -> ee_array.Array:
    """Extracts a named array value from a dictionary.

    Args:
      key: A string to look for in the dictionary.

    Returns:
      An ee.Array.
    """

    return apifunction.ApiFunction.call_(self.name() + '.getArray', self, key)

  def getGeometry(self, key: _EeKeyType) -> geometry.Geometry:
    """Extracts a named geometry value from a dictionary.

    Args:
      key: A string to look for in the dictionary.

    Returns:
      An ee.Geometry.
    """

    return apifunction.ApiFunction.call_(
        self.name() + '.getGeometry', self, key
    )

  def getNumber(self, key: _EeKeyType) -> ee_number.Number:
    """Extracts a named number value from a dictionary.

    Args:
      key: A string to look for in the dictionary.

    Returns:
      An ee.Number.
    """

    return apifunction.ApiFunction.call_(self.name() + '.getNumber', self, key)

  def getString(self, key: _EeKeyType) -> ee_string.String:
    """Extracts a named string value from a dictionary.

    Args:
      key: A string to look for in the dictionary.

    Returns:
      An ee.String.
    """

    return apifunction.ApiFunction.call_(self.name() + '.getString', self, key)

  def keys(self) -> ee_list.List:
    """Retrieve the keys of a dictionary as a list."""

    return apifunction.ApiFunction.call_(self.name() + '.keys', self)

  # pylint: disable-next=invalid-name
  def map(self, baseAlgorithm: _arg_types.Any) -> Dictionary:
    """Map an algorithm over a dictionary.

    The algorithm is expected to take 2 arguments, a key from the existing
    dictionary and the value it corresponds to, and return a new value for the
    given key. If the algorithm returns null, the key is dropped.

    Args:
      baseAlgorithm: A function taking key, value and returning the new value.

    Returns:
      An ee.Dictionary with new values for each key.
    """

    return apifunction.ApiFunction.call_(
        self.name() + '.map', self, baseAlgorithm
    )

  def remove(
      self,
      selectors: _arg_types.Any,
      # pylint: disable-next=invalid-name
      ignoreMissing: Optional[_arg_types.Bool] = None,
  ) -> Dictionary:
    """Returns a dictionary with the specified keys removed.

    Args:
      selectors: A list of key names or regular expressions of key names to
        remove.
      ignoreMissing: Ignore selectors that don't match at least 1 key. Defaults
        to false.

    Returns:
      An ee.Dictionary.
    """

    return apifunction.ApiFunction.call_(
        self.name() + '.remove', self, selectors, ignoreMissing
    )

  # TODO: Make a tighter method signature.
  # pylint: disable-next=g-doc-args
  def rename(self, *args, **kwargs) -> Dictionary:
    """Rename elements in a dictionary.

    Args:
      from: A list of keys to be renamed.
      to: A list of the new names for the keys listed in the 'from' parameter.
        Must have the same length as the 'from' list.
      overwrite: Allow overwriting existing properties with the same name.

    Returns:
      An ee.Dictionary.
    """

    return apifunction.ApiFunction.call_(
        self.name() + '.rename', self, *args, **kwargs
    )

  def select(
      self,
      selectors: _arg_types.Any,
      # pylint: disable-next=invalid-name
      ignoreMissing: Optional[_arg_types.Bool] = None,
  ) -> Dictionary:
    """Returns a dictionary with only the specified keys.

    Args:
      selectors: A list of keys or regular expressions to select.
      ignoreMissing: Ignore selectors that don't match at least 1 key.
        Defaults to false.

    Returns:
      An ee.Dictionary.
    """

    return apifunction.ApiFunction.call_(
        self.name() + '.select', self, selectors, ignoreMissing
    )

  def set(self, key: _EeKeyType, value: _arg_types.Any) -> Dictionary:
    """Set a value in a dictionary.

    Args:
      key: A string for where to set the value. Does not need to already exist.
      value: The value to set for the key.

    Returns:
      An ee.Dictionary.
    """

    return apifunction.ApiFunction.call_(self.name() + '.set', self, key, value)

  def size(self) -> ee_number.Number:
    """Returns the number of entries in a dictionary."""

    return apifunction.ApiFunction.call_(self.name() + '.size', self)

  def toArray(
      self,
      keys: Optional[_EeKeyListType] = None,
      axis: Optional[_arg_types.Integer] = None,
  ) -> ee_array.Array:
    """Returns numeric values of a dictionary as an array.

    If no keys are specified, all values are returned in the natural ordering of
    the dictionary's keys. The default 'axis' is 0.

    Args:
      keys: An optional list of keys to subselect.
      axis: How to interpret values that are ee.Arrays. Defaults to 0.

    Returns:
      An ee.Array.
    """

    return apifunction.ApiFunction.call_(
        self.name() + '.toArray', self, keys, axis
    )

  def toImage(self, names: Optional[_arg_types.Any] = None) -> image.Image:
    """Creates an image of constants from values in a dictionary.

    The bands of the image are ordered and named according to the names
    argument. If no names are specified, the bands are sorted
    alpha-numerically.

    Args:
      names: The order of the output bands.

    Returns:
      An ee.Image.
    """

    return apifunction.ApiFunction.call_(self.name() + '.toImage', self, names)

  def values(self, keys: Optional[_EeKeyListType] = None) -> ee_list.List:
    """Returns the values of a dictionary as a list.

    If no keys are specified, all values are returned in the natural ordering of
    the dictionary's keys.

    Args:
      keys: An optional list of keys to subselect.

    Returns:
      An ee.Array.
    """

    return apifunction.ApiFunction.call_(self.name() + '.values', self, keys)
