# coding=utf-8
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
                        unicode_literals, with_statement)

import collections
import inspect
from abc import abstractmethod
from functools import update_wrapper

import six

from pants.build_graph.address import Address
from pants.engine.objects import Resolvable, Serializable
from pants.util.meta import AbstractClass
from pants.util.objects import datatype


class Addresses(datatype('Addresses', ['dependencies'])):
  pass


class TypeConstraint(AbstractClass):
  """Represents a type constraint.

  Not intended for direct use; instead, use one of :class:`SuperclassesOf`, :class:`Exact` or
  :class:`SubclassesOf`.
  """

  def __init__(self, *types, **kwargs):
    """Creates a type constraint centered around the given types.

    The type constraint is satisfied as a whole if satisfied for at least one of the given types.

    :param type *types: The focus of this type constraint.
    :param str description: A description for this constraint if the list of types is too long.
    """
    if not types:
      raise ValueError('Must supply at least one type')
    if any(not isinstance(t, type) for t in types):
      raise TypeError('Supplied types must be types. {!r}'.format(types))

    self._types = types
    self._desc = kwargs.get('description', None)

  @property
  def types(self):
    """Return the subject types of this type constraint.

    :type: tuple of type
    """
    return self._types

  def satisfied_by(self, obj):
    """Return `True` if the given object satisfies this type constraint.

    :rtype: bool
    """
    return self.satisfied_by_type(type(obj))

  @abstractmethod
  def satisfied_by_type(self, obj_type):
    """Return `True` if the given object satisfies this type constraint.

    :rtype: bool
    """

  def __hash__(self):
    return hash((type(self), self._types))

  def __eq__(self, other):
    return type(self) == type(other) and self._types == other._types

  def __ne__(self, other):
    return not (self == other)

  def __str__(self):
    if self._desc:
      constrained_type = '({})'.format(self._desc)
    else:
      if len(self._types) == 1:
        constrained_type = self._types[0].__name__
      else:
        constrained_type = '({})'.format(', '.join(t.__name__ for t in self._types))
    return '{variance_symbol}{constrained_type}'.format(variance_symbol=self._variance_symbol,
                                                        constrained_type=constrained_type)

  def __repr__(self):
    if self._desc:
      constrained_type = self._desc
    else:
      constrained_type = ', '.join(t.__name__ for t in self._types)
    return ('{type_constraint_type}({constrained_type})'
      .format(type_constraint_type=type(self).__name__,
                    constrained_type=constrained_type))


class SuperclassesOf(TypeConstraint):
  """Objects of the exact type as well as any super-types are allowed."""

  _variance_symbol = '-'

  def satisfied_by_type(self, obj_type):
    return any(issubclass(t, obj_type) for t in self._types)


class Exactly(TypeConstraint):
  """Only objects of the exact type are allowed."""

  _variance_symbol = '='

  def satisfied_by_type(self, obj_type):
    return obj_type in self._types


class SubclassesOf(TypeConstraint):
  """Objects of the exact type as well as any sub-types are allowed."""

  _variance_symbol = '+'

  def satisfied_by_type(self, obj_type):
    return issubclass(obj_type, self._types)


class NotSerializableError(TypeError):
  """Indicates an addressable descriptor is illegally installed in a non-Serializable type."""


class MutationError(AttributeError):
  """Indicates an illegal attempt to mutate an addressable attribute that already has a value."""


class TypeConstraintError(TypeError):
  """Indicates a :class:`TypeConstraint` violation."""


