"""ENTITIES
Definition of classes & typing definitions
"""

# # Native # #
from typing import List, Optional, Union, Dict
from datetime import datetime, timedelta, tzinfo

# # Installed # #
import pydantic

# # Package # #
from .helpers import *

__all__ = ("Stop", "Bus", "BusTime", "Buses", "Stops", "BusesResult")


BusTime = Optional[Union[int, float, datetime, timedelta]]
"""Typing for the Time remaining or scheduled arrival/departure of a Bus. 
Can be an absolute datetime object, or the relative time as int or float
"""

Timestamp = Optional[Union[str, int, float, datetime]]
"""Typing for timestamps used on Stop created/updated values. 
Should be an absolute timestamp in any format prefered: datetime, str representation, or int/float epoch timestamp"""


class BaseEntity(pydantic.BaseModel):
    """Base Entity for all the PyBuses Entities
    """

    def get_dict(
            self,
            remove_none: bool = True,
            remove_empty_strings: bool = True,
            remove_empty_lists: bool = True,
            remove_empty_dicts: bool = True
    ) -> Dict:
        """Return the dict representation of the current object, with value cleaning.
        :param remove_none: if True, remove items with null/None value (default: True)
        :param remove_empty_strings: if True, remove empty-string items (default: True)
        :param remove_empty_lists: if True, remove empty lists (default: True)
        :param remove_empty_dicts: if True, remove empty dicts (default: True)
        """
        return clear_dict_values(dict(self), remove_none, remove_empty_strings, remove_empty_lists, remove_empty_dicts)

    @staticmethod
    def get_sort_key(attribute_name: str):
        """Get a sorting key based on the given attribute name
        """
        return lambda inst: inst.__getattr__(attribute_name)


class Stop(BaseEntity):
    """Stop to where Buses will arrive:
    - stopid: ID or code number of this Stop (required, int only)
    - name: canonical name of this stop (required)
    - lat, lon: location of this stop (latitude & longitude)
    """
    stopid: int
    name: str
    lat: Optional[float]
    lon: Optional[float]

    def has_location(self) -> bool:
        """Return True if this Stop has a location defined
        """
        return (self.lat is not None) and (self.lon is not None)


Stops = List[Stop]
"""Typing for a List of Stops. Usually a list of stops for a certain static bus line-route definition"""


class Bus(BaseEntity):
    """Vehicle that arrive to a Stop at or in a certain time:
    - busid: autogenerated if not specified, based on line and route
    - line: Bus line (required)
    - route: Bus route (required)
    - time: generic absolute or relative time, usually until arrival to the Stop.
            To differentiate between arrival and departure times, use the AdvancedBus
            with the arrival and departure attributes.
    """
    busid: Optional[str]
    line: str
    route: str
    time: BusTime

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.busid is None:
            self.busid = generate_busid(self.line, self.route)

    def relative_time(self, timezone: Optional[tzinfo]) -> timedelta:
        """Get the time difference between current time and the 'time' of this Bus (which must be a relative datetime).
        A custom timezone can be used for the current time, otherwise default 'datetime' parameter is system timezone
        """
        return calculate_relative_time(self.time, timezone)


Buses = List[Bus]
"""Typing for List (Array) of Buses. Usually a list of buses that will come to a Stop"""


class BusesResult(BaseEntity):
    """A contextualized Result of Buses Query, with useful fields to get more context of the result:
    - buses: List of Buses (default: empty list)
    - more_buses_available: True if more buses were available on the data source, but not all of them were returned
                            on the current query
    - stop: optional Stop that can be appended to a Buses Result Query, when the data source returns Stop info
            alongside the Buses list
    - source: name of the data source this data was fetched from
    """
    buses: Buses = []
    stop: Optional[Stop]
    more_buses_available: Optional[bool]
    source: Optional[str]
