from typing import (Any, Callable, Generic, Iterable, Optional, Type, cast)

import copy
import functools
import operator

from .. import optics
from ..maybe import Just as mJust, Nothing as mNothing
from ..typevars import S, T, A, B, X, Y


def _carry_binary_op(name):
    def operation(self, other):
        def modifier(focus):
            return getattr(operator, name)(focus, other)

        return self.modify(modifier)

    return operation, 'self.modify(operator.{}, other)'.format(name)


def _carry_reverse_op(name):
    opname = name.replace('__r', '__')

    def operation(self, other):
        def modifier(focus):
            return getattr(operator, opname)(other, focus)

        return self.modify(modifier)

    doc = 'self.modify(lambda s, o: operator.{}(o, s), other)'.format(opname)
    return operation, doc


def _carry_unary_op(name):
    def operation(self):
        def modifier(focus):
            return getattr(operator, name)(focus)

        return self.modify(modifier)

    return operation, 'self.modify(operator.{})'.format(name)


def _add_extra_methods(cls):
    # type: (Type[BaseUiLens]) -> Type[BaseUiLens]
    binary = '''__lt__ __le__ __eq__ __ne__ __gt__ __ge__
                __add__ __sub__ __mul__ __matmul__ __truediv__
                __floordiv__ __div__ __mod__ __divmod__ __pow__
                __lshift__ __rshift__ __xor__ __or__'''.split()
    reverse = '''__radd__ __rsub__ __rmul__ __rmatmul__ __rtruediv__
                 __rfloordiv__ __rdiv__ __rmod__ __rdivmod__ __rpow__
                 __rlshift__ __rrshift__ __rxor__ __ror__'''.split()
    unary = '__neg__ __pos__ __invert__'.split()

    funcs = [_carry_binary_op, _carry_reverse_op, _carry_unary_op]
    for func, dunders in zip(funcs, [binary, reverse, unary]):
        for dunder in dunders:
            operation, doc = func(dunder)
            operation.__name__ = dunder
            operation.__doc__ = doc
            setattr(cls, dunder, operation)

    return cls


