# -*- coding: utf8 -*-
# Copyright (c) 2019 Niklas Rosenstein
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

import dataclasses
import typing as t

import docspec

from pydoc_markdown.interfaces import Processor, Resolver


@dataclasses.dataclass
class FilterProcessor(Processor):
  """
  The `filter` processor removes module and class members based on certain criteria.

  Example configuration:

  ```yaml
  - type: filter
    expression: not name.startswith('_') and default()
    documented_only: false
  ```

  ### Options
  """

  #: A Python expression that is evaluated given the variables `name`, `obj` and `default`
  #: and is expected to return a boolean to indicate whether the #docspec.ApiObject should
  #: be kept or removed. If specified, the expression is the ultimate truth for determining
  #: the keep-or-remove state of a node. Using `'default()'` as the expression has the same
  #: semantic as not specifying this field. Default: `null`
  expression: t.Optional[str] = None

  #: Keep only API objects that have docstrings. Default: `true`
  documented_only: bool = True

  #: Exclude API objects that appear to be private members (i.e. their name begins with
  #: and underscore but does not end with one). Default: `true`
  exclude_private: bool = True

  #: Exclude special members (e.g.` __path__`, `__annotations__`, `__name__` and `__all__`).
  #: Default: `true`
  exclude_special: bool = True

  #: Do not filter #docspec.Module objects. Default: `true`
  do_not_filter_modules: bool = True

  #: Skip modules with no content. Default: `false`.
  skip_empty_modules: bool = False

  SPECIAL_MEMBERS = ('__path__', '__annotations__', '__name__', '__all__')

  def process(self, modules: t.List[docspec.Module], resolver: t.Optional[Resolver]) -> None:
    def m(obj):
      return self._match(obj)
    docspec.filter_visit(t.cast(t.List[docspec.ApiObject], modules), m, order='post')

  def _match(self, obj: docspec.ApiObject) -> bool:
    members = getattr(obj, 'members', [])

    def _check():
      if members:
        return True
      if self.skip_empty_modules and isinstance(obj, docspec.Module) and not members:
        return False
      if self.do_not_filter_modules and isinstance(obj, docspec.Module):
        return True
      if self.documented_only and not obj.docstring:
        return False
      if self.exclude_private and obj.name.startswith('_') and not obj.name.endswith('_'):
        return False
      if self.exclude_special and obj.name in self.SPECIAL_MEMBERS:
        return False
      return True

    if self.expression:
      scope = {'name': obj.name, 'obj': obj, 'default': _check}
      return bool(eval(self.expression, scope))  # pylint: disable=eval-used

    return _check()