class AddressableDescriptor(object):
  """A data descriptor for fields containing one or more addressable items.

  An addressable descriptor has lifecycle expectations tightly coupled with the contract of
  Serializable objects and the 2-phase hydration of AddressMap.parse, Graph.resolve.

  Decorated accessors are write-once, and then read-only.  They are intended to be written in a
  constructor such that objects containing them have immutable semantics. In other words, the
  descriptor is intended to be used like a type-checked `@property` with possibly lazily resolved
  values.

  The written value is type-checked against a :class:`TypeConstraint` and can only be one of 3
  types:

  1. An opaque string address.
  2. A Resolvable for the address that, when resolved, will meet the type constraint.
  3. A concrete value that meets the type constraint.

  The 1st type, an opaque string address, is also the type associated with the 1st stage of the
  2-stage lifecycle of Serializable objects containing addressable values.  In the second and final
  stage, the Serializable object is re-constructed with addressable values of the second or third
  types; ie: reconstructed with either resolvables or concrete values in place of the first stage
  address.

  Two affordances are made in type constraint handling:

  1. Either a :class:`TypeConstraint` instance can be given if the type constraint is fully known or
     else a type constraint class can be given if the type constraint should apply to the type of
     the enclosing class.  This is useful for declaring an addressable property in a baseclass that
     should be type-constrained based on the type of the derived class.
  2. Decorators for addressables (see `addressable`, `addressable_list` and `addressable_dict`)
     allow wrapping of either class functions - typical - or @property descriptors.  The property
     descriptor case sets up an idiom for recursive addressables.  The idiom looks like:

     >>> class Thing(Struct):
     ...   def __init__(self, thing):
     ...     super(Thing, self).__init__()
     ...     self.thing = thing
     ...   @property
     ...   def parent(self):
     ...     '''Return this thing's parent.
     ...
     ...     :rtype: :class:`Thing`
     ...     '''
     ...
     >>> Thing.parent = addressable(Exactly(Thing))(Thing.parent)

     Here the `Thing.parent` property is re-assigned with a type-constrained addressable descriptor
     after the class is defined so the class can be referred to in the type constraint.
  """
  _descriptors = set()

  @classmethod
  def is_addressable(cls, obj, key):
    """Return `True` if the given attribute of `obj` is an addressable attribute.

    :param obj: The object to inspect.
    :param string key: The name of the property on `obj` to check.
    """
    return (type(obj), key) in cls._descriptors

  @classmethod
  def _register(cls, obj, descriptor):
    cls._descriptors.add((type(obj), descriptor._name))

  def __init__(self, name, type_constraint):
    self._name = name
    self._type_constraint = type_constraint

  def __set__(self, instance, value):
    if not Serializable.is_serializable(instance):
      raise NotSerializableError('The addressable descriptor {} can only be applied to methods or '
                                 'properties of Serializable objects, applied to method {} of '
                                 'type {}'.format(type(self).__name__,
                                                  self._name,
                                                  type(instance).__name__))

    instance_dict = instance._asdict()
    if self._name in instance_dict:
      raise MutationError('Attribute {} of {} has already been set to {}, rejecting attempt to '
                          're-set with {}'.format(self._name,
                                                  instance,
                                                  instance_dict[self._name],
                                                  value))

    value = self._checked_value(instance, value)

    self._register(instance, self)

    # We mutate the instance dict, which is only OK if used in the conventional idiom of setting
    # the value via this data descriptor in the instance's constructor.
    instance_dict[self._name] = value

  def __get__(self, instance, unused_owner_type=None):
    # We know instance is a Serializable from the type-checking done in set.
    value = instance._asdict()[self._name]
    return self._resolve_value(instance, value)

  def _get_type_constraint(self, instance):
    if inspect.isclass(self._type_constraint):
      return self._type_constraint(type(instance))
    else:
      return self._type_constraint

  def _checked_value(self, instance, value):
    # We allow five forms of value:
    # 0. None.
    # 1. An opaque (to us) address pointing to a value that can be resolved by external
    #    means.
    # 2. A `Resolvable` value that we can lazily resolve and type-check in `__get__`.
    # 3. A concrete instance that meets our type constraint.
    # 4. A dict when our type constraint has exactly one Serializable subject type - we convert the
    #    dict into an instance of that type.
    if value is None:
      return None

    if isinstance(value, (six.string_types, Address, Resolvable)):
      return value

    # Support untyped dicts that we deserialize on-demand here into the required type.
    # This feature allows for more brevity in the JSON form (local type inference) and an alternate
    # construction style in the python forms.
    type_constraint = self._get_type_constraint(instance)
    if (isinstance(value, dict) and
        len(type_constraint.types) == 1 and
        Serializable.is_serializable_type(type_constraint.types[0])):
      if not value:
        # TODO(John Sirois): Is this the right thing to do?  Or should an empty serializable_type
        # be constructed?
        return None  # {} -> None.
      else:
        serializable_type = type_constraint.types[0]
        return serializable_type(**value)

    if not type_constraint.satisfied_by(value):
      raise TypeConstraintError('Got {} of type {} for {} attribute of {} but expected {!r}'
                                .format(value,
                                        type(value).__name__,
                                        self._name,
                                        instance,
                                        type_constraint))
    return value

  def _resolve_value(self, instance, value):
    if not isinstance(value, Resolvable):
      # The value is concrete which means we type-checked on set so no need to do so again, its a
      # raw address string or an instance that satisfies our type constraint.
      return value
    else:
      resolved_value = value.resolve()
      type_constraint = self._get_type_constraint(instance)
      if not type_constraint.satisfied_by(resolved_value):
        raise TypeConstraintError('The value resolved from {} did not meet the type constraint of '
                                  '{!r} for the {} property of {}: {}'
                                  .format(value.address,
                                          type_constraint,
                                          self._name,
                                          instance,
                                          resolved_value))
      return resolved_value


