import re
from collections import OrderedDict
from typing import List

import bs4

from tibiapy import abc
from tibiapy.character import OnlineCharacter
from tibiapy.enums import PvpType, TournamentWorldType, TransferType, WorldLocation, BattlEyeType
from tibiapy.errors import InvalidContent
from tibiapy.utils import get_tibia_url, parse_integer, parse_tibia_datetime, parse_tibia_full_date, \
    parse_tibiacom_content, try_date, try_datetime, try_enum

__all__ = (
    "ListedWorld",
    "World",
    "WorldOverview",
)

record_regexp = re.compile(r'(?P<count>[\d.,]+) players \(on (?P<date>[^)]+)\)')
battleye_regexp = re.compile(r'since ([^.]+).')


class ListedWorld(abc.BaseWorld, abc.Serializable):
    """Represents a game server listed in the World Overview section.

    Attributes
    ----------
    name: :class:`str`
        The name of the world.
    status: :class:`str`
        The current status of the world.
    online_count: :class:`int`
        The number of currently online players in the world.
    location: :class:`WorldLocation`
        The physical location of the game servers.
    pvp_type: :class:`PvpType`
        The type of PvP in the world.
    transfer_type: :class:`TransferType`
        The type of transfer restrictions this world has.
    battleye_date: :class:`datetime.date`
        The date when BattlEye was added to this world.
        If this is :obj:`None` and the world is protected, it means the world was protected from the beginning.
    battleye_type: :class:`BattlEyeType`
        The type of BattlEye protection this world has.

        .. versionadded:: 4.0.0
    experimental: :class:`bool`
        Whether the world is experimental or not.
    premium_only: :class:`bool`
        Whether only premium account players are allowed to play in this server.
    tournament_world_type: :class:`TournamentWorldType`
        The type of tournament world. :obj:`None` if this is not a tournament world.
    """
    __slots__ = (
        "name",
        "status",
        "location",
        "online_count",
        "pvp_type",
        "battleye_date",
        "battleye_type",
        "experimental",
        "premium_only",
        "tournament_world_type",
        "transfer_type"
    )

    _serializable_properties = (
        "battleye_protected",
    )

    def __init__(self, name, location=None, pvp_type=None, **kwargs):
        self.name: str = name
        self.location = try_enum(WorldLocation, location)
        self.pvp_type = try_enum(PvpType, pvp_type)
        self.status: str = kwargs.get("status")
        self.online_count: int = kwargs.get("online_count", 0)
        self.transfer_type = try_enum(TransferType, kwargs.get("transfer_type", TransferType.REGULAR))
        self.battleye_date = try_date(kwargs.get("battleye_date"))
        self.battleye_type = try_enum(BattlEyeType, kwargs.get("battleye_type"), BattlEyeType.UNPROTECTED)
        self.experimental: bool = kwargs.get("experimental", False)
        self.premium_only: bool = kwargs.get("premium_only", False)
        self.tournament_world_type = try_enum(TournamentWorldType, kwargs.get("tournament_world_type"), None)

    @property
    def battleye_protected(self):
        """:class:`bool`: Whether the server is currently protected with BattlEye or not.

        .. versionchanged:: 4.0.0
            Now a calculated property instead of a field.
        """
        return self.battleye_type and self.battleye_type != BattlEyeType.UNPROTECTED

    # region Public methods
    @classmethod
    def get_list_url(cls):
        """
        Gets the URL to the World Overview page in Tibia.com

        Returns
        -------
        :class:`str`
            The URL to the World Overview's page.
        """
        return WorldOverview.get_url()

    @classmethod
    def list_from_content(cls, content):
        """Parses the content of the World Overview section from Tibia.com and returns only the list of worlds.

        Parameters
        ----------
        content: :class:`str`
            The HTML content of the World Overview page in Tibia.com

        Returns
        -------
        :class:`list` of :class:`ListedWorld`
            A list of the worlds and their current information.

        Raises
        ------
        InvalidContent
            If the provided content is not the HTML content of the worlds section in Tibia.com
        """
        world_overview = WorldOverview.from_content(content)
        return world_overview.worlds

    # endregion

    # region Private methods
    def _parse_additional_info(self, additional_info, tournament=False):
        if "blocked" in additional_info:
            self.transfer_type = TransferType.BLOCKED
        elif "locked" in additional_info:
            self.transfer_type = TransferType.LOCKED
        else:
            self.transfer_type = TransferType.REGULAR
        self.experimental = "experimental" in additional_info
        self.premium_only = "premium" in additional_info
        if tournament:
            if "restricted Store products" in additional_info:
                self.tournament_world_type = TournamentWorldType.RESTRICTED
            else:
                self.tournament_world_type = TournamentWorldType.REGUlAR
    # endregion


