# Copyright (C) 2021 DigeeX
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""Common Plugin classes used by other plugins.
"""


import logging
from typing import Callable, Dict, List, Optional

import requests


class Plugin:
    """Parent class for all plugins.

    Each Plugin class inherits from here. "get_value" function should
    be called when extracting the value from the plugin, which will then
    be stored in the "value" attribute.

    Attributes:
      name:
        A string used as an identifier for the Plugin.
      function:
        A function which will be called to extract the "value" of the
        Plugin when used as an input in a Flow. The function should set
        self.value and also return it.
      value:
        A string containing the Plugin's output value to be used as
        input in the HTTP request.
      flags:
        An integer containing the flags that define the Plugin's
        behaviour. For now only NEEDS_USERDATA and NEEDS_RESPONSE is
        supported. If NEEDS_USERDATA is set, the plugin will get its
        value from the user's data, which will be sent to the function
        defined here. If NEEDS_RESPONSE is set, the Plugin will extract
        its value from the HTTP response instead.

    """

    # Plugin flags
    NEEDS_USERDATA = 0x01
    NEEDS_RESPONSE = 0x02
    DEPENDS_ON_OTHER_PLUGINS = 0x04
    NAME_NOT_KNOWN_IN_ADVANCE = 0x08

    def __init__(
        self,
        name: str,
        function: Optional[Callable[..., Optional[str]]] = None,
        flags: int = 0,
        value: Optional[str] = None,
    ) -> None:
        """Initializes a Plugin object.

        Creates a Plugin object, holding a "function" defining how to
        extract the "value".

        Args:
          name:
            A string with the unique identifier of the Plugin.
          function:
            A Callable function that will be used to extract the
            Plugin's value.
          value:
            A string with the extracted value from the Plugin.
          flags:
            An integer containing the flags that define the Plugin's
            behaviour. For now only NEEDS_USERDATA and NEEDS_RESPONSE is
            supported. If NEEDS_USERDATA is set, the plugin will get its
            value from the user's data, which will be sent to the function
            defined here. If NEEDS_RESPONSE is set, the Plugin will extract
            its value from the HTTP response instead.

        """
        self.name = name
        self.plugins: List["Plugin"] = []
        self.value: Optional[str] = value
        self.flags = flags

        self.function: Callable[..., Optional[str]]
        self.name_function: Optional[Callable[..., Optional[str]]] = None

        if (flags & Plugin.NEEDS_USERDATA) and not function:
            self.function = self.extract_value_from_userdata
        elif not function:
            self.function = self.return_value
        else:
            self.function = function

    def get_value(
        self,
        userdata: Dict[str, str],
    ) -> Optional[str]:
        """Gets the value from the Plugin.

        Depending on the Plugin's flags, extract and return its value.

        Args:
          userdata:
            A dictionary with the user specific data.
        """
        if not self.needs_response:
            if self.needs_userdata:
                self.value = self.function(userdata)
            elif self.depends_on_other_plugins:
                for item in self.plugins:
                    item.get_value(userdata)
                self.value = self.function()
            else:
                self.value = self.function()
        return self.value

    def extract_value_from_response(
        self,
        response: Optional[requests.models.Response],
    ) -> None:
        """Extracts the value of the Plugin from the HTTP response.

        If NEEDS_RESPONSE flag is set, the Plugin will extract its value
        upon receiving the HTTP response, and store it inside the "value"
        attribute.

        Args:
          response:
            An requests.models.Response object with the HTTP response.

        """
        output = self.function(response)
        if output:
            self.value = output
            logging.debug(
                "Found ouput %s = %s",
                self.name,
                self.value,
            )
        else:
            logging.warning("Couldn't extract output: %s", str(self.name))

    def extract_name_from_response(
        self,
        response: Optional[requests.models.Response],
    ) -> None:
        """Extracts the name of the Plugin from the HTTP response.

        If NAME_NOT_KNOWN_IN_ADVANCE flag is set, the Plugin will set
        its name after receiving the HTTP response, and store it inside
        the "name" attribute.

        Args:
          response:
            An requests.models.Response object with the HTTP response.

        """
        if callable(self.name_function):
            # pylint can't figure out name_function is callable
            # pylint: disable=E1102
            output = self.name_function(response)
        if output:
            self.name = output
        else:
            logging.warning("Couldn't extract name: %s", str(self.name))

    def extract_value_from_userdata(
        self, data: Dict[str, str] = None
    ) -> Optional[str]:
        """Extracts the plugin value from userdata.

        Given a dictionary with the userdata, return its value with the
        same name as the "name" attribute from this Plugin.

        Args:
          data:
            A dictionary with user specific data.

        Returns:
          A string with the value of the variable found. None if no such
          variable has been defined.

        """
        if data and self.name in data:
            self.value = data[self.name]
        return self.value

    def return_value(self) -> Optional[str]:
        """Just return plugin's value.

        This is used when needing a function just to return the value.

        """
        return self.value

    @property
    def needs_userdata(self) -> bool:
        """Returns True if the NEEDS_USERDATA flag is set."""
        return bool(self.flags & self.NEEDS_USERDATA)

    @property
    def needs_response(self) -> bool:
        """Returns True if the NEEDS_RESPONSE flag is set."""
        return bool(self.flags & self.NEEDS_RESPONSE)

    @property
    def depends_on_other_plugins(self) -> bool:
        """Returns True if the DEPENDS_ON_OTHER_PLUGINS flag is set."""
        return bool(self.flags & self.DEPENDS_ON_OTHER_PLUGINS)

    @property
    def name_not_known_in_advance(self) -> bool:
        """Returns True if the NAME_NOT_KNOWN_IN_ADVANCE flag is set."""
        return bool(self.flags & self.NAME_NOT_KNOWN_IN_ADVANCE)


class Parser(Plugin):
    """Plugins that parse other plugins."""

    def __init__(
        self,
        name: str,
        function: Callable[[], Optional[str]],
        value: str = None,
    ) -> None:
        """Initializes the Parser plugin."""
        super().__init__(
            name=name,
            value=value,
            function=function,
            flags=Plugin.DEPENDS_ON_OTHER_PLUGINS,
        )


class Empty(Plugin):
    """Empty plugin to use for fuzzing new data."""

    def __init__(self, name: str):
        """Initialize Empty plugin."""
        super().__init__(
            name=name,
            flags=0,
        )
