"""Filters parsed code to exclude methods or attributes that have certain
types as input parameters.

This module is available as the xdress plugin ``xdress.descfilter``.

:author: Spencer Lyon <spencerlyon2@gmail.com>

Filtering
=========

This plugin alters the description dictionary generated by other xdress
plugins (mainly ``xdress.autodescribe``) by removing attributes or
methods that contain argument types the user wishes to exclude from the
generated cython wrapper. To use ``xdress.descfilter``, you need to do
two things in the xdressrc file for your project.

1. Add it to the list of plugins
2. Define one or more of the following:

   a. ``skiptpyes``, a  list or dictionary. If ``skiptypes`` is a
      dictionary the keys are class names and the values are lists (or
      tuples) of data types that should be left out of the generated
      code for the class. If ``skiptypes`` is a list or tuple, the
      skipped types will be applied to all classes listed in the
      ``classes`` list in xdressrc. If ``skiptypes`` is empty or
      ``None`` this plugin will not try to filter any types.
   b. ``skipmethods`` dictionary. The keys for this dictionary
      are the class names and the values are a list of method names that
      should be excluded from the wrapper.

.. warning::

    It is important that ``xdress.descfilter`` comes after
    ``xdress.autoall``, and ``xdress.autodescribe`` but before
    ``xdress.cythongen``. This is necessary because the description
    dictionary needs to be populated by autodescribe before descfilter
    can act on it, but it also must do the filtering before cythongen
    generates any code.

Type Filtering Example
======================

To filter specific types you might do something like this in xdressrc::

    # Declare use of plugin (point 1)
    plugins = ('xdress.stlwrap', 'xdress.autoall', 'xdress.autodescribe',
               'xdress.descfilter', 'xdress.cythongen')

    # Specify skiptypes (point 2)
    skiptypes = {'classA': ['float64', (('int32', 'const'), '&')],
                 'classB': ['classA', ('vector', 'float32')]}

I also could have done the following to exclude all instances of
particular types, regardless of the class in which they arise.

    skiptypes = ['uint32', (('vector', 'float64', 'const'), '&')]

A Closer Look
-------------

As of right now ``xdress.descfilter`` is set up to handle skiptype
elements of two flavors.

1. A single type identifier. This could be any base type,  (e.g. int32,
   char, float64, ect), an STL type (e.g. vector, map, set), or any type
   xdress knows about (like classA in the first example above). In this
   case xdress will flatten all argument types and if the single type
   identifier appears anywhere in the flattened argument description,
   the method will be filtered out.

   For example, if 'float64' were in the ``skiptypes`` it would catch any
   of the following argument types (this is by no means a complete list)::

        "float64"
        (('vector', 'float64', 'const'), '&')
        ('set', 'float64')

2. A specific argument or return type that will match exactly. This
   option provides more control over what ``xdress.descfilter`` will
   catch and can prevent the plugin from being to ambitious with regards
   to the methods that are filtered out.

Typically, the first option would be used in situations where xdress,
for whatever reason, cannot create a wrapper for a user-defined C++
class. This might happen due to limitations with xdress itself, or
perhaps limitations with Cython.

Users are advised to use the second option in most other circumstances
in order to forestall any potential issues with needed methods not
being wrapped.

Method Filtering Example
========================

Suppose, in the C++ source, I had a class ``Computer`` with the
following methods:

    checkEmail
    turnOn
    blowUp
    runXdress
    sleep
    crash

Now, if I didn't want python users to have access to the ``blowUp``,
``sleep``, or ``crash`` methods, I would put the following in my
xdressrc file::

    # Declare use of plugin (point 1)
    plugins = ('xdress.stlwrap', 'xdress.autoall', 'xdress.autodescribe',
               'xdress.methodfilter', 'xdress.cythongen')

    # Specify methods to skip (point 2)
    skipmethods = {'Computer': ['blowUp', 'sleep', 'crash']}

Description Filtering API
=========================
"""
from __future__ import print_function
import sys
import collections
from .utils import isclassdesc, NotSpecified
from .typesystem import TypeMatcher
from .plugins import Plugin

if sys.version_info[0] >= 3:
    basestring = str

