"""The parsers and plugins interface classes."""

import abc
import os

from plaso.lib import errors


class BaseFileEntryFilter:
  """File entry filter interface."""

  # pylint: disable=redundant-returns-doc
  @abc.abstractmethod
  def Match(self, file_entry):
    """Determines if a file entry matches the filter.

    Args:
      file_entry (dfvfs.FileEntry): a file entry.

    Returns:
      bool: True if the file entry matches the filter.
    """


class FileNameFileEntryFilter(BaseFileEntryFilter):
  """File name file entry filter."""

  def __init__(self, filename):
    """Initializes a file entry filter.

    Args:
      filename (str): name of the file.
    """
    super().__init__()
    self._filename = filename.lower()

  def Match(self, file_entry):
    """Determines if a file entry matches the filter.

    Args:
      file_entry (dfvfs.FileEntry): a file entry.

    Returns:
      bool: True if the file entry matches the filter.
    """
    if not file_entry:
      return False

    filename = file_entry.name.lower()
    return filename == self._filename


class BaseParser:
  """The parser interface."""

  # The name of the parser. This is the name that is used in the registration
  # and used for parser/plugin selection, so this needs to be concise and unique
  # for all plugins/parsers, such as 'Chrome', 'Safari' or 'UserAssist'.
  NAME = 'base_parser'

  # Data format supported by the parser plugin. This information is used by
  # the parser manager to generate parser and plugin information.
  DATA_FORMAT = ''

  # List of filters that should match for the parser to be applied.
  FILTERS = frozenset()

  ALL_PLUGINS = set(['*'])

  # Every derived parser class that implements plugins should define
  # its own _plugin_classes dict:
  # _plugin_classes = {}

  # We deliberately don't define it here to make sure the plugins of
  # different parser classes don't end up in the same dict.
  _plugin_classes = None

  def __init__(self):
    """Initializes a parser.

    By default all plugins will be enabled. To only enable specific plugins use
    the EnablePlugins method and pass it a list of strings containing the names
    of the plugins to enable.

    The default plugin, named "{self.NAME:s}_default", if it exists, is always
    enabled and cannot be disabled.
    """
    super().__init__()
    self._default_plugin = None
    self._default_plugin_name = f'{self.NAME:s}_default'
    self._plugins_per_name = None
    self.EnablePlugins(self.ALL_PLUGINS)

  @classmethod
  def DeregisterPlugin(cls, plugin_class):
    """Deregisters a plugin class.

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

    Args:
      plugin_class (type): class of the plugin.

    Raises:
      KeyError: if plugin class is not set for the corresponding name.
    """
    plugin_name = plugin_class.NAME.lower()
    if plugin_name not in cls._plugin_classes:
      raise KeyError(f'Plugin class not set for name: {plugin_class.NAME:s}')

    del cls._plugin_classes[plugin_name]

  def EnablePlugins(self, plugin_includes):
    """Enables parser plugins.

    Args:
      plugin_includes (set[str]): names of the plugins to enable, where
          set(['*']) represents all plugins. Note the default plugin, if
          it exists, is always enabled and cannot be disabled.
    """
    self._plugins_per_name = {}
    if not self._plugin_classes:
      return

    for plugin_name, plugin_class in self._plugin_classes.items():
      if plugin_name == self._default_plugin_name:
        self._default_plugin = plugin_class()
        continue

      if (plugin_includes != self.ALL_PLUGINS and
          plugin_name not in plugin_includes):
        continue

      plugin_object = plugin_class()
      self._plugins_per_name[plugin_name] = plugin_object

  # TODO: move this to a filter.
  # pylint: disable=redundant-returns-doc
  @classmethod
  def GetFormatSpecification(cls):
    """Retrieves the format specification.

    Returns:
      FormatSpecification: a format specification or None if not available.
    """
    return None

  @classmethod
  def GetPluginNames(cls):
    """Retrieves the names of registered plugins.

    Returns:
      list[str]: names of the plugins.
    """
    return list(cls._plugin_classes.keys())

  @classmethod
  def GetPluginObjectByName(cls, plugin_name):
    """Retrieves a specific plugin object by its name.

    Args:
      plugin_name (str): name of the plugin.

    Returns:
      BasePlugin: a plugin object or None if not available.
    """
    plugin_class = cls._plugin_classes.get(plugin_name)
    if plugin_class:
      return plugin_class()

    return None

  @classmethod
  def GetPlugins(cls):
    """Retrieves the registered plugins.

    Yields:
      tuple[str, type]: name and class of the plugin.
    """
    for plugin_name, plugin_class in cls._plugin_classes.items():
      yield plugin_name, plugin_class

  @classmethod
  def RegisterPlugin(cls, plugin_class):
    """Registers a plugin class.

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

    Args:
      plugin_class (type): class of the plugin.

    Raises:
      KeyError: if plugin class is already set for the corresponding name.
    """
    plugin_name = plugin_class.NAME.lower()
    if plugin_name in cls._plugin_classes:
      raise KeyError(
          f'Plugin class already set for name: {plugin_class.NAME:s}')

    cls._plugin_classes[plugin_name] = plugin_class

  @classmethod
  def RegisterPlugins(cls, plugin_classes):
    """Registers plugin classes.

    Args:
      plugin_classes (list[type]): classes of plugins.

    Raises:
      KeyError: if plugin class is already set for the corresponding name.
    """
    for plugin_class in plugin_classes:
      cls.RegisterPlugin(plugin_class)

  @classmethod
  def SupportsPlugins(cls):
    """Determines if a parser supports plugins.

    Returns:
      bool: True if the parser supports plugins.
    """
    return cls._plugin_classes is not None


