from abc import ABC, abstractmethod
from multiprocessing.managers import ListProxy
from typing import Any, Tuple, List, Optional

from crawlMp import CrawlException, ActionException
from crawlMp.actions.action import Action
from crawlMp.enums import Mode, Header_ref
from crawlMp.results import Results


class Crawler(ABC):
    """
    Basic Crawler interface.
    """

    def __init__(self, links: Optional[List[Any]] = None, mode: Mode = Mode.SIMPLE,
                 actions: Optional[Tuple[Action, ...]] = None, *args, **kwargs):
        """
        :param list links: list of entrypoints
        :param str mode: Data collection mode
        :param args:
        :param kwargs:
        """
        # crawling mode must be in hits and links headers
        self.args = args
        self.kwargs = kwargs
        self.actions = actions
        self.mode = mode
        self.metadata: Tuple[Any, ...] = ()
        self.entrypoint: Any = None
        self.results = Results(self.hits_header(self.mode), self.links_header(self.mode))
        self.links = links

    def __iter__(self):
        return self

    def __next__(self):
        """
        Generate next link and execute crawl method over it.
        :return Crawler: self object
        """
        try:
            next_link = self.links.pop(0)
        except IndexError:
            # No other links exists, stop iteration
            raise StopIteration
        try:
            self.crawl(next_link)
        except CrawlException:
            # If crawl fails for any reason, don't follow that link
            self.results.links_skipped.append(next_link)
        return self

    @property
    def mode(self) -> Mode:
        return self._mode

    @mode.setter
    def mode(self, new_mode: Mode) -> None:
        assert new_mode in self.crawl_modes()
        self._mode = new_mode

    @property
    def links(self) -> List[Any]:
        return self._links

    @links.setter
    def links(self, new_links: Optional[List[Any]]) -> None:
        assert isinstance(new_links, (list, ListProxy)) or new_links is None
        self._links = new_links if new_links is not None else []

    @property
    def actions(self) -> Tuple[Action, ...]:
        return self._actions

    @actions.setter
    def actions(self, new_actions: Tuple[Action, ...]) -> None:
        self._actions = () if new_actions is None else new_actions
        for action in self._actions:
            assert isinstance(action, Action)

    @staticmethod
    @abstractmethod
    def crawl_modes() -> List[Mode]:
        """
        Get available crawl modes.
        :return tuple: (Mode1, Mode2, ...)
        """
        ...

    @staticmethod
    @abstractmethod
    def links_header(mode: Mode = Mode.SIMPLE) -> Tuple[Header_ref, ...]:
        """
        Get links header
        :param Mode mode: crawl mode
        :return tuple: (Name, Type, Unit)
        """
        ...

    @staticmethod
    @abstractmethod
    def hits_header(mode: Mode = Mode.SIMPLE) -> Tuple[Header_ref, ...]:
        """
        Get hits header
        :param Mode mode: crawl mode
        :return tuple: (Name, Type, Unit)
        """
        ...

    def execute_actions(self, hit: Any) -> bool:
        """
        Execute all actions sequence on the given hit.
        Actions are organized in a pipeline, feeding output of one action to the next action.
        Return True if whole pipeline was successfully executed.
        :param Any hit: input hit
        :return:
        """
        for action in self.actions:
            try:
                hit = action.do(hit)
            except ActionException:
                return False
        return True

    def crawl(self, entrypoint: Any) -> None:
        """
        Init entrypoint (next link), extract links and hits, close entrypoint.
        :param Any entrypoint: Address of entrypoint / resource
        :return: None
        """
        if entrypoint is None:
            return
        self.entrypoint = entrypoint
        self.metadata = self.init_entrypoint()
        self.results.links_followed.append(entrypoint)
        self.results.hits += self.extract_hits()
        self.links += self.extract_links()
        self.close_entrypoint()

    @abstractmethod
    def init_entrypoint(self) -> Tuple[Any, ...]:
        """
        Initialize entrypoint / resource
        Good example is opening new webpage, when entrypoint is url.
        :return tuple: Resource metadata
        """
        ...

    @abstractmethod
    def close_entrypoint(self) -> None:
        """
        Clean allocated resources attached to entrypoint / resource.
        This method must be reimplemented.
        :return: None
        """
        ...

    @abstractmethod
    def extract_hits(self) -> List[Any]:
        """
        Extract all hits from current entrypoint / resource.
        This method must be reimplemented.
        :return:
        """
        ...

    @abstractmethod
    def extract_links(self) -> List[Any]:
        """
        Extract all links from current entrypoint / resource.
        This method must be reimplemented.
        :return list: list of extracted links
        """
        ...

    @abstractmethod
    def is_hit(self, item: Any) -> bool:
        """
        Decide if input item is hits or not.
        This method must be reimplemented.
        :param Any item: Input item
        :return bool: True if item is a Hit
        """
        ...

    @abstractmethod
    def is_link(self, item: Any) -> bool:
        """
        Decide if input item is link or not.
        This method must be reimplemented.
        :param Any item: Input item
        :return bool: True if item is a Link
        """
        ...