@_add_extra_methods
class BaseUiLens(Generic[S, T, A, B]):
    '''This class contains all the methods that are common to both
    the BoundLens and UnboundLens classes. It is not intended to be
    instantiated directly.'''

    __slots__ = ()

    def call(self, method_name, *args, **kwargs):
        # type: (str, *Any, **Any) -> T
        '''Call a method on the focus. The method must return a new
        value for the focus.

            >>> from lenses import lens
            >>> lens[2].call('upper')(['alpha', 'beta', 'gamma'])
            ['alpha', 'beta', 'GAMMA']

        As a shortcut, you can include the name of the method you want
        to call immediately after `call_`:

            >>> lens[2].call_upper()(['alpha', 'beta', 'gamma'])
            ['alpha', 'beta', 'GAMMA']
        '''
        caller = operator.methodcaller(method_name, *args, **kwargs)
        return self.modify(caller)

    def call_mut(self, method_name, *args, **kwargs):
        # type: (str, *Any, **Any) -> T
        '''Call a method on the focus that will mutate it in place.
        Works by making a deep copy of the focus before calling the
        mutating method on it. The return value of that method is ignored.
        You can pass a keyword argument shallow=True to only make a
        shallow copy.

            >>> from lenses import lens
            >>> lens[0].call_mut('sort')([[3, 1, 2], [5, 4]])
            [[1, 2, 3], [5, 4]]

        As a shortcut, you can include the name of the method you want
        to call immediately after `call_mut_`:

            >>> lens[0].call_mut_sort()([[3, 1, 2], [5, 4]])
            [[1, 2, 3], [5, 4]]
        '''
        shallow = False
        if 'shallow' in kwargs:
            shallow = kwargs['shallow']
            del kwargs['shallow']

        def func(a):
            # type: (A) -> B
            a = copy.copy(a) if shallow else copy.deepcopy(a)
            getattr(a, method_name)(*args, **kwargs)
            return cast(B, a)

        return self.modify(func)

    def bitwise_and(self, other):
        # type: (Any) -> T
        '''Uses the bitwise and operator on the focus. A convenience
        method since lenses use __and__ for doing composition.

            >>> from lenses import lens
            >>> lens.Each().bitwise_and(5)([1, 2, 3, 4])
            [1, 0, 1, 4]
        '''

        def func(a):
            # type: (A) -> B
            return a & other

        return self.modify(func)

    def Contains(self, item):
        # type: (A) -> BaseUiLens[S, S, bool, bool]
        '''A lens that focuses a boolean that tells you whether the
        state contains some item.

            >>> from lenses import lens
            >>> lens.Contains(1)
            UnboundLens(ContainsLens(1))
            >>> lens.Contains(1).get()([2, 3])
            False
            >>> lens.Contains(1).get()([1, 2, 3])
            True
            >>> lens.Contains(1).set(False)([1, 2, 3])
            [2, 3]
            >>> lens.Contains(1).set(True)([2, 3])
            [2, 3, 1]

        The behaviour of this lens depends on the implementation of
        ``lenses.hooks.contains_add`` and ``lenses.hooks.contains_remove``.
        '''
        return self._compose_optic(optics.ContainsLens(item))

    def Decode(self, encoding='utf-8', errors='strict'):
        # type: (str, str) -> BaseUiLens[S, T, X, Y]
        '''An isomorphism that decodes and encodes its focus on the
        fly. Lets you focus a byte string as a unicode string. The
        arguments have the same meanings as `bytes.decode`. Analogous to
        `bytes.decode`.

            >>> from lenses import lens
            >>> lens.Decode(encoding='utf8')
            UnboundLens(DecodeIso('utf8', 'strict'))
            >>> lens.Decode().get()(b'hello')  # doctest: +SKIP
            'hello'
            >>> lens.Decode().set('world')(b'hello')  # doctest: +SKIP
            b'world'
        '''
        return self._compose_optic(optics.DecodeIso(encoding, errors))

    def Each(self):
        # type: () -> BaseUiLens[S, T, X, Y]
        '''A traversal that iterates over its state, focusing everything
        it iterates over. It uses `lenses.hooks.fromiter` to reform
        the state afterwards so it should work with any iterable that
        function supports. Analogous to `iter`.

            >>> from lenses import lens
            >>> data = [1, 2, 3]
            >>> lens.Each()
            UnboundLens(EachTraversal())
            >>> lens.Each().collect()(data)
            [1, 2, 3]
            >>> (lens.Each() + 1)(data)
            [2, 3, 4]

        For technical reasons, this lens iterates over dictionaries by
        their items and not just their keys.

            >>> data = {'one': 1}
            >>> lens.Each().collect()(data)
            [('one', 1)]
            >>> (lens.Each()[1] + 1)(data)
            {'one': 2}
        '''
        return self._compose_optic(optics.EachTraversal())

    def Error(self, exception, message=None):
        # type: (Exception, Optional[str]) -> BaseUiLens[S, T, X, Y]
        '''An optic that raises an exception whenever it tries to focus
        something. If `message is None` then the exception will be
        raised unmodified. If `message is not None` then when the lens
        is asked to focus something it will run `message.format(state)`
        and the exception will be called with the resulting formatted
        message as it's only argument. Useful for debugging.

            >>> from lenses import lens
            >>> lens.Error(Exception())
            UnboundLens(ErrorIso(Exception()))
            >>> lens.Error(Exception, '{}')
            UnboundLens(ErrorIso(<...Exception...>, '{}'))
            >>> lens.Error(Exception).get()(True)
            Traceback (most recent call last):
              File "<stdin>", line 1, in ?
            Exception
            >>> lens.Error(Exception('An error occurred')).set(False)(True)
            Traceback (most recent call last):
              File "<stdin>", line 1, in ?
            Exception: An error occurred
            >>> lens.Error(ValueError, 'applied to {}').get()(True)
            Traceback (most recent call last):
              File "<stdin>", line 1, in ?
            ValueError: applied to True
        '''
        return self._compose_optic(optics.ErrorIso(exception, message))

    def F(self, getter):
        # type: (Callable[[A], X]) -> BaseUiLens[S, T, X, Y]
        '''An optic that wraps a getter function. A getter function is
        one that takes a state and returns a value derived from that
        state. The function is called on the focus before it is returned.

            >>> from lenses import lens
            >>> lens.F(abs)
            UnboundLens(Getter(<built-in function abs>))
            >>> lens.F(abs).get()(-1)
            1
            >>> lens.Each().F(abs).collect()([-1, 2, -3])
            [1, 2, 3]

        This optic cannot be used to set or modify values.
        '''
        return self._compose_optic(optics.Getter(getter))

    def Filter(self, predicate):
        # type: (Callable[[A], bool]) -> BaseUiLens[S, T, X, Y]
        '''A prism that only focuses a value if the predicate returns
        `True` when called with that value as an argument. Best used
        when composed after a traversal. It only prevents the traversal
        from visiting foci, it does not filter out values the way that
        python's regular `filter` function does.

            >>> from lenses import lens
            >>> lens.Filter(all)
            UnboundLens(FilteringPrism(<built-in function all>))
            >>> data = [[1, 2], [0], ['a'], ['', 'b']]
            >>> lens.Each().Filter(all).collect()(data)
            [[1, 2], ['a']]
            >>> lens.Each().Filter(all).set(2)(data)
            [2, [0], 2, ['', 'b']]

        The filtering is done to foci before the lens' manipulation is
        applied. This means that the resulting foci can still violate
        the predicate if the manipulating function doesn't respect it:

            >>> lens.Each().Filter(bool).set(None)(['', 2, ''])
            ['', None, '']
        '''
        return self._compose_optic(optics.FilteringPrism(predicate))

    def Fold(self, func):
        # type: (Callable[[A], Iterable[X]]) -> BaseUiLens[S, T, X, Y]
        '''A fold that takes a function that returns an iterable and
        focuses all the values in that iterable.

            >>> from lenses import lens
            >>> def ends(state):
            ...     yield state[0]
            ...     yield state[-1]
            >>> lens.Fold(ends).collect()([1, 2, 3])
            [1, 3]
        '''
        return self._compose_optic(optics.Fold(func))

    def Fork(self, *lenses):
        # type: (*BaseUiLens[A, B, X, Y])-> BaseUiLens[S, T, X, Y]
        '''A setter representing the parallel composition of several
        sub-lenses.

            >>> from lenses import lens
            >>> lens.Fork(lens[0], lens[2])
            UnboundLens(ForkedSetter(GetitemLens(0), GetitemLens(2)))
            >>> lens.Fork(lens[0][1], lens[2]).set(1)([[0, 0], 0, 0])
            [[0, 1], 0, 1]
        '''
        true_lenses = [l._optic for l in lenses]
        return self._compose_optic(optics.ForkedSetter(*true_lenses))

    def Get(self, key, default=None):
        # type: (Any, Optional[Y]) -> BaseUiLens[S, T, X, Y]
        '''A lens that focuses an item inside a container by calling
        its `get` method, allowing you to specify a default value for
        missing keys.  Analogous to `dict.get`.

            >>> from lenses import lens
            >>> lens.Get('foo')
            UnboundLens(GetitemOrElseLens('foo', default=None))
            >>> lens.Get('baz').get()({'foo': 'bar'})
            >>> lens.Get('baz', []).get()({'foo': 'bar'})
            []
            >>> from collections import OrderedDict
            >>> lens.Get('baz').set('qux')(OrderedDict({'foo': 'bar'}))
            OrderedDict([('foo', 'bar'), ('baz', 'qux')])
        '''
        return self._compose_optic(optics.GetitemOrElseLens(key, default))

    def GetAttr(self, name):
        # type: (str) -> BaseUiLens[S, T, X, Y]
        '''A lens that focuses an attribute of an object. Analogous to
        `getattr`.

            >>> from lenses import lens
            >>> from collections import namedtuple
            >>> Pair = namedtuple('Pair', 'left right')
            >>> lens.GetAttr('left')
            UnboundLens(GetattrLens('left'))
            >>> lens.GetAttr('left').get()(Pair(1, 2))
            1
            >>> lens.GetAttr('right').set(3)(Pair(1, 2))
            Pair(left=1, right=3)
        '''
        return self._compose_optic(optics.GetattrLens(name))

    def GetItem(self, key):
        # type: (Any) -> BaseUiLens[S, T, X, Y]
        '''A lens that focuses an item inside a container. Analogous to
        `operator.itemgetter`.

            >>> from lenses import lens
            >>> lens[0]
            UnboundLens(GetitemLens(0))
            >>> lens.GetItem(0)
            UnboundLens(GetitemLens(0))
            >>> lens[0].get()([1, 2, 3])
            1
            >>> lens['hello'].get()({'hello': 'world'})
            'world'
            >>> lens[0].set(4)([1, 2, 3])
            [4, 2, 3]
            >>> lens['hello'].set('universe')({'hello': 'world'})
            {'hello': 'universe'}
        '''
        return self._compose_optic(optics.GetitemLens(key))

    def Lens(self, getter, setter):
        # type: (Callable[[A], X], Callable[[A, Y], B]) -> BaseUiLens[S, T, X, Y]
        '''An optic that wraps a pair of getter and setter functions. A
        getter function is one that takes a state and returns a value
        derived from that state. A setter function takes an old state
        and a new value and uses them to construct a new state.

            >>> from lenses import lens
            >>> def getter(state):
            ...     'Get the average of a list'
            ...     return sum(state) // len(state)
            ...
            >>> def setter(old_state, value):
            ...     'Set the average of a list by changing the final value'
            ...     target_sum = value * len(old_state)
            ...     prefix = old_state[:-1]
            ...     return prefix + [target_sum - sum(prefix)]
            ...
            >>> average_lens = lens.Lens(getter, setter)
            >>> average_lens
            UnboundLens(Lens(<function getter...>, <function setter...>))
            >>> average_lens.get()([1, 2, 4, 5])
            3
            >>> average_lens.set(4)([1, 2, 3])
            [1, 2, 9]
            >>> (average_lens - 1)([1, 2, 3])
            [1, 2, 0]
        '''
        return self._compose_optic(optics.Lens(getter, setter))

    def GetZoomAttr(self, name):
        # type: (str) -> BaseUiLens[S, T, X, Y]
        '''A traversal that focuses an attribute of an object, though if
        that attribute happens to be a lens it will zoom the lens. This
        is used internally to make lenses that are attributes of objects
        transparent. If you already know whether you are focusing a lens
        or a non-lens you should be explicit and use a ZoomAttrTraversal
        or a GetAttrLens respectively.

            >>> from lenses import lens
            >>> from collections import namedtuple
            >>> Triple = namedtuple('Triple', 'left mid right')
            >>> state = Triple(1, 2, lens.mid)
            >>> lens.left
            UnboundLens(GetZoomAttrTraversal('left'))
            >>> lens.left.get()(state)
            1
            >>> lens.left.set(3)(state)
            Triple(left=3, mid=2, right=UnboundLens(GetZoomAttrTraversal('mid')))
            >>> lens.right.get()(state)
            2
            >>> lens.right.set(4)(state)
            Triple(left=1, mid=4, right=UnboundLens(GetZoomAttrTraversal('mid')))
        '''
        return self._compose_optic(optics.GetZoomAttrTraversal(name))

    def Instance(self, type_):
        # type: (Type) -> BaseUiLens[S, T, X, Y]
        '''A prism that focuses a value only when that value is an
        instance of `type_`.

            >>> from lenses import lens
            >>> lens.Instance(int)
            UnboundLens(InstancePrism(...))
            >>> lens.Instance(int).collect()(1)
            [1]
            >>> lens.Instance(float).collect()(1)
            []
            >>> lens.Instance(int).set(2)(1)
            2
            >>> lens.Instance(float).set(2)(1)
            1
        '''
        return self._compose_optic(optics.InstancePrism(type_))

    def Iso(self, forwards, backwards):
        # type: (Callable[[A], X], Callable[[Y], B]) -> BaseUiLens[S, T, X, Y]
        '''A lens based on an isomorphism. An isomorphism can be
        formed by two functions that mirror each other; they can convert
        forwards and backwards between a state and a focus without losing
        information. The difference between this and a regular Lens is
        that here the backwards functions don't need to know anything
        about the original state in order to produce a new state.

        These equalities should hold for the functions you supply (given
        a reasonable definition for __eq__)::

            backwards(forwards(state)) == state
            forwards(backwards(focus)) == focus

        These kinds of conversion functions are very common across
        the python ecosystem. For example, NumPy has `np.array` and
        `np.ndarray.tolist` for converting between python lists and its
        own arrays. Isomorphism makes it easy to store data in one form,
        but interact with it in a more convenient form.

            >>> from lenses import lens
            >>> lens.Iso(chr, ord)
            UnboundLens(Isomorphism(<... chr>, <... ord>))
            >>> lens.Iso(chr, ord).get()(65)
            'A'
            >>> lens.Iso(chr, ord).set('B')(65)
            66

        Due to their symmetry, isomorphisms can be flipped, thereby
        swapping thier forwards and backwards functions:

            >>> flipped = lens.Iso(chr, ord).flip()
            >>> flipped
            UnboundLens(Isomorphism(<... ord>, <... chr>))
            >>> flipped.get()('A')
            65
        '''
        return self._compose_optic(optics.Isomorphism(forwards, backwards))

    def Item(self, key):
        # type: (Any) -> BaseUiLens[S, T, X, Y]
        '''A lens that focuses a single item (key-value pair) in a
        dictionary by its key. Set an item to `None` to remove it from
        the dictionary.

            >>> from lenses import lens
            >>> from collections import OrderedDict
            >>> data = OrderedDict([(1, 10), (2, 20)])
            >>> lens.Item(1)
            UnboundLens(ItemLens(1))
            >>> lens.Item(1).get()(data)
            (1, 10)
            >>> lens.Item(3).get()(data) is None
            True
            >>> lens.Item(1).set((1, 11))(data)
            OrderedDict([(1, 11), (2, 20)])
            >>> lens.Item(1).set(None)(data)
            OrderedDict([(2, 20)])
        '''
        return self._compose_optic(optics.ItemLens(key))

    def ItemByValue(self, value):
        # type: (Any) -> BaseUiLens[S, T, X, Y]
        '''A lens that focuses a single item (key-value pair) in a
        dictionary by its value. Set an item to `None` to remove it
        from the dictionary. This lens assumes that there will only be
        a single key with that particular value. If you violate that
        assumption then you're on your own.

            >>> from lenses import lens
            >>> from collections import OrderedDict
            >>> data = OrderedDict([(1, 10), (2, 20)])
            >>> lens.ItemByValue(10)
            UnboundLens(ItemByValueLens(10))
            >>> lens.ItemByValue(10).get()(data)
            (1, 10)
            >>> lens.ItemByValue(30).get()(data) is None
            True
            >>> lens.ItemByValue(10).set((3, 10))(data)
            OrderedDict([(2, 20), (3, 10)])
            >>> lens.ItemByValue(10).set(None)(data)
            OrderedDict([(2, 20)])
        '''
        return self._compose_optic(optics.ItemByValueLens(value))

    def Items(self):
        # type: () -> BaseUiLens[S, T, X, Y]
        '''A traversal focusing key-value tuples that are the items of
        a dictionary. Analogous to `dict.items`.

            >>> from lenses import lens
            >>> from collections import OrderedDict
            >>> data = OrderedDict([(1, 10), (2, 20)])
            >>> lens.Items()
            UnboundLens(ItemsTraversal())
            >>> lens.Items().collect()(data)
            [(1, 10), (2, 20)]
            >>> lens.Items()[1].modify(lambda n: n + 1)(data)
            OrderedDict([(1, 11), (2, 21)])
        '''
        return self._compose_optic(optics.ItemsTraversal())

    def Iter(self):
        # type: () -> BaseUiLens[S, T, X, Y]
        '''A fold that can get values from any iterable object in python
        by iterating over it. Like any fold, you cannot set values.

            >>> from lenses import lens
            >>> lens.Iter()
            UnboundLens(IterableFold())
            >>> data = {2, 1, 3}
            >>> lens.Iter().collect()(data) == list(data)
            True
            >>> def numbers():
            ...     yield 1
            ...     yield 2
            ...     yield 3
            ...
            >>> lens.Iter().collect()(numbers())
            [1, 2, 3]
            >>> lens.Iter().collect()([])
            []

        If you want to be able to set values as you iterate then look
        into the EachTraversal.
        '''
        return self._compose_optic(optics.IterableFold())

    def Json(self):
        # type: () -> BaseUiLens[S, T, X, Y]
        '''An isomorphism that focuses a string containing json data as
        its parsed equivalent. Analogous to `json.loads`.

            >>> from lenses import lens
            >>> data = '[{"points": [4, 7]}]'
            >>> lens.Json()
            UnboundLens(JsonIso())
            >>> lens.Json()[0]['points'][1].get()(data)
            7
            >>> lens.Json()[0]['points'][0].set(8)(data)
            '[{"points": [8, 7]}]'
        '''
        return self._compose_optic(optics.JsonIso())

    def Just(self):
        # type: () -> BaseUiLens[S, T, X, Y]
        '''A prism that focuses the value inside a `lenses.maybe.Just`
        object.

            >>> from lenses import lens
            >>> from lenses.maybe import Just, Nothing
            >>> lens.Just()
            UnboundLens(JustPrism())
            >>> lens.Just().collect()(Just(1))
            [1]
            >>> lens.Just().collect()(Nothing())
            []
            >>> lens.Just().set(2)(Just(1))
            Just(2)
            >>> lens.Just().set(2)(Nothing())
            Nothing()
        '''
        return self._compose_optic(optics.JustPrism())

    def Keys(self):
        # type: () -> BaseUiLens[S, T, X, Y]
        '''A traversal focusing the keys of a dictionary. Analogous to
        `dict.keys`.

            >>> from lenses import lens
            >>> from collections import OrderedDict
            >>> data = OrderedDict([(1, 10), (2, 20)])
            >>> lens.Keys()
            UnboundLens(ItemsTraversal() & GetitemLens(0))
            >>> lens.Keys().collect()(data)
            [1, 2]
            >>> lens.Keys().modify(lambda n: n + 1)(data)
            OrderedDict([(2, 10), (3, 20)])
        '''
        return self._compose_optic(
            optics.ItemsTraversal() & optics.GetitemLens(0)
        )

    def Norm(self, setter):
        # type: (Callable[[A], X]) -> BaseUiLens[S, T, X, Y]
        '''An isomorphism that applies a function as it sets a new
        focus without regard to the old state. It will get foci without
        transformation. This lens allows you to pre-process values before
        you set them, but still get values as they exist in the state.
        Useful for type conversions or normalising data.

        For best results, your normalisation function should be
        idempotent.  That is, applying the function twice should have
        no effect::

            setter(setter(value)) == setter(value)

        Equivalent to `Isomorphism((lambda s: s), setter)`.

            >>> from lenses import lens
            >>> def real_only(num):
            ...     return num.real
            ...
            >>> lens.Norm(real_only)
            UnboundLens(NormalisingIso(<function real_only at ...>))
            >>> lens[0].Norm(real_only).get()([1.0, 2.0, 3.0])
            1.0
            >>> lens[0].Norm(real_only).set(4+7j)([1.0, 2.0, 3.0])
            [4.0, 2.0, 3.0]

        Types with constructors that do conversion are often good targets
        for this lens:

            >>> lens[0].Norm(int).set(4.0)([1, 2, 3])
            [4, 2, 3]
            >>> lens[1].Norm(int).set('5')([1, 2, 3])
            [1, 5, 3]
        '''
        return self._compose_optic(optics.NormalisingIso(setter))

    def Prism(self, unpack, pack, ignore_none=False, ignore_errors=None):
        # type: (Callable[[A], mJust[X]], Callable[[Y], B], bool, Optional[tuple]) -> BaseUiLens[S, T, X, Y]
        '''A prism is an optic made from a pair of functions that pack and
        unpack a state where the unpacking process can potentially fail.

        `pack` is a function that takes a focus and returns that focus
        wrapped up in a new state. `unpack` is a function that takes
        a state and unpacks it to get a focus. The unpack function may
        choose to fail to unpack a focus, either by returning None or
        raising an exception (or both).

        All prisms are also traversals that have exactly zero or one foci.

        You must pass one or both of the ``ignore_none=True`` or
        ``ignore_errors=True`` keyword arguments. If you pass the former
        then the prism will fail to focus anything when your unpacking
        function returns ``None``. If you pass the latter then it will
        fail to focus when your unpacking function raises an error.

            >>> from lenses import lens
            >>> lens.Prism(int, str)
            Traceback (most recent call last):
              File "<stdin>", line 1 in ?
            ValueError: Must specify what to ignore
            >>> lens.Prism(int, str, ignore_errors=True)
            UnboundLens(Prism(..., ...))
            >>> lens.Prism(int, str, ignore_errors=True).collect()('42')
            [42]
            >>> lens.Prism(int, str, ignore_errors=True).collect()('fourty two')
            []

        If you set ``ignore_errors`` to ``True`` then it will catch any
        and all exceptions. A better alternative is to set it to a tuple of
        exception types to ignore (such as you would pass to ``isinstance``).

            >>> errors = (ValueError,)
            >>> lens.Prism(int, str, ignore_errors=errors)
            UnboundLens(Prism(..., ...))
            >>> lens.Prism(int, str, ignore_errors=errors).collect()('42')
            [42]
            >>> lens.Prism(int, str, ignore_errors=errors).collect()('fourty two')
            []
            >>> lens.Prism(int, str, ignore_errors=errors).collect()([1, 2, 3])
            Traceback (most recent call last):
              File "<stdin>", line 1 in ?
            TypeError: int() argument must be ...
        '''

        if not (ignore_none or ignore_errors):
            raise ValueError('Must specify what to ignore')

        if ignore_errors is True:
            ignore_errors = (Exception,)

        @functools.wraps(unpack)
        def new_unpack(state):
            try:
                result = unpack(state)
            except Exception as e:
                if ignore_errors and isinstance(e, ignore_errors):
                    return mNothing()
                else:
                    raise e
            if ignore_none:
                return mNothing() if result is None else mJust(result)
            return mJust(result)

        return self._compose_optic(optics.Prism(new_unpack, pack))

    def Recur(self, cls):
        '''A traversal that recurses through an object focusing everything it
        can find of a particular type. This traversal will probe arbitrarily
        deep into the contents of the state looking for sub-objects. It
        uses some naughty tricks to do this including looking at an object's
        `__dict__` attribute.

        It is somewhat analogous to haskell's uniplate optic.

            >>> from lenses import lens
            >>> lens.Recur(int)
            UnboundLens(RecurTraversal(<... 'int'>))
            >>> data = [[1, 2, 100.0], [3, 'hello', [{}, 4], 5]]
            >>> lens.Recur(int).collect()(data)
            [1, 2, 3, 4, 5]
            >>> (lens.Recur(int) + 1)(data)
            [[2, 3, 100.0], [4, 'hello', [{}, 5], 6]]

        It also works on custom classes:

            >>> class Container(object):
            ...     def __init__(self, contents):
            ...         self.contents = contents
            ...     def __repr__(self):
            ...         return 'Container({!r})'.format(self.contents)
            >>> data = [Container(1), 2, Container(Container(3)), [4, 5]]
            >>> (lens.Recur(int) + 1)(data)
            [Container(2), 3, Container(Container(4)), [5, 6]]
            >>> lens.Recur(Container).collect()(data)
            [Container(1), Container(Container(3))]

        Be careful with this; it can focus things you might not expect.
        '''
        return self._compose_optic(optics.RecurTraversal(cls))

    def Traversal(self, folder, builder):
        # type: (Callable[[A], Iterable[X]], Callable[[A, Iterable[Y]], B]) -> BaseUiLens[S, T, X, Y]
        '''An optic that wraps folder and builder functions.

        The folder function is a function that takes a single argument -
        the state - and returns an iterable containing all the foci that
        exist in that state. Generators are a good option for writing
        folder functions.

        A builder function takes the old state and an list of values
        and constructs a new state with the old state's values swapped
        out. The number of values passed to builder for any given state
        should always be the same as the number of values that the folder
        function would have returned for that same state.

            >>> from lenses import lens
            >>> def ends_folder(state):
            ...     'Yields the first and last elements of a list'
            ...     yield state[0]
            ...     yield state[-1]
            >>> def ends_builder(state, values):
            ...     'Sets the first and last elements of a list'
            ...     result = list(state)
            ...     result[0] = values[0]
            ...     result[-1] = values[1]
            ...     return result
            >>> both_ends = lens.Traversal(ends_folder, ends_builder)
            >>> both_ends
            UnboundLens(Traversal(...ends_folder..., ...ends_builder...))
            >>> both_ends.collect()([1, 2, 3, 4])
            [1, 4]
            >>> both_ends.set(5)([1, 2, 3, 4])
            [5, 2, 3, 5]
        '''

        return self._compose_optic(optics.Traversal(folder, builder))

    def Tuple(self, *lenses):
        # type: (*BaseUiLens[A, B, X, Y]) -> BaseUiLens[S, T, X, Y]
        '''A lens that combines the focuses of other lenses into a
        single tuple. The sublenses must be optics of kind Lens; this
        means no Traversals.

            >>> from lenses import lens
            >>> lens.Tuple()
            UnboundLens(TupleLens())
            >>> tl = lens.Tuple(lens[0], lens[2])
            >>> tl
            UnboundLens(TupleLens(GetitemLens(0), GetitemLens(2)))
            >>> tl.get()([1, 2, 3, 4])
            (1, 3)
            >>> tl.set((5, 6))([1, 2, 3, 4])
            [5, 2, 6, 4]

        This lens is particularly useful when immediately followed by
        an EachLens, allowing you to traverse data even when it comes
        from disparate locations within the state.

            >>> state = ([1, 2, 3], 4, [5, 6])
            >>> tl.Each().Each().collect()(state)
            [1, 2, 3, 5, 6]
            >>> (tl.Each().Each() + 10)(state)
            ([11, 12, 13], 4, [15, 16])
        '''
        true_lenses = [l._optic for l in lenses]
        return self._compose_optic(optics.TupleLens(*true_lenses))

    def Values(self):
        # type: () -> BaseUiLens[S, T, X, Y]
        '''A traversal focusing the values of a dictionary. Analogous to
        `dict.values`.

            >>> from lenses import lens
            >>> from collections import OrderedDict
            >>> data = OrderedDict([(1, 10), (2, 20)])
            >>> lens.Values()
            UnboundLens(ItemsTraversal() & GetitemLens(1))
            >>> lens.Values().collect()(data)
            [10, 20]
            >>> lens.Values().modify(lambda n: n + 1)(data)
            OrderedDict([(1, 11), (2, 21)])
        '''
        return self._compose_optic(
            optics.ItemsTraversal() & optics.GetitemLens(1)
        )

    def Zoom(self):
        # type: () -> BaseUiLens[S, T, X, Y]
        '''Follows its state as if it were a `BoundLens` object.

            >>> from lenses import lens, bind
            >>> data = [bind([1, 2])[1], 4]
            >>> lens.Zoom()
            UnboundLens(ZoomTraversal())
            >>> lens[0].Zoom().get()(data)
            2
            >>> lens[0].Zoom().set(3)(data)
            [[1, 3], 4]
        '''
        return self._compose_optic(optics.ZoomTraversal())

    def ZoomAttr(self, name):
        # type: (str) -> BaseUiLens[S, T, X, Y]
        '''A lens that looks up an attribute on its target and follows
        it as if were a `BoundLens` object. Ignores the state, if any,
        of the lens that is being looked up.

            >>> from lenses import lens
            >>> class ClassWithLens(object):
            ...     def __init__(self, items):
            ...         self._private_items = items
            ...     def __repr__(self):
            ...         return 'ClassWithLens({!r})'.format(self._private_items)
            ...     first = lens._private_items[0]
            ...
            >>> data = (ClassWithLens([1, 2, 3]), 4)
            >>> lens.ZoomAttr('first')
            UnboundLens(ZoomAttrTraversal('first'))
            >>> lens[0].ZoomAttr('first').get()(data)
            1
            >>> lens[0].ZoomAttr('first').set(5)(data)
            (ClassWithLens([5, 2, 3]), 4)
        '''
        return self._compose_optic(optics.ZoomAttrTraversal(name))

    def __getattr__(self, name):
        # type: (str) -> Any
        if name.startswith('__') and name.endswith('__'):
            raise AttributeError('no attribute {}'.format(name))

        if name.startswith('call_mut_'):

            def caller(*args, **kwargs):
                # type: (*Any, **Any) -> T
                return self.call_mut(name[9:], *args, **kwargs)

            return caller

        if name.startswith('call_'):

            def caller(*args, **kwargs):
                # type: (*Any, **Any) -> T
                return self.call(name[5:], *args, **kwargs)

            return caller

        return self.GetZoomAttr(name)

    def __getitem__(self, name):
        # type: (Any) -> BaseUiLens[S, T, X, Y]
        return self.GetItem(name)
