#!/usr/bin/env python3
#
#  fields.py
"""
Primitive field types.
"""
#
#  Copyright © 2023 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  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 Any, Generic, Type, TypeVar

# 3rd party
import attrs  # nodep

__all__ = (
		"Boolean",
		"FieldType",
		"Integer",
		"Number",
		"String",
		)

_FT = TypeVar("_FT")


class FieldType(Generic[_FT]):
	"""
	Customisable config field type.
	"""

	#: The Python type of the object.
	field_type: Type[_FT]

	#: String name of the field.
	field_name: str

	def __new__(cls, default: _FT) -> _FT:  # type: ignore[misc]
		"""
		The wrong way to construct an attrs field.

		:param default:
		"""

		# TODO: emit warning

		return cls.field(default=default)

	@classmethod
	def validator(  # noqa: PRM002
		cls: Type["FieldType"],
		inst: object,
		attr: attrs.Attribute,
		value: Any,
	) -> None:
		"""
		Check if a value conforms to this field's expected datatype.

		This method is called by attrs.
		"""

		if not isinstance(value, cls.field_type):  # pragma: no cover
			err_msg = f"'{attr.name}' must be {cls.field_name} (got {value!r} that is a {value.__class__!r})."
			raise TypeError(err_msg)

	@classmethod
	def on_setattr(  # noqa: PRM002
		cls: Type["FieldType"],
		inst: object,
		attr: attrs.Attribute,
		value: Any,
	) -> None:
		"""
		Converts values on ``__setattr__``.

		This method is called by attrs.
		"""

		return cls.field_type(value)

	@classmethod
	def field(cls: Type["FieldType"], default: _FT) -> _FT:
		"""
		Construct an attrs field.

		:param default:
		"""

		# Actually returns attr.Attribute, but mypy doesn't like it

		return attrs.field(
				default=default,
				converter=cls.field_type,
				validator=cls.validator,
				on_setattr=cls.on_setattr,
				)


# TODO: sequence types


class Boolean(FieldType):
	"""
	Boolean config field type.
	"""

	field_type = bool
	field_name = "boolean"


class String(FieldType):
	"""
	String config field type.
	"""

	field_type = str
	field_name = "a string"


class Integer(FieldType):
	"""
	Integer config field type.
	"""

	field_type = int
	field_name = "an integer"


class Number(FieldType):
	"""
	Numerical config field type.
	"""

	field_type = float
	field_name = "a number"

	@classmethod
	def validator(  # noqa: PRM002
		cls: Type["FieldType"],
		inst: object,
		attr: attrs.Attribute,
		value: Any,
	) -> None:
		"""
		Check if a value is a number.

		This method is called by attrs.
		"""

		if not isinstance(value, (int, float)):  # pragma: no cover
			err_msg = f"'{attr.name}' must be {cls.field_name} (got {value!r} that is a {value.__class__!r})."
			raise TypeError(err_msg)