class World(abc.BaseWorld, abc.Serializable):
    """Represents a Tibia game server.


    Attributes
    ----------
    name: :class:`str`
        The name of the world.
    status: :class:`str`
        The current status of the world.
    online_count: :class:`int`
        The number of currently online players in the world.
    record_count: :class:`int`
        The server's online players record.
    record_date: :class:`datetime.datetime`
        The date when the online record was achieved.
    location: :class:`WorldLocation`
        The physical location of the game servers.
    pvp_type: :class:`PvpType`
        The type of PvP in the world.
    creation_date: :class:`str`
        The month and year the world was created. In YYYY-MM format.
    transfer_type: :class:`TransferType`
        The type of transfer restrictions this world has.
    world_quest_titles: :obj:`list` of :class:`str`
        List of world quest titles the server has achieved.
    battleye_date: :class:`datetime.date`
        The date when BattlEye was added to this world.
        If this is :obj:`None` and the world is protected, it means the world was protected from the beginning.
    battleye_type: :class:`BattlEyeType`
        The type of BattlEye protection this world has.

        .. versionadded:: 4.0.0
    experimental: :class:`bool`
        Whether the world is experimental or not.
    tournament_world_type: :class:`TournamentWorldType`
        The type of tournament world. :obj:`None` if this is not a tournament world.
    online_players: :obj:`list` of :class:`OnlineCharacter`.
        A list of characters currently online in the server.
    premium_only: :class:`bool`
        Whether only premium account players are allowed to play in this server.
    """
    __slots__ = (
        "name",
        "status",
        "location",
        "pvp_type",
        "battleye_date",
        "battleye_type",
        "experimental",
        "premium_only",
        "tournament_world_type",
        "transfer_type",
        "online_count",
        "record_count",
        "record_date",
        "creation_date",
        "world_quest_titles",
        "online_players"
    )

    _serializable_properties = (
        "battleye_protected",
    )

    def __init__(self, name, location=None, pvp_type=None, **kwargs):
        self.name: str = name
        self.location = try_enum(WorldLocation, location)
        self.pvp_type = try_enum(PvpType, pvp_type)
        self.status: bool = kwargs.get("status")
        self.online_count: int = kwargs.get("online_count", 0)
        self.record_count: int = kwargs.get("record_count", 0)
        self.record_date = try_datetime(kwargs.get("record_date"))
        self.creation_date: str = kwargs.get("creation_date")
        self.transfer_type = try_enum(TransferType, kwargs.get("transfer_type", TransferType.REGULAR))
        self.world_quest_titles: List[str] = kwargs.get("world_quest_titles", [])
        self.battleye_date = try_date(kwargs.get("battleye_date"))
        self.battleye_type = try_enum(BattlEyeType, kwargs.get("battleye_type"), BattlEyeType.UNPROTECTED)
        self.experimental: bool = kwargs.get("experimental", False)
        self.online_players: List[OnlineCharacter] = kwargs.get("online_players", [])
        self.premium_only: bool = kwargs.get("premium_only", False)
        self.tournament_world_type = try_enum(TournamentWorldType, kwargs.get("tournament_world_type"), None)

    # region Properties
    @property
    def battleye_protected(self):
        """:class:`bool`: Whether the server is currently protected with BattlEye or not.

        .. versionchanged:: 4.0.0
            Now a calculated property instead of a field.
        """
        return self.battleye_type and self.battleye_type != BattlEyeType.UNPROTECTED

    @property
    def creation_year(self):
        """:class:`int`: Returns the year when the world was created."""
        return int(self.creation_date.split("-")[0]) if self.creation_date else None

    @property
    def creation_month(self):
        """:class:`int`: Returns the month when the world was created."""
        return int(self.creation_date.split("-")[1])if self.creation_date else None
    # endregion

    # region Public methods
    @classmethod
    def from_content(cls, content):
        """Parses a Tibia.com response into a :class:`World`.

        Parameters
        ----------
        content: :class:`str`
            The raw HTML from the server's information page.

        Returns
        -------
        :class:`World`
            The World described in the page, or :obj:`None`.

        Raises
        ------
        InvalidContent
            If the provided content is not the HTML content of the world section in Tibia.com
        """
        parsed_content = parse_tibiacom_content(content)
        tables = cls._parse_tables(parsed_content)
        try:
            error = tables.get("Error")
            if error and error[0].text == "World with this name doesn't exist!":
                return None
            selected_world = parsed_content.find('option', selected=True)
            world = cls(selected_world.text)
            world._parse_world_info(tables.get("World Information", []))

            online_table = tables.get("Players Online", [])
            world.online_players = []
            for row in online_table[1:]:
                cols_raw = row.find_all('td')
                name, level, vocation = (c.text.replace('\xa0', ' ').strip() for c in cols_raw)
                world.online_players.append(OnlineCharacter(name, world.name, int(level), vocation))
        except AttributeError:
            raise InvalidContent("content is not from the world section in Tibia.com")

        return world
    # endregion

    # region Private methods
    def _parse_world_info(self, world_info_table):
        """
        Parses the World Information table from Tibia.com and adds the found values to the object.

        Parameters
        ----------
        world_info_table: :class:`list`[:class:`bs4.Tag`]
        """
        world_info = {}
        for row in world_info_table:
            cols_raw = row.find_all('td')
            cols = [ele.text.strip() for ele in cols_raw]
            field, value = cols
            field = field.replace("\xa0", "_").replace(" ", "_").replace(":", "").lower()
            value = value.replace("\xa0", " ")
            world_info[field] = value
        try:
            self.online_count = parse_integer(world_info.pop("players_online"))
        except KeyError:
            self.online_count = 0
        self.location = try_enum(WorldLocation, world_info.pop("location"))
        self.pvp_type = try_enum(PvpType, world_info.pop("pvp_type"))
        self.transfer_type = try_enum(TransferType, world_info.pop("transfer_type", None), TransferType.REGULAR)
        m = record_regexp.match(world_info.pop("online_record"))
        if m:
            self.record_count = parse_integer(m.group("count"))
            self.record_date = parse_tibia_datetime(m.group("date"))
        if "world_quest_titles" in world_info:
            self.world_quest_titles = [q.strip() for q in world_info.pop("world_quest_titles").split(",")]
        if self.world_quest_titles and "currently has no title" in self.world_quest_titles[0]:
            self.world_quest_titles = []
        self.experimental = world_info.pop("game_world_type", None) == "Experimental"
        self.tournament_world_type = try_enum(TournamentWorldType, world_info.pop("tournament_world_type", None))
        self._parse_battleye_status(world_info.pop("battleye_status"))
        self.premium_only = "premium_type" in world_info

        month, year = world_info.pop("creation_date").split("/")
        month = int(month)
        year = int(year)
        if year > 90:
            year += 1900
        else:
            year += 2000
        self.creation_date = "%d-%02d" % (year, month)

        for k, v in world_info.items():
            try:
                setattr(self, k, v)
            except AttributeError:
                pass

    def _parse_battleye_status(self, battleye_string):
        """Parses the BattlEye string and applies the results.

        Parameters
        ----------
        battleye_string: :class:`str`
            String containing the world's Battleye Status.
        """
        m = battleye_regexp.search(battleye_string)
        if m:
            self.battleye_date = parse_tibia_full_date(m.group(1))
            self.battleye_type = BattlEyeType.PROTECTED if self.battleye_date else BattlEyeType.INITIALLY_PROTECTED
        else:
            self.battleye_date = None
            self.battleye_type = BattlEyeType.UNPROTECTED

    @classmethod
    def _parse_tables(cls, parsed_content):
        """
        Parses the information tables found in a world's information page.

        Parameters
        ----------
        parsed_content: :class:`bs4.BeautifulSoup`
            A :class:`BeautifulSoup` object containing all the content.

        Returns
        -------
        :class:`OrderedDict`[:class:`str`, :class:`list`[:class:`bs4.Tag`]]
            A dictionary containing all the table rows, with the table headers as keys.
        """
        tables = parsed_content.find_all('div', attrs={'class': 'TableContainer'})
        output = OrderedDict()
        for table in tables:
            title = table.find("div", attrs={'class': 'Text'}).text
            title = title.split("[")[0].strip()
            inner_table = table.find("div", attrs={'class': 'InnerTableContainer'})
            output[title] = inner_table.find_all("tr")
        return output
    # endregion