def modify_desc(skips, desc):
    """Deletes specified methods from a class description (desc).

    Parameters
    ----------
    skips : dict or list
        The attribute rc.skiptypes from the run controller managing
        the desc dictionary. This is filled with
        xdress.typesystem.TypeMatcher objects and should have been
        populated as such by xdress.descfilter.setup

    desc : dictionary
        The class dictionary that is to be altered or tested to see
        if any methods need to be removed.

    """
    # remove attrs with bad types
    for at_name, at_t in desc['attrs'].copy().items():
        for tm in skips:
            if tm.flatmatches(at_t):
                del desc['attrs'][at_name]
                break

    # remove methods with bad parameter types or return types
    for m_key, m_ret in desc['methods'].copy().items():
        _deleted = False
        # Check return types
        for tm in skips:
            if tm.flatmatches(m_ret):
                del desc['methods'][m_key]
                _deleted = True
                break

        # Stop here if we already got it
        if _deleted:
            continue

        m_args = m_key[1:]

        for arg in m_args:
            t = arg[1]  # Just use type, not parameter name or default val
            for tm in skips:
                # c1 = tm.flatmatches(t)
                # c2 =
                if tm.flatmatches(t):
                    del desc['methods'][m_key]
                    _deleted = True
                    break

            if _deleted:
                break


class XDressPlugin(Plugin):
    """Plugin for filtering API description dictionaries."""

    # Require base, autoall, and autodescribe so that rc.env is populated
    requires = ('xdress.autodescribe',)

    defaultrc = {'skiptypes': NotSpecified,
                 'skipmethods': NotSpecified}

    rcdocs = {
        'skiptypes': 'The types to filter out from being wrapped',
        'skipmethods': 'Method names to filter out from being wrapped',
        }

    def setup(self, rc):
        if rc.skiptypes is NotSpecified:
            return
        if isinstance(rc.skiptypes, collections.Mapping):
            # Update dict so everything is a TypeMatcher instance
            _skippers = {}
            for kls in rc.skiptypes.keys():
                _skippers[kls] = [TypeMatcher(t) for t in rc.skiptypes[kls]]
            rc.skiptypes = _skippers
        elif isinstance(rc.skiptypes, collections.Sequence):
            # Update tuple or list to be full of TypeMatchers
            rc.skiptypes = [TypeMatcher(t) for t in rc.skiptypes]
            if rc.verbose:
                print("descfilter: skipping these types: {0}".format(rc.skiptypes))

    def skip_types(self, rc):
        if rc.skiptypes is NotSpecified:
            return
        print("descfilter: removing unwanted types from desc dictionary")
        if isinstance(rc.skiptypes, collections.Mapping):
            skip_classes = rc.skiptypes.keys()
            for mod_key, mod in rc.env.items():
                for kls_key, desc in mod.items():
                    if isclassdesc(desc):
                        if desc['name'] in skip_classes:
                            # Pull out skiptypes
                            skips = rc.skips[desc['name']]
                            # let modify_desc remove unwanted methods
                            modify_desc(skips, desc)
        elif isinstance(rc.skiptypes, collections.Sequence):
            for mod_key, mod in rc.env.items():
                for kls_key, desc in mod.items():
                    if isclassdesc(desc):
                        skips = rc.skiptypes
                        modify_desc(skips, desc)

    def skip_methods(self, rc):
        if rc.skipmethods is NotSpecified:
            return
        print("descfilter: removing unwanted methods from desc dictionary")
        skip_classes = rc.skipmethods.keys()
        for m_key, mod in rc.env.items():
            for k_key, kls_desc in mod.items():
                if isclassdesc(kls_desc):
                    if kls_desc['name'] in skip_classes:
                        skippers = rc.skipmethods[k_key]
                        m_nms = rc.env[m_key][k_key]['methods'].keys()
                        for m in skippers:
                            # Find method key
                            # import pdb; pdb.set_trace()
                            try:
                                del_key = filter(lambda x: x[0].startswith(m),
                                                 m_nms)[0]
                            except IndexError:
                                msg = 'descfilter: Could not find method {0} '
                                msg += 'in {1}. Moving on to next method'
                                print(msg.format(m, k_key))
                                continue
                            # Remove that method
                            del rc.env[m_key][k_key]['methods'][del_key]

    def execute(self, rc):
        self.skip_types(rc)
        self.skip_methods(rc)
