#
# ddSMT: A delta debugger for SMT benchmarks in SMT-Lib v2 format.
#
# This file is part of ddSMT.
#
# Copyright (C) 2013-2021 by the authors listed in AUTHORS file.
#
# ddSMT is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ddSMT is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ddSMT.  If not, see <https://www.gnu.org/licenses/>.

from .nodes import Node, count_nodes
from .smtlib import *
from . import options
from .mutator_utils import Simplification


class BinaryReduction:
    """Binary reduction on the top level node.

    This mutator mimics what strategy ``ddmin`` achieves when applying
    mutator :class:`ddsmt.mutators_core.EraseNode`.

    If ``self.ident`` is set, only nodes with the identifier specified as
    ``self.ident`` are considered.

    .. note::
      This mutator is always disabled for strategy ``ddmin``.
    """
    def mutations(self, node):
        if node.is_leaf() or len(node) < 8:
            return
        for sec in nodes.binary_search(len(node)):
            yield Simplification({node.id: node[:sec[0]] + node[sec[1]:]}, [])

    def global_mutations(self, node, input_):
        if node != input_[0]:
            return []
        # generate all sublists as generated by binary-search in bfs order
        # let den be the denominator of the list length (the tree level)
        # let num be the numerator within the current tree level
        if hasattr(self, 'ident'):
            ids = [
                node.id for node in input_
                if is_operator_app(node, self.ident)
            ]
        else:
            ids = [node.id for node in input_]
        for sec in nodes.binary_search(len(ids)):
            yield Simplification(
                {node_id: None
                 for node_id in ids[sec[0]:sec[1]]}, [])

    def __str__(self):
        if hasattr(self, 'ident'):
            return f'binary reduction ({self.ident})'
        return 'binary reduction'


class Constants:
    """Replace given node with a default value of the same sort.

    Default values are defined in :func:`ddsmt.smtlib.get_default_constants`.
    """
    # Requires that global information has been populated via
    # ``collect_information``.
    def filter(self, node):
        return not is_definition_node(node) and get_sort(node) is not None

    def mutations(self, node):
        t = get_sort(node)
        if t is None:
            return []
        res = get_default_constants(t)
        if node in res:
            return []
        for c in res:
            yield Simplification({node.id: c}, [])

    def __str__(self):
        return 'substitute by a constant'


class EraseNode:
    """Erase given given node.

    If ``self.ident`` is set, only nodes with the identifier specified as
    ``self.ident`` are considered.
    """
    def filter(self, node):
        if not hasattr(self, 'ident'):
            return True
        return node.has_ident() and node.get_ident() == self.ident

    def mutations(self, node):
        return [Simplification({node.id: None}, [])]

    def __str__(self):
        if hasattr(self, 'ident'):
            return f'erase node ({self.ident})'
        return 'erase node'


class MergeWithChildren:
    """Merge given node with one of its children.

    This mutator can only be applied to n-ary operations like ``and`` or ``+``.
    """
    def filter(self, node):
        return has_nary_operator(node)

    def mutations(self, node):
        for cid, child in enumerate(node):
            if child.has_ident() and node.get_ident() == child.get_ident():
                simp = Node(*node[:cid], *node[cid][1:], *node[cid + 1:])
                yield Simplification({node.id: simp}, [])

    def __str__(self):
        return 'merge with child'


class ReplaceByChild:
    """Replace given node with one of its children."""
    def filter(self, node):
        return not is_leaf(node) and not is_operator_app(node, 'let')

    def mutations(self, node):
        sort = get_sort(node)
        for n in node[1:]:
            if get_sort(n) == sort:
                yield Simplification({node.id: n}, [])

    def __str__(self):
        return 'replace by a child'


class ReplaceByVariable:
    """Replace given node with an existing variable of the same type.

    .. note::
      Replacing variables with other variables **potentially introduces
      cycles**.
      To avoid this, for leaf nodes we only allow substitutions with
      variables that are lexicographically **smaller** (default)
      or **larger** than the leaf mode.
      Configure with option ``--replace-by-variable-mode`` (use ``inc`` for
      substitution with smaller, and ``dec`` for substition with larger
      variables).
    """
    # Requires that global information has been populated via
    # ``collect_information``.
    def filter(self, node):
        return not is_const(node) and not is_definition_node(
            node) and get_sort(node) is not None

    def mutations(self, node):
        if not hasattr(self, 'repl_mode'):
            self.repl_mode = options.args().replace_by_variable_mode

        ret_sort = get_sort(node)
        if ret_sort is None:
            return []
        variables = get_variables_with_sort(ret_sort)
        # Replacing by a defined variable may loop with inlining
        variables = filter(lambda v: not is_defined_fun(v), variables)
        if is_leaf(node):
            if self.repl_mode == 'inc':
                variables = [v for v in variables if v > node.data]
            else:
                variables = [v for v in variables if v < node.data]
        yield from [Simplification({node.id: Node(v)}, []) for v in variables]

    def __str__(self):
        if hasattr(self, 'repl_mode'):
            return f'substitute by existing variable ({self.repl_mode})'
        return 'substitute by existing variable'


class SortChildren:
    """Sort the children of a given node by size (count of sub-nodes)."""
    def filter(self, node):
        return not is_leaf(node)

    def mutations(self, node):
        s = nodes.Node(*sorted(node, key=count_nodes))
        if s != node:
            return [Simplification({node.id: s}, [])]
        return []

    def __str__(self):
        return 'sort children'


def get_mutator_options(argparser):
    """Add additional options to the argument parser."""
    argparser.add_argument(
        '--replace-by-variable-mode',
        choices=['inc', 'dec'],
        default='inc',
        help='replace with existing variables that are larger or smaller')


def get_mutators():
    """Return a mapping from mutator class names to the name of their config
    options."""
    return {
        'BinaryReduction': 'binary-reduction',
        'Constants': 'constants',
        'EraseNode': 'erase-node',
        'MergeWithChildren': 'merge-children',
        'ReplaceByChild': 'substitute-children',
        'ReplaceByVariable': 'replace-by-variable',
        'SortChildren': 'sort-children',
    }