class WorldOverview(abc.Serializable):
    """Container class for the World Overview section.

    Attributes
    ----------
    record_count: :class:`int`
        The overall player online record.
    record_date: :class:`datetime.datetime`
        The date when the record was achieved.
    worlds: :class:`list` of :class:`ListedWorld`
        List of worlds, with limited info.
    """
    __slots__ = (
        "record_count",
        "record_date",
        "worlds",
    )

    serializable_properties = ('total_online',)

    def __init__(self, **kwargs):
        self.record_count: int = kwargs.get("record_count", 0)
        self.record_date = try_datetime(kwargs.get("record_date"))
        self.worlds: List[ListedWorld] = kwargs.get("worlds", [])

    def __repr__(self):
        return f"<{self.__class__.__name__} total_online={self.total_online:d}>"

    @property
    def total_online(self):
        """:class:`int`: Total players online across all worlds."""
        return sum(w.online_count for w in self.worlds)

    @property
    def tournament_worlds(self):
        """:class:`list` of :class:`GuildMember`: List of tournament worlds.

        Note that tournament worlds are not listed when there are no active or upcoming tournaments."""
        return [w for w in self.worlds if w.tournament_world_type is not None]

    @property
    def regular_worlds(self):
        """:class:`list` of :class:`ListedWorld`: List of worlds that are not tournament worlds."""
        return [w for w in self.worlds if w.tournament_world_type is None]

    @classmethod
    def get_url(cls):
        """
        Gets the URL to the World Overview page in Tibia.com

        Returns
        -------
        :class:`str`
            The URL to the World Overview's page.
        """
        return get_tibia_url("community", "worlds")

    @classmethod
    def from_content(cls, content):
        """Parses the content of the World Overview section from Tibia.com into an object of this class.

        Parameters
        ----------
        content: :class:`str`
            The HTML content of the World Overview page in Tibia.com

        Returns
        -------
        :class:`WorldOverview`
            An instance of this class containing all the information.

        Raises
        ------
        InvalidContent
            If the provided content is not the HTML content of the worlds section in Tibia.com
        """
        parsed_content = parse_tibiacom_content(content)
        world_overview = WorldOverview()
        try:
            record_table, worlds_header_table, *worlds_tables \
                = parsed_content.find_all("table", {"class": "TableContent"})
            m = record_regexp.search(record_table.text)
            world_overview.record_count = parse_integer(m.group("count"))
            world_overview.record_date = parse_tibia_datetime(m.group("date"))
            if len(worlds_tables) > 1:
                tournament_tables = worlds_tables[0]
                worlds_table = worlds_tables[2]
            else:
                tournament_tables = None
                worlds_table = worlds_tables[0]
            regular_world_rows = worlds_table.find_all("tr", attrs={"class": ["Odd", "Even"]})
            world_overview._parse_worlds(regular_world_rows)
            if tournament_tables:
                tournament_world_rows = tournament_tables.find_all("tr", attrs={"class": ["Odd", "Even"]})
                world_overview._parse_worlds(tournament_world_rows, True)
            return world_overview
        except (AttributeError, KeyError, ValueError):
            raise InvalidContent("content does not belong to the World Overview section in Tibia.com")

    def _parse_worlds(self, world_rows, tournament=False):
        """Parses the world columns and adds the results to :py:attr:`worlds`.

        Parameters
        ----------
        world_rows: :class:`list` of :class:`bs4.Tag`
            A list containing the rows of each world.
        tournament: :class:`bool`
            Whether these are tournament worlds or not.
        """
        for world_row in world_rows:
            cols = world_row.find_all("td")
            name = cols[0].text.strip()
            status = "Online"
            online = parse_integer(cols[1].text.strip(), None)
            if online is None:
                online = 0
                status = "Offline"
            location = cols[2].text.replace("\u00a0", " ").strip()
            pvp = cols[3].text.strip()

            world = ListedWorld(name, location, pvp, online_count=online, status=status)
            # Check Battleye icon to get information
            battleye_icon = cols[4].find("span", attrs={"class": "HelperDivIndicator"})
            if battleye_icon is not None:
                m = battleye_regexp.search(battleye_icon["onmouseover"])
                if m:
                    world.battleye_date = parse_tibia_full_date(m.group(1))
                    world.battleye_type = BattlEyeType.PROTECTED if world.battleye_date else BattlEyeType.INITIALLY_PROTECTED
            additional_info = cols[5].text.strip()
            world._parse_additional_info(additional_info, tournament)
            self.worlds.append(world)
