# Copyright 2012 Google Inc. All Rights Reserved.

"""An object representing EE Features."""



# Using old-style python function naming on purpose to match the
# javascript version's naming.
# pylint: disable-msg=C6003,C6409

import collections
import numbers

import ee_exception
import image
import serializer


class Feature(object):
  """An object representing EE Features."""

  def __init__(self, arg, opt_properties=None):
    """Create a feature from either a GeoJSON geometry or another feature.

    Args:
      arg: The geometry to use, or a pre-constructed Feature, or a JSON
          description of a call that returns a feature.
      opt_properties: A dictionary of metadata properties.  If a Feature
          is passed (instead of a geometry) this is unused.

    Raises:
      EEException: if the given geometry isn't valid.

    """
    if isinstance(arg, Feature):
      if opt_properties is not None:
        raise ee_exception.EEException(
            'Can\'t create Feature out of a Feature and properties.')
      self._description = dict(arg._description)    # pylint: disable-msg=W0212
      return

    if Feature.isValidGeometry(arg):
      self._description = {
          'type': 'Feature',
          'geometry': arg,
          'properties': opt_properties
      }
    elif 'algorithm' in arg and not opt_properties:
      self._description = arg
    else:
      raise ee_exception.EEException(
          'Not a geometry, feature or JSON description.')

  def __eq__(self, other):
    return self._description == other._description   # pylint: disable-msg=W0212

  def __ne__(self, other):
    return self._description != other._description   # pylint: disable-msg=W0212

  def __str__(self):
    """Writes out the feature in a human-readable form."""
    return 'Feature(%s)' % serializer.toJSON(self._description)

  def __repr__(self):
    """Writes out the feature in an eval-able form."""
    return 'ee.Feature(%s)' % self._description

  def serialize(self, opt_pretty=True):
    """Serialize this object into a JSON string.

    Args:
      opt_pretty: A flag indicating whether to pretty-print the JSON.

    Returns:
      A JSON represenation of this image.
    """
    return serializer.toJSON(self._description, opt_pretty)

  def getMapId(self, vis_params=None):
    """Fetch and return a map id and token, suitable for use in a Map overlay.

    Args:
      vis_params: The visualization parameters. Currently only one parameter,
          'color', containing a hex RGB color string is allowed.

    Returns:
      An object containing a mapid string, an access token, plus a DrawVector
      image wrapping a FeatureCollection containing this feature.
    """
    painted = image.Image({
        'algorithm': 'DrawVector',
        'collection': {
            'type': 'FeatureCollection',
            'features': [self]
        },
        'color': (vis_params or {}).get('color', '000000')
    })
    return painted.getMapId({})

  @staticmethod
  def isValidGeometry(geometry):
    """Check if a geometry looks valid.

    Args:
      geometry: The geometry to check.

    Returns:
      True if the geometry looks valid.
    """
    t = geometry.get('type')
    coords = geometry.get('coordinates')
    return (
        ((t == 'Point' and Feature.validateCoordinates(coords) == 1) or
         (t == 'MultiPoint' and Feature.validateCoordinates(coords) == 2) or
         (t == 'LineString' and Feature.validateCoordinates(coords) == 2) or
         (t == 'LinearRing' and Feature.validateCoordinates(coords) == 2) or
         (t == 'MultiLine' and Feature.validateCoordinates(coords) == 3) or
         (t == 'Polygon' and Feature.validateCoordinates(coords) == 3) or
         (t == 'MultiPolygon' and Feature.validateCoordinates(coords) == 4)))

  @staticmethod
  def validateCoordinates(shape):
    """Validate the coordinates of a geometry.

    Args:
      shape: The coordinates to validate.

    Returns:
      The number of nested arrays or -1 on error.
    """
    if not isinstance(shape, collections.Iterable):
      return -1

    shape = list(shape)
    if isinstance(shape[0], collections.Iterable):
      count = Feature.validateCoordinates(shape[0])
      # If more than 1 ring or polygon, they should have the same nesting.
      for i in xrange(1, len(shape)):
        if Feature.validateCoordinates(shape[i]) != count:
          return -1
      return count + 1
    else:
      # Make sure the pts are all numbers.
      for i in xrange(0, len(shape)):
        if not isinstance(shape[i], numbers.Number):
          return -1

      # Test that we have an even number of pts.
      if len(shape) % 2 == 0:
        return 1
      else:
        return -1

  @staticmethod
  def coordinatesToLine(coordinates):
    """Create a line from a list of points.

    Args:
      coordinates: The points to convert.  Must be list of numbers of
          even length, in the format [x1, y1, x2, y2, ...]

    Returns:
      An array of pairs of points.
    """
    if isinstance(coordinates[0], numbers.Number):
      line = []
      for i in xrange(0, len(coordinates), 2):
        pt = [coordinates[i], coordinates[i + 1]]
        line.append(pt)

      coordinates = line
    return coordinates

  @staticmethod
  def _makeGeometry(geometry, nesting, opt_coordinates=()):
    """Check that the given geometry has the specified level of nesting.

    If the user passed a list of points to one of the Geometry functions,
    then geometry will not be used and the coordinates in opt_coordinates
    will be processed instead.  This is to allow calls such as:
    Polygon(1,2,3,4,5,6) and Polygon([[[1,2],[3,4],[5,6]]])

    Args:
      geometry: The geometry to check.
      nesting: The expected level of array nesting.
      opt_coordinates: A list of coordinates to decode.

    Returns:
      The processed geometry.

    Raises:
      EEException: if the nesting level of the arrays isn't supported.
    """
    if nesting < 2 or nesting > 4:
      raise ee_exception.EEException('Unexpected nesting level.')

    # Handle a list of points.
    if isinstance(geometry, numbers.Number) and opt_coordinates:
      coordinates = [geometry]
      coordinates.extend(opt_coordinates)
      geometry = Feature.coordinatesToLine(coordinates)

    # Make sure the number of nesting levels is correct.
    item = geometry
    count = 0
    while isinstance(item, list) or isinstance(item, tuple):
      item = item[0]
      count += 1

    while count < nesting:
      geometry = [geometry]
      count += 1

    return geometry

  @staticmethod
  def Point(lon, lat):
    """Construct a GeoJSON Point.

    Args:
      lon: The longitude of the point.
      lat: The latitude of the point.

    Returns:
      A dictionary representing a GeoJSON Point.
    """
    return {
        'type': 'Point',
        'coordinates': [lon, lat]
        }

  @staticmethod
  def MultiPoint(coordinates, *rest):
    """Create a GeoJSON MultiPoint.

    Args:
      coordinates: The coordinates as either an array of [lon, lat] tuples,
          or literal pairs of coordinate longitudes and latitudes, such as
          MultiPoint(1, 2, 3, 4).

    Returns:
      A dictionary representing a GeoJSON MultiPoint.
    """
    return {
        'type': 'MultiPoint',
        'coordinates': Feature._makeGeometry(coordinates, 2, rest)
        }

  @staticmethod
  def Rectangle(xlo, ylo, xhi, yhi):
    """Construct a rectangular polygon from the given corner points.

    Args:
      xlo: The lower left x coordinate.
      ylo: The lower left y coordinate.
      xhi: The upper right x coordinate.
      yhi: The upepr right y coordinate.

    Returns:
      A dictionary representing a GeoJSON Polygon.
    """
    return {
        'type': 'Polygon',
        'coordinates': [[[xlo, yhi], [xlo, ylo], [xhi, ylo], [xhi, yhi]]]
        }

  @staticmethod
  def LineString(coordinates, *rest):
    """Create a GeoJSON LineString.

    Args:
      coordinates: The coordinates as either an array of [lon, lat] tuples,
          or literal pairs of coordinate longitudes and latitudes, such as
          LineString(1, 2, 3, 4).

    Returns:
      A dictionary representing a GeoJSON LineString.
    """
    return {
        'type': 'LineString',
        'coordinates': Feature._makeGeometry(coordinates, 2, rest)
        }

  @staticmethod
  def LinearRing(coordinates, *rest):
    """Construct a LinearRing from the given coordinates.

    Args:
      coordinates: The coordinates as either an array of [lon, lat] tuples,
          or literal pairs of coordinate longitudes and latitudes, such as
          LinearRing(1, 2, 3, 4, 5, 6).

    Returns:
      A dictionary representing a GeoJSON LinearRing.
    """
    return {
        'type': 'LinearRing',
        'coordinates': Feature._makeGeometry(coordinates, 2, rest)
        }

  @staticmethod
  def MultiLine(coordinates, *rest):
    """Create a GeoJSON MutliLine.

    Create a GeoJSON MultiLine from either a list of points, or an array
    of lines (each an array of Points).  If a list of points is specified,
    only a single line is created.

    Args:
      coordinates: The coordinates as either an array of arrays of
          [lon, lat] tuples, or literal pairs of coordinate longitudes
          and latitudes, such as MultiLine(1, 2, 3, 4, 5, 6).

    Returns:
      A dictionary representing a GeoJSON MultiLine.

    TODO(user): This actually doesn't accept an array of
    Feature.LineString, but it should.
    """
    return {
        'type': 'MultiLine',
        'coordinates': Feature._makeGeometry(coordinates, 3, rest)
        }

  @staticmethod
  def Polygon(coordinates, *rest):
    """Create a GeoJSON Polygon.

    Create a GeoJSON Polygon from either a list of points, or an array of
    linear rings.  If created from points, only an outer ring can be specified.

    Args:
      coordinates: The polygon coordinates as either a var_args list of
          numbers, or an array of rings, each of which is an array of points.

    Returns:
      A dictionary representing a GeoJSON polygon.

    TODO(user): This actually doesn't accept an array of
    Feature.LinearRings, but it should.
    """
    return {
        'type': 'Polygon',
        'coordinates': Feature._makeGeometry(coordinates, 3, rest)
        }

  @staticmethod
  def MultiPolygon(coordinates, *rest):
    """Create a GeoJSON MultiPolygon.

    If created from points, only one polygon can be specified.

    Args:
      coordinates: The multipolygon coordinates either as a var_args list
      of numbers of an array of polygons.
    Returns:
      A dictionary representing a GeoJSON MultiPolygon.

    TODO(user): This actually doesn't accept an array of
    Feature.Polygon, but it should.
    """
    return {
        'type': 'MultiPolygon',
        'coordinates': Feature._makeGeometry(coordinates, 4, rest)
        }