class FileEntryParser(BaseParser):
  """The file entry parser interface."""

  def Parse(self, parser_mediator):
    """Parses a file entry.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.

    Raises:
      WrongParser: when the file cannot be parsed.
    """
    file_entry = parser_mediator.GetFileEntry()
    if not file_entry:
      raise errors.WrongParser('Invalid file entry')

    parser_mediator.AppendToParserChain(self.NAME)

    parser_chain = parser_mediator.GetParserChain()
    parser_mediator.SampleStartTiming(parser_chain)

    try:
      self.ParseFileEntry(parser_mediator, file_entry)

    finally:
      parser_mediator.SampleStopTiming(parser_chain)

      parser_mediator.PopFromParserChain()

  @abc.abstractmethod
  def ParseFileEntry(self, parser_mediator, file_entry):
    """Parses a file entry.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      file_entry (dfvfs.FileEntry): a file entry to parse.

    Raises:
      WrongParser: when the file cannot be parsed.
    """


class FileObjectParser(BaseParser):
  """The file-like object parser interface."""

  # The initial file offset. Set this value to None if no initial file offset
  # seek needs to be performed.
  _INITIAL_FILE_OFFSET = 0

  # The maximum file size supported by the parser. Set this value to None if no
  # file size check needs to be performed.
  _MAXIMUM_FILE_SIZE = None

  # The minimum file size supported by the parser. Set this value to None if no
  # file size check needs to be performed.
  _MINIMUM_FILE_SIZE = None

  def Parse(self, parser_mediator, file_object):
    """Parses a single file-like object.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      file_object (dfvfs.FileIO): a file-like object to parse.

    Raises:
      WrongParser: when the file cannot be parsed.
    """
    if not file_object:
      raise errors.WrongParser('Invalid file object')

    file_size = file_object.get_size()
    if file_size == 0:
      return

    if (self._MINIMUM_FILE_SIZE is not None and
        file_size < self._MINIMUM_FILE_SIZE):
      raise errors.WrongParser(
          f'File size: {file_size:d} too small, minimum: '
          f'{self._MINIMUM_FILE_SIZE:d}')

    if (self._MAXIMUM_FILE_SIZE is not None and
        file_size > self._MAXIMUM_FILE_SIZE):
      raise errors.WrongParser(
          f'File size: {file_size:d} too large, maximum: '
          f'{self._MAXIMUM_FILE_SIZE:d}')

    if self._INITIAL_FILE_OFFSET is not None:
      file_object.seek(self._INITIAL_FILE_OFFSET, os.SEEK_SET)

    parser_mediator.AppendToParserChain(self.NAME)

    parser_chain = parser_mediator.GetParserChain()
    parser_mediator.SampleStartTiming(parser_chain)

    try:
      self.ParseFileObject(parser_mediator, file_object)

    finally:
      parser_mediator.SampleStopTiming(parser_chain)

      parser_mediator.PopFromParserChain()

  @abc.abstractmethod
  def ParseFileObject(self, parser_mediator, file_object):
    """Parses a file-like object.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      file_object (dfvfs.FileIO): a file-like object to parse.

    Raises:
      WrongParser: when the file cannot be parsed.
    """
