"""Models related to the highscores section in Tibia.com."""
import datetime
import re
from collections import OrderedDict
from typing import List, Optional

from tibiapy import abc
from tibiapy.enums import Category, Vocation, VocationFilter, BattlEyeTypeFilter, PvpTypeFilter, \
    BattlEyeHighscoresFilter
from tibiapy.errors import InvalidContent
from tibiapy.utils import get_tibia_url, parse_form_data, parse_tibiacom_content, try_enum, parse_integer

__all__ = (
    "Highscores",
    "HighscoresEntry",
    "LoyaltyHighscoresEntry",
)

results_pattern = re.compile(r'Results: ([\d,]+)')
numeric_pattern = re.compile(r'(\d+)')


class Highscores(abc.Serializable):
    """Represents the highscores of a world.

    .. versionadded:: 1.1.0

    Attributes
    ----------
    world: :class:`str`
        The world the highscores belong to. If this is :obj:`None`, the highscores shown are for all worlds.
    category: :class:`Category`
        The selected category to displays the highscores of.
    vocation: :class:`VocationFilter`
        The selected vocation to filter out values.
    battleye_filter: :class:`BattlEyeHighscoresFilter`
        The selected BattlEye filter. If :obj:`None`, all worlds will be displayed.

        Only applies for global highscores. Only characters from worlds with the matching BattlEye protection will be
        shown.
    pvp_types_filter: :class:`list` of :class:`PvpTypeFilter`
        The selected PvP types filter. If :obj:`None`, all world will be displayed.

        Only applies for global highscores. Only characters from worlds with the matching PvP type will be shown.
    page: :class:`int`
        The page number being displayed.
    total_pages: :class:`int`
        The total number of pages.
    results_count: :class:`int`
        The total amount of highscores entries in this category. These may be shown in another page.
    last_updated: :class:`datetime.timedelta`
        How long ago were this results updated. The resolution is 1 minute.
    entries: :class:`list` of :class:`HighscoresEntry`
        The highscores entries found.
    available_worlds: :class:`list` of :class:`str`
        The worlds available for selection.
    """

    _ENTRIES_PER_PAGE = 50

    def __init__(self, world, category=Category.EXPERIENCE, **kwargs):
        self.world: Optional[str] = world
        self.category: Category = try_enum(Category, category, Category.EXPERIENCE)
        self.vocation: VocationFilter = try_enum(VocationFilter, kwargs.get("vocation"), VocationFilter.ALL)
        self.battleye_filter: Optional[BattlEyeTypeFilter] = try_enum(BattlEyeTypeFilter, kwargs.get("battleye_filter"))
        self.pvp_types_filter: List[PvpTypeFilter] = kwargs.get("pvp_types_filter", [])
        self.entries: List[HighscoresEntry] = kwargs.get("entries", [])
        self.results_count: int = kwargs.get("results_count", 0)
        self.page: int = kwargs.get("page", 1)
        self.total_pages: int = kwargs.get("total_pages", 1)

    __slots__ = (
        'world',
        'category',
        'vocation',
        'battleye_filter',
        "pvp_types_filter",
        'page',
        'total_pages',
        'results_count',
        'last_updated',
        'entries',
        'available_worlds',
    )

    _serializable_properties = (

    )

    def __repr__(self):
        return f"<{self.__class__.__name__} world={self.world!r} category={self.category!r} vocation={self.vocation!r}>"

    @property
    def from_rank(self):
        """:class:`int`: The starting rank of the provided entries."""
        return self.entries[0].rank if self.entries else 0

    @property
    def to_rank(self):
        """:class:`int`: The last rank of the provided entries."""
        return self.entries[-1].rank if self.entries else 0

    @property
    def url(self):
        """:class:`str`: The URL to the highscores page on Tibia.com containing the results."""
        return self.get_url(self.world, self.category, self.vocation, self.page, self.battleye_filter,
                            self.pvp_types_filter)

    @property
    def previous_page_url(self):
        """:class:`str`: The URL to the previous page of the current highscores, if there's any."""
        return self.get_page_url(self.page - 1) if self.page > 1 else None

    @property
    def next_page_url(self):
        """:class:`str`: The URL to the next page of the current highscores, if there's any."""
        return self.get_page_url(self.page + 1) if self.page < self.total_pages else None

    def get_page_url(self, page):
        """Get the URL to a specific page for the current highscores.

        Parameters
        ----------
        page: :class:`int`
            The page to get the URL for.

        Returns
        -------
        :class:`str`
            The URL to the page of the current highscores.

        Raises
        ------
        ValueError
            The provided page is less or equals than zero.
        """
        if page <= 0:
            raise ValueError("page cannot be less or equals than zero")
        return self.get_url(self.world, self.category, self.vocation, page, self.battleye_filter, self.pvp_types_filter)

    @classmethod
    def from_content(cls, content):
        """Create an instance of the class from the html content of a highscores page.

        Notes
        -----
        Tibia.com only shows up to 50 entries per page, so in order to obtain the full highscores, all pages must be
        obtained individually and merged into one.

        Parameters
        ----------
        content: :class:`str`
            The HTML content of the page.

        Returns
        -------
        :class:`Highscores`
            The highscores results contained in the page.

        Raises
        ------
        InvalidContent
            If content is not the HTML of a highscore's page.
        """
        parsed_content = parse_tibiacom_content(content)
        form = parsed_content.find("form")
        tables = cls._parse_tables(parsed_content)
        if form is None:
            if "Error" in tables and "The world doesn't exist!" in tables["Error"].text:
                return None
            raise InvalidContent("content does is not from the highscores section of Tibia.com")
        highscores = cls(None)
        highscores._parse_filters_table(form)
        last_update_container = parsed_content.find("span", attrs={"class": "RightArea"})
        if last_update_container:
            m = numeric_pattern.search(last_update_container.text)
            highscores.last_updated = datetime.timedelta(minutes=int(m.group(1))) if m else datetime.timedelta()
        entries_table = tables.get("Highscores")
        highscores._parse_entries_table(entries_table)
        return highscores

    @classmethod
    def get_url(cls, world=None, category=Category.EXPERIENCE, vocation=VocationFilter.ALL, page=1,
                battleye_type=None, pvp_types=None):
        """Get the Tibia.com URL of the highscores for the given parameters.

        Parameters
        ----------
        world: :class:`str`, optional
            The game world of the desired highscores. If no world is passed, ALL worlds are shown.
        category: :class:`Category`
            The desired highscores category.
        vocation: :class:`VocationFilter`
            The vocation filter to apply. By default all vocations will be shown.
        page: :class:`int`
            The page of highscores to show.
        battleye_type: :class:`BattlEyeHighscoresFilter`, optional
            The battleEye filters to use.
        pvp_types: :class:`list` of :class:`PvpTypeFilter`, optional
            The list of PvP types to filter the results for.

        Returns
        -------
        The URL to the Tibia.com highscores.
        """
        pvp_types = pvp_types or []
        pvp_params = [("worldtypes[]", p.value) for p in pvp_types]
        return get_tibia_url("community", "highscores", *pvp_params, world=world, category=category.value,
                             profession=vocation.value, currentpage=page,
                             beprotection=battleye_type.value if battleye_type else None)

    # region Private methods
    def _parse_entries_table(self, table):
        """Parse the table containing the highscore entries.

        Parameters
        ----------
        table: :class:`bs4.Tag`
            The table containing the entries.
        """
        entries = table.find_all("tr")
        if entries is None:
            return
        _, header, *rows = entries
        info_row = rows.pop()
        pages_div, results_div = info_row.find_all("div")
        page_links = pages_div.find_all("a")
        listed_pages = [int(p.text) for p in page_links]
        if listed_pages:
            self.page = next((x for x in range(1, listed_pages[-1] + 1) if x not in listed_pages), listed_pages[-1] + 1)
            self.total_pages = max(int(page_links[-1].text), self.page)
        self.results_count = parse_integer(results_pattern.search(results_div.text).group(1))
        for row in rows:
            cols_raw = row.find_all('td')
            if "There is currently no data" in cols_raw[0].text:
                break
            if len(cols_raw) <= 2:
                break
            self._parse_entry(cols_raw)

    def _parse_filters_table(self, form):
        """
        Parse the filters table found in a highscores page.

        Parameters
        ----------
        form: :class:`bs4.Tag`
            The table containing the filters.
        """
        data = parse_form_data(form, include_options=True)
        self.world = data["world"] if data.get("world") else None
        self.battleye_filter = try_enum(BattlEyeHighscoresFilter, parse_integer(data.get("beprotection"), None))
        self.category = try_enum(Category, parse_integer(data.get("category"), None))
        self.vocation = try_enum(VocationFilter, parse_integer(data.get("profession"), None), VocationFilter.ALL)
        checkboxes = form.find_all("input", {"type": "checkbox", "checked": "checked"})
        values = [int(c["value"]) for c in checkboxes]
        self.pvp_types_filter = [try_enum(PvpTypeFilter, v) for v in values]
        self.available_words = [v for v in data["__options__"]["world"].values() if v]

    @classmethod
    def _parse_tables(cls, parsed_content):
        """
        Parse the information tables found in a highscores page.

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

        Returns
        -------
        :class:`OrderedDict`[:class:`str`, :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()
            title = re.sub(r'Last Update.*', '', title)
            inner_table = table.find("div", attrs={'class': 'InnerTableContainer'})
            output[title] = inner_table
        return output

    def _parse_entry(self, cols):
        """Parse an entry's row and adds the result to py:attr:`entries`.

        Parameters
        ----------
        cols: :class:`bs4.ResultSet`
            The list of columns for that entry.
        """
        rank, name, *values = [c.text.replace('\xa0', ' ').strip() for c in cols]
        rank = int(rank)
        extra = None
        if self.category == Category.LOYALTY_POINTS:
            extra, vocation, world, level, value = values
        else:
            vocation, world, level, value = values
        value = int(value.replace(',', ''))
        level = int(level)
        if self.category == Category.LOYALTY_POINTS:
            entry = LoyaltyHighscoresEntry(rank, name, vocation, world, level, value, extra)
        else:
            entry = HighscoresEntry(rank, name, vocation, world, level, value)
        self.entries.append(entry)
    # endregion


class HighscoresEntry(abc.BaseCharacter, abc.Serializable):
    """Represents a entry for the highscores.

    Attributes
    ----------
    name: :class:`str`
        The name of the character.
    rank: :class:`int`
        The character's rank in the respective highscores.
    vocation: :class:`Vocation`
        The character's vocation.
    world: :class:`str`
        The character's world.
    level: :class:`int`
        The character's level.
    value: :class:`int`
        The character's value for the highscores.
    """

    def __init__(self, rank, name, vocation, world, level, value):
        self.name: str = name
        self.rank: int = rank
        self.vocation = try_enum(Vocation, vocation)
        self.value: int = value
        self.world: str = world
        self.level: int = level

    __slots__ = (
        'rank',
        'name',
        'vocation',
        'world',
        'level',
        'value',
    )

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} rank={self.rank} name={self.name!r} value={self.value}>"


class LoyaltyHighscoresEntry(HighscoresEntry):
    """Represents a entry for the highscores loyalty points category.

    This is a subclass of :class:`HighscoresEntry`, adding an extra field for title.

    Attributes
    ----------
    name: :class:`str`
        The name of the character.
    rank: :class:`int`
        The character's rank in the respective highscores.
    vocation: :class:`Vocation`
        The character's vocation.
    world: :class:`str`
        The character's world.
    level: :class:`int`
        The character's level.
    value: :class:`int`
        The character's loyalty points.
    title: :class:`str`
        The character's loyalty title.
    """

    def __init__(self, rank, name, vocation, world, level, value, title):
        super().__init__(rank, name, vocation, world, level, value)
        self.title: str = title

    __slots__ = (
        'title',
    )
