#!/usr/bin/env python
"""Collection filters.

Example usage:
  Filter('time', low, high)
     .bounds(ring)
     .eq('time', value)
     .lt('time', value)
"""



# Using lowercase function naming to match the JavaScript names.
# pylint: disable=g-bad-name

from . import apifunction
from . import computedobject
from . import ee_exception


# A map from the deprecated old-style comparison operator names to API
# function names, implicitly prefixed with "Filter.". Negative operators
# (those starting with "not_") are not included.
_FUNCTION_NAMES = {
    'equals': 'equals',
    'less_than': 'lessThan',
    'greater_than': 'greaterThan',
    'contains': 'stringContains',
    'starts_with': 'stringStartsWith',
    'ends_with': 'stringEndsWith',
}


class Filter(computedobject.ComputedObject):
  """An object to represent collection filters."""

  _initialized = False

  def __init__(self, opt_filter=None):
    """Construct a filter.

    This constructor accepts the following args:
      1) Another filter.
      2) An array of filters (which are implicitly ANDed together).
      3) A ComputedObject returning a filter. Users shouldn't be making these;
         they're produced by the generator functions below.

    Args:
      opt_filter: Optional filter to add.
    """
    self.initialize()

    if isinstance(opt_filter, (list, tuple)):
      if not opt_filter:
        raise ee_exception.EEException('Empty list specified for ee.Filter().')
      elif len(opt_filter) == 1:
        opt_filter = opt_filter[0]
      else:
        self._filter = tuple(opt_filter)
        super(Filter, self).__init__(
            apifunction.ApiFunction.lookup('Filter.and'),
            {'filters': self._filter})
        return

    if isinstance(opt_filter, computedobject.ComputedObject):
      super(Filter, self).__init__(
          opt_filter.func, opt_filter.args, opt_filter.varName)
      self._filter = (opt_filter,)
    elif opt_filter is None:
      # A silly call with no arguments left for backward-compatibility.
      # Encoding such a filter is expected to fail, but it can be composed
      # by calling the various methods that end up in _append().
      super(Filter, self).__init__(None, None)
      self._filter = ()
    else:
      raise ee_exception.EEException(
          'Invalid argument specified for ee.Filter(): %s' % opt_filter)

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

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

  def predicateCount(self):
    """Return the number of predicates that have been added to this filter.

    Returns:
      The number of predicates that have been added to this filter.
      This does not count nested predicates.
    """
    return len(self._filter)

  def _append(self, new_filter):
    """Append a predicate to this filter.

    These are implicitly ANDed.

    Args:
      new_filter: The filter to append to this one.  Possible types are:
          1) another fully constructed Filter,
          2) a JSON representation of a filter,
          3) an array of 1 or 2.

    Returns:
      A new filter that is the combination of both.
    """
    if new_filter is not None:
      prev = list(self._filter)
      if isinstance(new_filter, Filter):
        prev.extend(new_filter._filter)  # pylint: disable=protected-access
      elif isinstance(new_filter, list):
        prev.extend(new_filter)
      else:
        prev.append(new_filter)
    return Filter(prev)

  def Not(self):
    """Returns the opposite of this filter.

    Returns:
      The negated filter, which will match iff this filter doesn't.
    """
    return apifunction.ApiFunction.call_('Filter.not', self)

  @staticmethod
  def metadata_(name, operator, value):
    """Filter on metadata. This is deprecated.

    Args:
      name: The property name to filter on.
      operator: The type of comparison. One of:
          "equals", "less_than", "greater_than", "contains", "begins_with",
          "ends_with", or any of these prefixed with "not_".
      value: The value to compare against.

    Returns:
      The new filter.

    Deprecated.  Use ee.Filter.eq(), ee.Filter.gte(), etc.'
    """
    operator = operator.lower()

    # Check for negated filters.
    negated = False
    if operator.startswith('not_'):
      negated = True
      operator = operator[4:]

    # Convert the operator to a function.
    if operator not in _FUNCTION_NAMES:
      raise ee_exception.EEException(
          'Unknown filtering operator: %s' % operator)
    func_name = 'Filter.' + _FUNCTION_NAMES[operator]
    new_filter = apifunction.ApiFunction.call_(func_name, name, value)

    return new_filter.Not() if negated else new_filter

  @staticmethod
  def eq(name, value):
    """Filter to metadata equal to the given value."""
    return apifunction.ApiFunction.call_('Filter.equals', name, value)

  @staticmethod
  def neq(name, value):
    """Filter to metadata not equal to the given value."""
    return Filter.eq(name, value).Not()

  @staticmethod
  def lt(name, value):
    """Filter to metadata less than the given value."""
    return apifunction.ApiFunction.call_('Filter.lessThan', name, value)

  @staticmethod
  def gte(name, value):
    """Filter on metadata greater than or equal to the given value."""
    return Filter.lt(name, value).Not()

  @staticmethod
  def gt(name, value):
    """Filter on metadata greater than the given value."""
    return apifunction.ApiFunction.call_('Filter.greaterThan', name, value)

  @staticmethod
  def lte(name, value):
    """Filter on metadata less than or equal to the given value."""
    return Filter.gt(name, value).Not()

  @staticmethod
  def And(*args):
    """Combine two or more filters using boolean AND."""
    if len(args) == 1 and isinstance(args[0], (list, tuple)):
      args = args[0]
    return apifunction.ApiFunction.call_('Filter.and', args)

  @staticmethod
  def Or(*args):
    """Combine two or more filters using boolean OR."""
    if len(args) == 1 and isinstance(args[0], (list, tuple)):
      args = args[0]
    return apifunction.ApiFunction.call_('Filter.or', args)

  @staticmethod
  def date(start, opt_end=None):
    """Filter images by date.

    The start and end may be a Date, numbers (interpreted as milliseconds since
    1970-01-01T00:00:00Z), or strings (such as '1996-01-01T08:00'). Based on
    'system:time_start'.

    Args:
      start: The inclusive start date.
      opt_end: The optional exclusive end date, If not specified, a
               1-millisecond range starting at 'start' is created.

    Returns:
      The modified filter.
    """
    date_range = apifunction.ApiFunction.call_('DateRange', start, opt_end)
    return apifunction.ApiFunction.apply_('Filter.dateRangeContains', {
        'leftValue': date_range,
        'rightField': 'system:time_start'
    })

  @staticmethod
  def inList(opt_leftField=None,
             opt_rightValue=None,
             opt_rightField=None,
             opt_leftValue=None):
    """Filter on metadata contained in a list.

    Args:
      opt_leftField: A selector for the left operand.
          Should not be specified if leftValue is specified.
      opt_rightValue: The value of the right operand.
          Should not be specified if rightField is specified.
      opt_rightField: A selector for the right operand.
          Should not be specified if rightValue is specified.
      opt_leftValue: The value of the left operand.
          Should not be specified if leftField is specified.

    Returns:
      The constructed filter.
    """
    # Implement this in terms of listContains, with the arguments switched.
    # In listContains the list is on the left side, while in inList it's on
    # the right.
    return apifunction.ApiFunction.apply_('Filter.listContains', {
        'leftField': opt_rightField,
        'rightValue': opt_leftValue,
        'rightField': opt_leftField,
        'leftValue': opt_rightValue
    })

  @staticmethod
  def geometry(geometry, opt_errorMargin=None):
    """Filter on intersection with geometry.

    Items in the collection with a footprint that fails to intersect
    the given geometry will be excluded.

    Args:
      geometry: The geometry to filter to either as a GeoJSON geometry,
          or a FeatureCollection, from which a geometry will be extracted.
      opt_errorMargin: An optional error margin. If a number, interpreted as
          sphere surface meters.

    Returns:
      The constructed filter.
    """
    # Invoke geometry promotion then manually promote to a Feature.
    args = {
        'leftField': '.all',
        'rightValue': apifunction.ApiFunction.call_('Feature', geometry)
    }
    if opt_errorMargin is not None:
      args['maxError'] = opt_errorMargin
    return apifunction.ApiFunction.apply_('Filter.intersects', args)

  @staticmethod
  def bounds(geometry, opt_errorMargin=None):
    """Filter on intersection with geometry.

    Items in the collection with a footprint that fails to intersect
    the given geometry will be excluded. This is an alias for geometry().

    Caution: providing a large or complex collection as input can result in poor
    performance. Collating the geometry of collections does not scale well, use
    the smallest collection (or geometry) that is required to achieve the
    desired outcome.

    Args:
      geometry: The geometry to filter to either as a GeoJSON geometry,
          or a FeatureCollection, from which a geometry will be extracted.
      opt_errorMargin: An optional error margin. If a number, interpreted as
          sphere surface meters.

    Returns:
      The constructed filter.
    """
    return Filter.geometry(geometry, opt_errorMargin)

  @staticmethod
  def name():
    return 'Filter'