from abc import ABC, abstractmethod
from multiprocessing.managers import ListProxy
from typing import Any, Tuple, List, Optional

from crawlMp import CrawlException, ActionException
from crawlMp.actions.action import Action
from crawlMp.enums import Mode, Header_ref
from crawlMp.results import Results


class Crawler(ABC):
    """
    Basic Crawler interface.
    """

    def __init__(self, links: Optional[List[Any]] = None, mode: Mode = Mode.SIMPLE,
                 actions: Optional[Tuple[Action, ...]] = None, *args, **kwargs):
        """
        :param list links: list of entrypoints
        :param str mode: Data collection mode
        :param args:
        :param kwargs:
        """
        # crawling mode must be in hits and links headers
        self.args = args
        self.kwargs = kwargs
        self.actions = actions
        self.mode = mode
        self.metadata: Tuple[Any, ...] = ()
        self.entrypoint: Any = None
        self.results = Results(self.hits_header(self.mode), self.links_header(self.mode))
        self.links = links

    def __iter__(self):
        return self

    def __next__(self):
        """
        Generate next link and execute crawl method over it.
        :return Crawler: self object
        """
        try:
            next_link = self.links.pop(0)
        except IndexError:
            # No other links exists, stop iteration
            raise StopIteration
        try:
            self.crawl(next_link)
        except CrawlException:
            # If crawl fails for any reason, don't follow that link
            self.results.links_skipped.append(next_link)
        return self

    @property
    def mode(self) -> Mode:
        return self._mode

    @mode.setter
    def mode(self, new_mode: Mode) -> None:
        assert new_mode in self.crawl_modes()
        self._mode = new_mode

    @property
    def links(self) -> List[Any]:
        return self._links

    @links.setter
    def links(self, new_links: Optional[List[Any]]) -> None:
        assert isinstance(new_links, (list, ListProxy)) or new_links is None
        self._links = new_links if new_links is not None else []

    @property
    def actions(self) -> Tuple[Action, ...]:
        return self._actions

    @actions.setter
    def actions(self, new_actions: Tuple[Action, ...]) -> None:
        self._actions = () if new_actions is None else new_actions
        for action in self._actions:
            assert isinstance(action, Action)

    @staticmethod
    @abstractmethod
    def crawl_modes() -> List[Mode]:
        """
        Get available crawl modes.
        :return tuple: (Mode1, Mode2, ...)
        """
        ...

    @staticmethod
    @abstractmethod
    def links_header(mode: Mode = Mode.SIMPLE) -> Tuple[Header_ref, ...]:
        """
        Get links header
        :param Mode mode: crawl mode
        :return tuple: (Name, Type, Unit)
        """
        ...

    @staticmethod
    @abstractmethod
    def hits_header(mode: Mode = Mode.SIMPLE) -> Tuple[Header_ref, ...]:
        """
        Get hits header
        :param Mode mode: crawl mode
        :return tuple: (Name, Type, Unit)
        """
        ...

    def execute_actions(self, hit: Any) -> bool:
        """
        Execute all actions sequence on the given hit.
        Actions are organized in a pipeline, feeding output of one action to the next action.
        Return True if whole pipeline was successfully executed.
        :param Any hit: input hit
        :return:
        """
        for action in self.actions:
            try:
                hit = action.do(hit)
            except ActionException:
                return False
        return True

    def crawl(self, entrypoint: Any) -> None:
        """
        Init entrypoint (next link), extract links and hits, close entrypoint.
        :param Any entrypoint: Address of entrypoint / resource
        :return: None
        """
        if entrypoint is None:
            return
        self.entrypoint = entrypoint
        self.metadata = self.init_entrypoint()
        self.results.links_followed.append(entrypoint)
        self.results.hits += self.extract_hits()
        self.links += self.extract_links()
        self.close_entrypoint()

    @abstractmethod
    def init_entrypoint(self) -> Tuple[Any, ...]:
        """
        Initialize entrypoint / resource
        Good example is opening new webpage, when entrypoint is url.
        :return tuple: Resource metadata
        """
        ...

    @abstractmethod
    def close_entrypoint(self) -> None:
        """
        Clean allocated resources attached to entrypoint / resource.
        This method must be reimplemented.
        :return: None
        """
        ...

    @abstractmethod
    def extract_hits(self) -> List[Any]:
        """
        Extract all hits from current entrypoint / resource.
        This method must be reimplemented.
        :return:
        """
        ...

    @abstractmethod
    def extract_links(self) -> List[Any]:
        """
        Extract all links from current entrypoint / resource.
        This method must be reimplemented.
        :return list: list of extracted links
        """
        ...

    @abstractmethod
    def is_hit(self, item: Any) -> bool:
        """
        Decide if input item is hits or not.
        This method must be reimplemented.
        :param Any item: Input item
        :return bool: True if item is a Hit
        """
        ...

    @abstractmethod
    def is_link(self, item: Any) -> bool:
        """
        Decide if input item is link or not.
        This method must be reimplemented.
        :param Any item: Input item
        :return bool: True if item is a Link
        """
        ...

