"""The parsers and plugins manager."""

import pysigscan

from plaso.filters import parser_filter
from plaso.lib import specification


class ParsersManager:
    """The parsers and plugins manager."""

    ALL_PLUGINS = set(["*"])

    _parser_classes = {}

    @classmethod
    def _GetParsers(cls, parser_filter_expression=None):
        """Retrieves the registered parsers and plugins.

        Args:
          parser_filter_expression (Optional[str]): parser filter expression,
              where None represents all parsers and plugins.

              A parser filter expression is a comma separated value string that
              denotes which parsers and plugins should be used. See
              filters/parser_filter.py for details of the expression syntax.

              This function does not support presets, and requires a parser
              filter expression where presets have been expanded.

        Yields:
          tuple: containing:

          * str: name of the parser:
          * type: parser class (subclass of BaseParser).
        """
        parser_filter_helper = parser_filter.ParserFilterExpressionHelper()
        excludes, includes = parser_filter_helper.SplitExpression(
            parser_filter_expression
        )

        for parser_name, parser_class in cls._parser_classes.items():
            # If there are no includes all parsers are included by default.
            if not includes and parser_name in excludes:
                continue

            if includes and parser_name not in includes:
                continue

            yield parser_name, parser_class

    @classmethod
    def CreateSignatureScanner(cls, specification_store):
        """Creates a signature scanner for format specifications with signatures.

        Args:
          specification_store (FormatSpecificationStore): format specifications
              with signatures.

        Returns:
          pysigscan.scanner: signature scanner.
        """
        scanner_object = pysigscan.scanner()

        for format_specification in specification_store.specifications:
            for signature in format_specification.signatures:
                pattern_offset = signature.offset

                if pattern_offset is None:
                    signature_flags = pysigscan.signature_flags.NO_OFFSET
                elif pattern_offset < 0:
                    pattern_offset *= -1
                    signature_flags = pysigscan.signature_flags.RELATIVE_FROM_END
                else:
                    signature_flags = pysigscan.signature_flags.RELATIVE_FROM_START

                scanner_object.add_signature(
                    signature.identifier,
                    pattern_offset,
                    signature.pattern,
                    signature_flags,
                )

        return scanner_object

    @classmethod
    def CheckFilterExpression(cls, parser_filter_expression):
        """Checks parser and plugin names in a parser filter expression.

        Args:
          parser_filter_expression (str): parser filter expression,
              where None represents all parsers and plugins.

              A parser filter expression is a comma separated value string that
              denotes which parsers and plugins should be used. See
              filters/parser_filter.py for details of the expression syntax.

              This function does not support presets, and requires a parser filter
              expression where presets have been expanded.

        Returns:
          tuple: containing:

          * set(str): parser filter expression elements that contain known parser
              and/or plugin names.
          * set(str): parser filter expression elements that contain unknown parser
              and/or plugin names.
        """
        known_parser_elements = set()
        unknown_parser_elements = set()

        if not parser_filter_expression:
            for parser_name, parser_class in cls._parser_classes.items():
                known_parser_elements.add(parser_name)
                if parser_class.SupportsPlugins():
                    for plugin_name in parser_class.GetPluginNames():
                        known_parser_elements.add("/".join([parser_name, plugin_name]))

        else:
            for element in parser_filter_expression.split(","):
                parser_expression = element
                if element.startswith("!"):
                    parser_expression = element[1:]

                parser_name, _, plugin_name = parser_expression.partition("/")
                parser_class = cls._parser_classes.get(parser_name)
                if not parser_class:
                    unknown_parser_elements.add(element)
                    continue

                if parser_class.SupportsPlugins():
                    plugins = dict(parser_class.GetPlugins())
                    if not plugin_name:
                        for plugin in plugins:
                            known_parser_elements.add("/".join([parser_name, plugin]))
                    elif plugin_name in plugins:
                        known_parser_elements.add(element)
                    else:
                        unknown_parser_elements.add(element)

                elif not plugin_name:
                    known_parser_elements.add(element)

        return known_parser_elements, unknown_parser_elements

    @classmethod
    def DeregisterParser(cls, parser_class):
        """Deregisters a parser class.

        The parser classes are identified based on their lower case name.

        Args:
          parser_class (type): parser class (subclass of BaseParser).

        Raises:
          KeyError: if parser class is not set for the corresponding name.
        """
        parser_name = parser_class.NAME.lower()
        if parser_name not in cls._parser_classes:
            raise KeyError(f"Parser class not set for name: {parser_class.NAME:s}.")

        del cls._parser_classes[parser_name]

    @classmethod
    def GetFormatsWithSignatures(cls, parser_filter_expression=None):
        """Retrieves the format specifications that have signatures.

        This method will create a specification store for parsers that define
        a format specification with signatures and a list of parser names for
        those that do not.

        Args:
          parser_filter_expression (Optional[str]): parser filter expression,
              where None represents all parsers and plugins.

              A parser filter expression is a comma separated value string that
              denotes which parsers and plugins should be used. See
              filters/parser_filter.py for details of the expression syntax.

              This function does not support presets, and requires a parser filter
              expression where presets have been expanded.

        Returns:
          tuple: containing:

          * FormatSpecificationStore: format specifications with signatures.
          * list[str]: names of parsers that do not have format specifications with
              signatures, or have signatures but also need to be applied 'brute
              force'.
        """
        specification_store = specification.FormatSpecificationStore()
        remainder_list = []

        for parser_name, parser_class in cls._GetParsers(
            parser_filter_expression=parser_filter_expression
        ):
            format_specification = parser_class.GetFormatSpecification()

            if format_specification and format_specification.signatures:
                specification_store.AddSpecification(format_specification)
                # The plist parser is a special case, where it both defines a signature
                # and also needs to be applied 'brute-force' to non-matching files,
                # as the signature matches binary plists, but not XML or JSON plists.
                if parser_name == "plist":
                    remainder_list.append(parser_name)
            else:
                remainder_list.append(parser_name)

        return specification_store, remainder_list

    @classmethod
    def GetNamesOfParsersWithPlugins(cls):
        """Retrieves the names of all parsers with plugins.

        Returns:
          list[str]: names of all parsers with plugins.
        """
        parser_names = []

        for parser_name, parser_class in cls._GetParsers():
            if parser_class.SupportsPlugins():
                parser_names.append(parser_name)

        return sorted(parser_names)

    @classmethod
    def GetParserPluginsInformation(cls, parser_filter_expression=None):
        """Retrieves the parser plugins information.

        Args:
          parser_filter_expression (Optional[str]): parser filter expression,
              where None represents all parsers and plugins.

              A parser filter expression is a comma separated value string that
              denotes which parsers and plugins should be used. See
              filters/parser_filter.py for details of the expression syntax.

              This function does not support presets, and requires a parser
              filter expression where presets have been expanded.

        Returns:
          list[tuple[str, str]]: pairs of parser plugin names and descriptions.
        """
        parser_plugins_information = []
        for _, parser_class in cls._GetParsers(
            parser_filter_expression=parser_filter_expression
        ):
            if parser_class.SupportsPlugins():
                for plugin_name, plugin_class in parser_class.GetPlugins():
                    description = ""

                    data_format = getattr(plugin_class, "DATA_FORMAT", "")
                    if data_format:
                        if data_format.endswith(" file"):
                            description = f"Parser for {data_format}s."
                        else:
                            description = f"Parser for {data_format:s}."

                    parser_plugins_information.append((plugin_name, description))

        return parser_plugins_information

    @classmethod
    def GetParserObjects(cls, parser_filter_expression=None):
        """Retrieves the parser objects.

        Args:
          parser_filter_expression (Optional[str]): parser filter expression,
              where None represents all parsers and plugins.

              A parser filter expression is a comma separated value string that
              denotes which parsers and plugins should be used. See
              filters/parser_filter.py for details of the expression syntax.

              This function does not support presets, and requires a parser
              filter expression where presets have been expanded.

        Returns:
          dict[str, BaseParser]: parsers per name.
        """
        parser_filter_helper = parser_filter.ParserFilterExpressionHelper()
        excludes, includes = parser_filter_helper.SplitExpression(
            parser_filter_expression
        )

        parser_objects = {}
        for parser_name, parser_class in cls._parser_classes.items():
            # If there are no includes all parsers are included by default.
            if not includes and parser_name in excludes:
                continue

            if includes and parser_name not in includes:
                continue

            parser_object = parser_class()
            if parser_class.SupportsPlugins():
                plugin_includes = includes.get(parser_name, cls.ALL_PLUGINS)
                parser_object.EnablePlugins(plugin_includes)

            parser_objects[parser_name] = parser_object

        return parser_objects

    @classmethod
    def GetParsersInformation(cls):
        """Retrieves the parsers information.

        Returns:
          list[tuple[str, str]]: parser names and descriptions.
        """
        parsers_information = []
        for _, parser_class in cls._GetParsers():
            description = ""

            data_format = getattr(parser_class, "DATA_FORMAT", "")
            if data_format:
                if data_format.endswith(" file"):
                    description = f"Parser for {data_format}s."
                else:
                    description = f"Parser for {data_format:s}."

            parsers_information.append((parser_class.NAME, description))

        return parsers_information

    @classmethod
    def RegisterParser(cls, parser_class):
        """Registers a parser class.

        The parser classes are identified based on their lower case name.

        Args:
          parser_class (type): parser class (subclass of BaseParser).

        Raises:
          KeyError: if parser class is already set for the corresponding name.
        """
        parser_name = parser_class.NAME.lower()
        if parser_name in cls._parser_classes:
            raise KeyError(f"Parser class already set for name: {parser_class.NAME:s}.")

        cls._parser_classes[parser_name] = parser_class

    @classmethod
    def RegisterParsers(cls, parser_classes):
        """Registers parser classes.

        The parser classes are identified based on their lower case name.

        Args:
          parser_classes (list[type]): parsers classes (subclasses of BaseParser).

        Raises:
          KeyError: if parser class is already set for the corresponding name.
        """
        for parser_class in parser_classes:
            cls.RegisterParser(parser_class)
