"""Helper for parser and plugin filter expressions."""


class ParserFilterExpressionHelper:
    """Helper for parser and plugin filter expressions.

    A parser filter expression is a comma separated value string that denotes
    which parsers and plugins should be used. Each element can contain either:

    * The name of a preset (case sensitive), which is a predefined list of parsers
      and/or plugins (see data/presets.yaml for the default presets).
    * The name of a parser (case insensitive), for example 'msiecf'.
    * The name of a plugin, prefixed with the parser name and a '/', for example
      'sqlite/chrome_history'.

    If the element begins with an exclamation mark ('!') the item will be
    excluded from the set of enabled parsers and plugins, otherwise the element
    will be included.
    """

    def _GetParserAndPluginsList(self, parsers_and_plugins):
        """Flattens the parsers and plugins dictionary into a list.

        Args:
          parsers_and_plugins (dict[str, set[str]]): parsers and plugins.

        Returns:
          list[str]: alphabetically sorted list of the parsers and plugins.
        """
        parser_filters = []
        for parser_name, plugins in sorted(parsers_and_plugins.items()):
            for plugin_name in sorted(plugins):
                if plugin_name == "*":
                    parser_filters.append(parser_name)
                else:
                    parser_filters.append("/".join([parser_name, plugin_name]))

        return parser_filters

    def _JoinExpression(self, excludes, includes):
        """Creates an expression string of the excluded and included parsers.

        Note that exclusion takes precedence over inclusion.

        Args:
          excludes (dict[str, set[str]]): excluded parsers and plugins.
          includes (dict[str, set[str]]): included parsers and plugins.

        Returns:
          str: a parser filter expression.

            A parser filter expression is a comma separated value string that
            denotes which parsers should be used. Each element can contain either:

            * The name of a parser (case insensitive), for example 'msiecf'.
            * The name of a plugin, prefixed with the parser name and a '/', for
              example 'sqlite/chrome_history'.

            If the element begins with an exclamation mark ('!') the item will be
            excluded from the set of enabled parsers and plugins, otherwise the
            element will be included.

        Raises:
          RuntimeError: if a specific plugin is excluded but no corresponding parser
              is included.
        """
        excluded_parsers_and_plugins = self._GetParserAndPluginsList(excludes)
        included_parsers_and_plugins = self._GetParserAndPluginsList(includes)

        # Note that below set comprehension is used to determine the set of
        # excluded and included parsers.
        excluded_parsers = {
            name for name, plugins in excludes.items() if plugins != set(["*"])
        }
        included_parsers = {name for name, plugins in includes.items() if plugins}
        missing_parser_filters = excluded_parsers.difference(included_parsers)
        if missing_parser_filters:
            missing_parser_filters = self._GetParserAndPluginsList(
                {name: excludes[name] for name in missing_parser_filters}
            )
            filters_string = ",".join(missing_parser_filters)
            raise RuntimeError(
                f"Parser filters: {filters_string:s} defined to be excluded but no "
                f"corresponding include expression"
            )

        overlapping_parser_filters = set(included_parsers_and_plugins).intersection(
            set(excluded_parsers_and_plugins)
        )
        if overlapping_parser_filters:
            included_parsers_and_plugins = sorted(
                set(included_parsers_and_plugins) - overlapping_parser_filters
            )

        parser_filters = [
            f"!{parser_filter:s}" for parser_filter in excluded_parsers_and_plugins
        ]
        parser_filters.extend(included_parsers_and_plugins)
        return ",".join(parser_filters)

    def _ExpandPreset(self, presets_manager, preset_name, parsers_and_plugins):
        """Expands a preset in the parsers and plugins.

        This functions replaces the preset in parsers_and_plugins with the parser
        and plugin or preset names defined by the preset.

        Args:
          presets_manager (ParserPresetsManager): a parser preset manager, that
              is used to resolve which parsers and/or plugins are defined by
              presets.
          preset_name (str): name of the preset to expand.
          parsers_and_plugins (dict[str, set[str]]): parsers, plugins and presets.

        Raises:
          RuntimeError: if the plugins list of a preset in parsers_and_plugins,
              contains plugin definitions other than '*'.
        """
        if preset_name not in parsers_and_plugins:
            return

        plugins = parsers_and_plugins.get(preset_name)
        if plugins != set(["*"]):
            raise RuntimeError(
                f"{preset_name:s} cannot be used as a preset name and plugin name at "
                f"same time."
            )

        del parsers_and_plugins[preset_name]

        for parser_filter in presets_manager.GetParsersByPreset(preset_name):
            parser, _, plugin = parser_filter.partition("/")
            if not plugin:
                plugin = "*"

            parsers_and_plugins.setdefault(parser, set())
            parsers_and_plugins[parser].add(plugin)

    def _ExpandPresets(self, presets_manager, preset_names, parsers_and_plugins):
        """Expands the presets in the parsers and plugins.

        This functions replaces the presets in parsers_and_plugins with the parser
        and plugin or preset names defined by the presets.

        Args:
          presets_manager (ParserPresetsManager): a parser preset manager, that
              is used to resolve which parsers and/or plugins are defined by
              presets.
          preset_names (set[str]): names of the presets defined by the presets
              manager.
          parsers_and_plugins (dict[str, set[str]]): parsers, plugins and presets.
        """
        for preset_name in preset_names:
            self._ExpandPreset(presets_manager, preset_name, parsers_and_plugins)

    def ExpandPresets(self, presets_manager, expression):
        """Expands all presets in a parser filter expression.

        Args:
          presets_manager (ParserPresetsManager): a parser preset manager, that
              is used to resolve which parsers and/or plugins are defined by
              presets.
          expression (str): parser filter expression, where an empty expression
              represents all parsers and plugins.

              A parser filter expression is a comma separated value string that
              denotes which parsers and plugins should be used. Each element can be
              either:

              * The name of a preset (case sensitive), which is a predefined list of
                parsers and/or plugins (see data/presets.yaml for the default
                presets).
              * The name of a parser (case insensitive), for example 'msiecf'.
              * The name of a plugin, prefixed with the parser name and a '/', for
                example 'sqlite/chrome_history'.

              If the element begins with an exclamation mark ('!') the item will
              be excluded from the set of enabled parsers and plugins, otherwise
              the element will be included.

        Returns:
          str: a parser filter expression where presets have been expanded or None
              to represent all parsers and plugins.
        """
        if not expression:
            return None

        preset_names = set(presets_manager.GetNames())

        excludes, includes = self.SplitExpression(expression)

        while set(excludes.keys()).intersection(preset_names):
            self._ExpandPresets(presets_manager, preset_names, excludes)

        while set(includes.keys()).intersection(preset_names):
            self._ExpandPresets(presets_manager, preset_names, includes)

        return self._JoinExpression(excludes, includes)

    def SplitExpression(self, expression):
        """Determines the excluded and included elements in an expression string.

        This method will not expand presets, and preset names are treated like
        parser names.

        Args:
          expression (str): parser filter expression.

              A parser filter expression is a comma separated value string that
              denotes which parsers and plugins should be used. Each element can be
              either:

              * The name of a preset (case sensitive), which is a predefined list of
                parsers and/or plugins (see data/presets.yaml for the default
                presets).
              * The name of a parser (case insensitive), for example 'msiecf'.
              * The name of a plugin, prefixed with the parser name and a '/', for
                example 'sqlite/chrome_history'.

              If the element begins with an exclamation mark ('!') the item will
              be excluded from the set of enabled parsers and plugins, otherwise
              the element will be included.

        Returns:
          tuple: containing:

            excludes (dict[str, set[str]]): excluded presets, plugins and presets.
                Dictionary keys are preset and/or parser names, and values are
                sets containing plugin names to enable for a parser or an asterisk
                character ('*') to represent all plugins, or that no specific
                plugins were specified.
            includes (dict[str, set[str]]): included presets, parsers and plugins.
                Dictionary keys are preset and/or parser names, and values are
                sets containing plugin names to enable for a parser or an asterisk
                character ('*') to represent all plugins, or that no specific
                plugins were specified.
        """
        if not expression:
            return {}, {}

        excludes = {}
        includes = {}

        for expression_element in expression.split(","):
            expression_element = expression_element.strip()
            if not expression_element:
                continue

            expression_element = expression_element.lower()

            if expression_element.startswith("!"):
                expression_element = expression_element[1:]
                parsers_and_plugins = excludes
            else:
                parsers_and_plugins = includes

            parser, _, plugin = expression_element.partition("/")
            if not plugin:
                plugin = "*"

            parsers_and_plugins.setdefault(parser, set())
            parsers_and_plugins[parser].add(plugin)

        return excludes, includes