Basic Crawler interface. :param list links: list of entrypoints         :param str mode: Data collection mode         :param args:         :param kwargs: # crawling mode must be in hits and links headers Generate next link and execute crawl method over it.         :return Crawler: self object # No other links exists, stop iteration # If crawl fails for any reason, don't follow that link Get available crawl modes.         :return tuple: (Mode1, Mode2, ...) Get links header         :param Mode mode: crawl mode         :return tuple: (Name, Type, Unit) Get hits header         :param Mode mode: crawl mode         :return tuple: (Name, Type, Unit) Execute all actions sequence on the given hit.         Actions are organized in a pipeline, feeding output of one action to the next action.         Return True if whole pipeline was successfully executed.         :param Any hit: input hit         :return: Init entrypoint (next link), extract links and hits, close entrypoint.         :param Any entrypoint: Address of entrypoint / resource         :return: None Initialize entrypoint / resource         Good example is opening new webpage, when entrypoint is url.         :return tuple: Resource metadata Clean allocated resources attached to entrypoint / resource.         This method must be reimplemented.         :return: None Extract all hits from current entrypoint / resource.         This method must be reimplemented.         :return: Extract all links from current entrypoint / resource.         This method must be reimplemented.         :return list: list of extracted links Decide if input item is hits or not.         This method must be reimplemented.         :param Any item: Input item         :return bool: True if item is a Hit Decide if input item is link or not.         This method must be reimplemented.         :param Any item: Input item         :return bool: True if item is a Link