# 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 inspect
from abc import abstractmethod, abstractproperty
from collections import namedtuple

from pants.util.meta import AbstractClass


class SerializationError(Exception):
  """Indicates an error serializing an object."""


# TODO: Likely no longer necessary, due to the laziness of the product graph.
class Resolvable(AbstractClass):
  """Represents a resolvable object."""

  @abstractproperty
  def address(self):
    """Return the opaque address descriptor that this resolvable resolves."""

  @abstractmethod
  def resolve(self):
    """Resolve and return the resolvable object."""


def _unpickle_serializable(serializable_class, kwargs):
  # A pickle-compatible top-level function for custom unpickling of Serializables.
  return serializable_class(**kwargs)


class Locatable(AbstractClass):
  """Marks a class whose constructor should receive its spec_path relative to the build root.

  Locatable objects will be passed a `spec_path` constructor kwarg that indicates where they
  were parsed. If the object also has a `name` (not all do), then these two fields can be
  combined into an Address.
  """


class SerializablePickle(namedtuple('CustomPickle', ['unpickle_func', 'args'])):
  """A named tuple to help the readability of the __reduce__ protocol.

  See: https://docs.python.org/2.7/library/pickle.html#pickling-and-unpickling-extension-types
  """

  @classmethod
  def create(cls, serializable_instance):
    """Return a tuple that implements the __reduce__ pickle protocol for serializable_instance."""
    if not Serializable.is_serializable(serializable_instance):
      raise ValueError('Can only create pickles for Serializable objects, given {} of type {}'
                       .format(serializable_instance, type(serializable_instance).__name__))
    return cls(unpickle_func=_unpickle_serializable,
               args=(type(serializable_instance), serializable_instance._asdict()))


class Serializable(AbstractClass):
  """Marks a class that can be serialized into and reconstituted from python builtin values.

  Also provides support for the pickling protocol out of the box.
  """

  @staticmethod
  def is_serializable(obj):
    """Return `True` if the given object conforms to the Serializable protocol.

    :rtype: bool
    """
    return isinstance(obj, Serializable) or (not inspect.isclass(obj) and hasattr(obj, '_asdict'))

  @staticmethod
  def is_serializable_type(type_):
    """Return `True` if the given type's instances conform to the Serializable protocol.

    :rtype: bool
    """
    return issubclass(type_, Serializable) or (inspect.isclass(type_) and hasattr(type_, '_asdict'))

  @abstractmethod
  def _asdict(self):
    """Return a dict mapping this class' properties.

    To meet the contract of a serializable the constructor must accept all the properties returned
    here as constructor parameters; ie the following must be true::

    >>> s = Serializable(...)
    >>> Serializable(**s._asdict()) == s

    Additionally the dict must also contain nothing except Serializables, python primitive values,
    ie: dicts, lists, strings, numbers, bool values, etc or Resolvables that resolve to Serilizables
    or primitive values.

    Any :class:`collections.namedtuple` satisfies the Serializable contract automatically via duck
    typing if it is composed of only primitive python values or Serializable values.
    """

  def __reduce__(self):
    # We implement __reduce__ to steer the pickling process away from __getattr__ scans.  This is
    # both more direct - we know where our instance data lives - and it notably allows __getattr__
    # implementations by Serializable subclasses.  Without the __reduce__, __getattr__ is rendered
    # unworkable since it causes pickle failures.
    # See the note at the bottom of this section:
    # https://docs.python.org/2.7/library/pickle.html#pickling-and-unpickling-normal-class-instances
    return SerializablePickle.create(self)


class SerializableFactory(AbstractClass):
  """Creates :class:`Serializable` objects."""

  @abstractmethod
  def create(self):
    """Return a serializable object.

    :rtype: :class:`Serializable`
    """


class ValidationError(Exception):
  """Indicates invalid fields on an object."""

  def __init__(self, identifier, message):
    """Creates a validation error pertaining to the identified invalid object.

    :param object identifier: Any object whose string representation identifies the invalid object
                              that led to this validation error.
    :param string message: A message describing the invalid Struct field.
    """
    super(ValidationError, self).__init__('Failed to validate {id}: {msg}'
                                          .format(id=identifier, msg=message))


class Validatable(AbstractClass):
  """Marks a class whose instances should validated post-construction."""

  @abstractmethod
  def validate(self):
    """Check that this object's fields are valid.

    :raises: :class:`ValidationError` if this object is invalid.
    """


class Closable(object):
  """Marks a class that cleans up itself, usually at the end of lifecycle."""

  def close(self):
    """A no-op implementation, overwrite to actually clean up resources etc."""
    return
