#!/usr/bin/env python
#
#  annotations.py
"""
Add serialisation and deserialisation capability to attrs_ classes.

.. _attrs: https://www.attrs.org/en/stable/

Based on `attrs-serde <https://github.com/jondot/attrs-serde>`_.

Example usage
--------------

.. code-block:: python

	>>> import attr
	>>> from attr_utils.serialise import serde

	>>> person_dict = {"contact": {"personal": {"name": "John"}, "phone": "555-112233"}}

	>>> name_path = ["contact", "personal", "name"]
	>>> phone_path = ["contact", "phone"]

	>>> @serde
	... @attr.s
	... class Person(object):
	... 	name = attr.ib(metadata={"to": name_path, "from": name_path})
	... 	phone = attr.ib(metadata={"to": phone_path, "from": phone_path})

	>>> p = Person.from_dict(person_dict)
	Person(name=John phone=555-112233)

	>>> p.to_dict
	{"contact": {"personal": {"name": "John"}, "phone": "555-112233"}}


API Reference
---------------
"""

#  From https://github.com/jondot/attrs-serde
#  MIT License
#
#  Copyright (c) 2019 Dotan Nahum
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#  SOFTWARE.
#

# stdlib
from typing import TYPE_CHECKING, Any, Callable, Mapping, MutableMapping, Optional, Type, Union, overload

try:
	# 3rd party
	from cytoolz.curried import filter, get_in, map, pipe, reduce, update_in  # type: ignore
except ImportError:
	from toolz.curried import get_in, update_in, reduce, pipe, map, filter  # type: ignore

# 3rd party
from attr import asdict, fields

__all__ = ["serde"]

if TYPE_CHECKING:
	AttrsClass = Any
else:
	# this package
	from attr_utils.utils import AttrsClass


@overload
def serde(
		cls: Type,
		from_key: str = ...,
		to_key: str = ...,
		) -> Type[AttrsClass]:
	...  # pragma: no cover


@overload
def serde(
		cls: None = None,
		from_key: str = ...,
		to_key: str = ...,
		) -> Callable[[Type[AttrsClass]], Type[AttrsClass]]:
	...  # pragma: no cover


def serde(
		cls: Optional[Type[AttrsClass]] = None,
		from_key: str = "from",
		to_key: str = "to",
		) -> Union[Type[AttrsClass], Callable[[Type[AttrsClass]], Type[AttrsClass]]]:
	r"""
	Decorator to add serialisation and deserialisation capabilities to attrs classes.

	The keys used in the dictionary output, and used when creating the class from a dictionary,
	can be controlled using the ``metadata`` argument in :func:`attr.ib`:

	.. code-block::

		from attr_utils.serialize import serde
		import attr

		@serde
		@attr.s
		class Person(object):
			name = attr.ib(metadata={"to": name_path, "from": name_path})
			phone = attr.ib(metadata={"to": phone_path, "from": phone_path})

	The names of the keys given in the ``metadata`` argument can be controlled with the
	``from_key`` and ``to_key`` arguments:

	.. code-block::

		from attr_utils.serialize import serde
		import attr

		@serde(from_key="get", to_key="set")
		@attr.s
		class Person(object):
			name = attr.ib(metadata={"get": name_path, "set": name_path})
			phone = attr.ib(metadata={"get": phone_path, "set": phone_path})


	This may be required when using other extensions to attrs.


	:param cls: The attrs class to add the methods to.
	:param from_key:
	:param to_key:

	:rtype:

	Classes decorated with :deco:`~attr_utils.serialise.serde` will have two new methods added:

	.. py:classmethod:: from_dict(d)

		Construct an instance of the class from a dictionary.

		:param d: The dictionary
		:type d: :class:`~typing.Mapping`\[:class:`str`, :py:obj:`~typing.Any`\]

	.. py:method:: to_dict(convert_values=False):

		Returns a dictionary containing the contents of the class.

		:param convert_values: Recurse into other attrs classes, and convert tuples, sets etc.
			into lists. This may be required to later construct a new class from the
			dictionary if the class uses complex converter functions.
		:type convert_values: :class:`bool`

		:rtype: :class:`~typing.MutableMapping`\[:class:`str`, :py:obj:`~typing.Any`\]

		.. versionchanged:: 0.5.0

			By default values are left unchanged. In version 0.4.0 these were converted
			to basic Python types, which may be undesirable. The original behaviour can be
			restored using the ``convert_values`` parameter.

	"""

	def serde_with_class(cls: Type[AttrsClass]) -> Type[AttrsClass]:

		def from_dict(cls, d: Mapping[str, Any]):
			from_fields = list(map(lambda a: (a, get_in([from_key], a.metadata, [a.name])), fields(cls)))

			return cls(**dict(map(
					lambda f: (f[0].name, get_in(f[1], d, f[0].default)),
					from_fields,
					)))

		def to_dict(self, convert_values: bool = False) -> MutableMapping[str, Any]:
			to_fields = pipe(
					fields(self.__class__),
					map(lambda a: (a, get_in([to_key], a.metadata))),
					filter(lambda f: f[1]),
					list,
					)

			if convert_values:
				d = asdict(self)
			else:
				d = {a.name: getattr(self, a.name) for a in fields(self.__class__)}

			if not to_fields:
				return d

			return reduce(
					lambda acc, f: update_in(acc, f[1], lambda _: d[f[0].name]),
					to_fields,
					{},
					)

		from_dict.__doc__ = f"""
		Construct an instance of :class:`~.{cls.__name__}` from a dictionary.

		:param d: The dictionary
		"""
		from_dict.__qualname__ = f"{cls.__name__}.from_dict"
		from_dict.__module__ = cls.__module__
		cls.from_dict = classmethod(from_dict)

		to_dict.__doc__ = f"""
Returns a dictionary containing the contents of the :class:`~.{cls.__name__}` object.

:param convert_values: Recursively convert values into dictionaries, lists etc. as appropriate.
"""
		to_dict.__qualname__ = f"{cls.__name__}.to_dict"
		to_dict.__module__ = cls.__module__
		cls.to_dict = to_dict

		return cls

	if cls is not None:
		return serde_with_class(cls)
	else:
		return serde_with_class