def _addressable_wrapper(addressable_descriptor, type_constraint):
  def wrapper(func):
    # We allow for wrapping property objects to support the following idiom for defining recursive
    # addressables:
    #
    # class Thing(Struct):
    #   def __init__(self, thing):
    #      super(Thing, self).__init__()
    #      self.thing = thing
    #
    #   @property
    #   def parent(self):
    #     """Return this thing's parent.
    #
    #     :rtype: :class:`Thing`
    #     """"
    #
    # Thing.parent = addressable(Exactly(Thing))(Thing.parent)
    func = func.fget if isinstance(func, property) else func

    addressable_accessor = addressable_descriptor(func.__name__, type_constraint)
    return update_wrapper(addressable_accessor, func)
  return wrapper


def addressable(type_constraint):
  """Return an addressable attribute for Serializable classes.

  The attribute should have no implementation (it will be ignored), but can carry a docstring.
  The implementation is provided by this wrapper.  Idiomatic use assigns the value, which can
  either be an opaque address string or a resolved value that meets the type constraint, in the
  constructor::

  >>> class Employee(Serializable):
  ...   def __init__(self, person):
  ...     self.person = person
  ...   @addressable(SubclassesOf(Person))
  ...   def person(self):
  ...     '''The person that is this employee.'''

  Addressable attributes are only assignable once, so this pattern yields an immutable `Employee`
  whose `person` attribute is either a `Person` instance or
  :class:`pants.engine.objects.Resolvable` person or else a string address pointing to one.

  See :class:`AddressableDescriptor` for more details.

  :param type_constraint: The type constraint the value must satisfy.
  :type type_constraint: :class:`TypeConstraint`
  """
  return _addressable_wrapper(AddressableDescriptor, type_constraint)


class AddressableList(AddressableDescriptor):
  def _checked_value(self, instance, value):
    if value is None:
      return None

    if not isinstance(value, collections.MutableSequence):
      raise TypeError('The {} property of {} must be a list, given {} of type {}'
                      .format(self._name, instance, value, type(value).__name__))
    return [super(AddressableList, self)._checked_value(instance, v) for v in value]

  def _resolve_value(self, instance, value):
    return [super(AddressableList, self)._resolve_value(instance, v)
            for v in value] if value else []


def addressable_list(type_constraint):
  """Marks a list's values as satisfying a given type constraint.

  Some (or all) elements of the list may be :class:`pants.engine.objects.Resolvable` elements
  to resolve later.

  See :class:`AddressableDescriptor` for more details.

  :param type_constraint: The type constraint the list's values must all satisfy.
  :type type_constraint: :class:`TypeConstraint`
  """
  return _addressable_wrapper(AddressableList, type_constraint)


class AddressableDict(AddressableDescriptor):
  def _checked_value(self, instance, value):
    if value is None:
      return None

    if not isinstance(value, collections.MutableMapping):
      raise TypeError('The {} property of {} must be a dict, given {} of type {}'
                      .format(self._name, instance, value, type(value).__name__))
    return {k: super(AddressableDict, self)._checked_value(instance, v) for k, v in value.items()}

  def _resolve_value(self, instance, value):
    return {k: super(AddressableDict, self)._resolve_value(instance, v)
            for k, v in value.items()} if value else {}


def addressable_dict(type_constraint):
  """Marks a dicts's values as satisfying a given type constraint.

  Some (or all) values in the dict may be :class:`pants.engine.objects.Resolvable` values to
  resolve later.

  See :class:`AddressableDescriptor` for more details.

  :param type_constraint: The type constraint the dict's values must all satisfy.
  :type type_constraint: :class:`TypeConstraint`
  """
  return _addressable_wrapper(AddressableDict, type_constraint)


# TODO(John Sirois): Move variants into Address 1st class as part of merging the engine/exp
# into the mainline (if they survive).
# TODO: Variants currently require an explicit name (and thus a `:`) in order to parse correctly.
def strip_variants(address):
  """Return a copy of the given address with the variants (if any) stripped from the name.

  :rtype: :class:`pants.build_graph.address.Address`
  """
  address, _ = parse_variants(address)
  return address


def _extract_variants(address, variants_str):
  """Return the variants (if any) represented by the given variants_str.

  :returns: The variants or else `None` if there are none.
  :rtype: tuple of tuples (key, value) strings
  """
  def entries():
    for entry in variants_str.split(','):
      key, _, value = entry.partition('=')
      if not key or not value:
        raise ValueError('Invalid variants after the @ in: {}'.format(address))
      yield (key, value)
  return tuple(entries())


def parse_variants(address):
  target_name, _, variants_str = address.target_name.partition('@')
  variants = _extract_variants(address, variants_str) if variants_str else None
  normalized_address = Address(spec_path=address.spec_path, target_name=target_name)
  return normalized_address, variants
