"""
Provide the place entity.
"""

from __future__ import annotations

from contextlib import suppress
from typing import TYPE_CHECKING, final, override

from betty.ancestry.has_file_references import HasFileReferences
from betty.ancestry.has_links import HasLinks
from betty.ancestry.has_notes import HasNotes
from betty.ancestry.name import Name
from betty.json.linked_data import JsonLdObject, dump_context
from betty.json.schema import Array, Number, Object
from betty.locale.localizable.gettext import _, ngettext
from betty.model import EntityDefinition
from betty.model.association import BidirectionalToManySingleType, ToManyAssociates
from betty.place_type.place_types import Unknown as UnknownPlaceType
from betty.privacy import HasPrivacy

if TYPE_CHECKING:
    from collections.abc import MutableSequence

    from geopy import Point

    from betty.ancestry.enclosure import Enclosure
    from betty.ancestry.event import Event
    from betty.ancestry.link import Link
    from betty.ancestry.note import Note
    from betty.locale.localizable import Localizable
    from betty.place_type import PlaceType
    from betty.portable import PortableMapping
    from betty.privacy import Privacy
    from betty.project import Project


@final
@EntityDefinition(
    "place",
    label=_("Place"),
    label_plural=_("Places"),
    label_countable=ngettext("{count} place", "{count} places"),
)
class Place(HasLinks, HasFileReferences, HasNotes, HasPrivacy):
    """
    .. plugin:: entity:place.
    """

    events = BidirectionalToManySingleType["Place", "Event"](
        "betty.ancestry.event:Event",
        "place",
        label=_("Events"),
        description=_("The events that happened in this place"),
    )
    """
    The events that happened here.
    """

    enclosers = BidirectionalToManySingleType["Place", "Enclosure"](
        "betty.ancestry.enclosure:Enclosure",
        "enclosee",
        label=_("Enclosers"),
        description=_("The places this place is enclosed or contained by"),
        linked_data_embedded=True,
    )
    """
    Other places containing this one.
    """

    enclosees = BidirectionalToManySingleType["Place", "Enclosure"](
        "betty.ancestry.enclosure:Enclosure",
        "encloser",
        label=_("Enclosees"),
        description=_("The places this place encloses or contains"),
        linked_data_embedded=True,
    )
    """
    Other places contained by this one.
    """

    def __init__(
        self,
        *,
        id: str | None = None,  # noqa: A002
        names: MutableSequence[Name] | None = None,
        events: ToManyAssociates[Event] | None = None,
        enclosers: ToManyAssociates[Enclosure] | None = None,
        enclosees: ToManyAssociates[Enclosure] | None = None,
        notes: ToManyAssociates[Note] | None = None,
        coordinates: Point | None = None,
        links: MutableSequence[Link] | None = None,
        privacy: Privacy | None = None,
        place_type: PlaceType | None = None,
    ):
        super().__init__(id, notes=notes, links=links, privacy=privacy)
        self._names = [] if names is None else names
        self._coordinates = coordinates
        if events is not None:
            self.events = events
        if enclosers is not None:
            self.enclosers = enclosers
        if enclosees is not None:
            self.enclosees = enclosees
        self._place_type = place_type or UnknownPlaceType()

    @property
    def place_type(self) -> PlaceType:
        """
        The type of this place.
        """
        return self._place_type

    @place_type.setter
    def place_type(self, place_type: PlaceType) -> None:
        self._place_type = place_type

    @property
    def names(self) -> MutableSequence[Name]:
        """
        The place's names.

        The first name is considered the :py:attr:`place label <betty.ancestry.place.Place.label>`.
        """
        return self._names

    @property
    def coordinates(self) -> Point | None:
        """
        The place's coordinates.
        """
        return self._coordinates

    @coordinates.setter
    def coordinates(self, coordinates: Point):
        self._coordinates = coordinates

    @override
    @property
    def label(self) -> Localizable:
        with suppress(IndexError):
            return self.names[0].name
        return super().label

    @override
    async def dump_linked_data(self, project: Project, /) -> PortableMapping:
        portable = await super().dump_linked_data(project)
        dump_context(
            portable,
            names="https://schema.org/name",
            events="https://schema.org/event",
            enclosers="https://schema.org/containedInPlace",
            enclosees="https://schema.org/containsPlace",
        )
        portable["@type"] = "https://schema.org/Place"
        portable["names"] = [
            await name.dump_linked_data(project) for name in self.names
        ]
        if self.coordinates is not None:
            portable_coordinates: PortableMapping = {
                "@type": "https://schema.org/GeoCoordinates",
                "latitude": self.coordinates.latitude,
                "longitude": self.coordinates.longitude,
            }
            dump_context(portable, coordinates="https://schema.org/geo")
            dump_context(portable_coordinates, latitude="https://schema.org/latitude")
            dump_context(portable_coordinates, longitude="https://schema.org/longitude")
            portable["coordinates"] = portable_coordinates
        return portable

    @override
    @classmethod
    async def linked_data_schema(cls, project: Project, /) -> JsonLdObject:
        schema = await super().linked_data_schema(project)
        schema.add_property(
            "names", Array(await Name.linked_data_schema(project), title="Names")
        )
        coordinate_schema = Number(title="Coordinate")
        coordinates_schema = Object(title="Coordinates")
        coordinates_schema.add_property("latitude", coordinate_schema, False)
        coordinates_schema.add_property("longitude", coordinate_schema, False)
        schema.add_property("coordinates", coordinates_schema, False)
        return schema
