"""
Module detecting tautologies and contradictions based on types in comparison operations over integers
"""
from typing import List, Set, Tuple

from slither.core.cfg.node import Node
from slither.core.declarations import Function
from slither.core.declarations.contract import Contract
from slither.core.solidity_types.elementary_type import Int, Uint
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.slithir.operations import Binary, BinaryType
from slither.slithir.variables import Constant
from slither.utils.output import Output


def typeRange(t: str) -> Tuple[int, int]:
    bits = int(t.split("int")[1])
    if t in Uint:
        return 0, (2**bits) - 1
    assert t in Int
    v = (2 ** (bits - 1)) - 1
    return -v, v


def _detect_tautology_or_contradiction(low: int, high: int, cval: int, op: BinaryType) -> bool:
    """
    Return true if "[low high] op cval " is always true or always false
    :param low:
    :param high:
    :param cval:
    :param op:
    :return:
    """
    if op == BinaryType.LESS:
        # a < cval
        # its a tautology if
        # high(a) < cval
        # its a contradiction if
        # low(a) >= cval
        return high < cval or low >= cval
    if op == BinaryType.GREATER:
        # a > cval
        # its a tautology if
        # low(a) > cval
        # its a contradiction if
        # high(a) <= cval
        return low > cval or high <= cval
    if op == BinaryType.LESS_EQUAL:
        # a <= cval
        # its a tautology if
        # high(a) <= cval
        # its a contradiction if
        # low(a) > cval
        return (high <= cval) or (low > cval)
    if op == BinaryType.GREATER_EQUAL:
        # a >= cval
        # its a tautology if
        # low(a) >= cval
        # its a contradiction if
        # high(a) < cval
        return (low >= cval) or (high < cval)
    return False


class TypeBasedTautology(AbstractDetector):
    """
    Type-based tautology or contradiction
    """

    ARGUMENT = "tautology"
    HELP = "Tautology or contradiction"
    IMPACT = DetectorClassification.MEDIUM
    CONFIDENCE = DetectorClassification.HIGH

    WIKI = (
        "https://github.com/crytic/slither/wiki/Detector-Documentation#tautology-or-contradiction"
    )

    WIKI_TITLE = "Tautology or contradiction"
    WIKI_DESCRIPTION = """Detects expressions that are tautologies or contradictions."""

    # region wiki_exploit_scenario
    WIKI_EXPLOIT_SCENARIO = """
```solidity
contract A {
	function f(uint x) public {
		// ...
        if (x >= 0) { // bad -- always true
           // ...
        }
		// ...
	}

	function g(uint8 y) public returns (bool) {
		// ...
        return (y < 512); // bad!
		// ...
	}
}
```
`x` is a `uint256`, so `x >= 0` will be always true.
`y` is a `uint8`, so `y <512` will be always true.  
"""
    # endregion wiki_exploit_scenario

    WIKI_RECOMMENDATION = (
        """Fix the incorrect comparison by changing the value type or the comparison."""
    )

    flip_table = {
        BinaryType.GREATER: BinaryType.LESS,
        BinaryType.GREATER_EQUAL: BinaryType.LESS_EQUAL,
        BinaryType.LESS: BinaryType.GREATER,
        BinaryType.LESS_EQUAL: BinaryType.GREATER_EQUAL,
    }

    def detect_type_based_tautologies(self, contract: Contract) -> List[Tuple[Function, Set[Node]]]:
        """
        Detects and returns all nodes with tautology/contradiction comparisons (based on type alone).
        :param contract: Contract to detect assignment within.
        :return: A list of nodes with tautolgies/contradictions.
        """

        # Create our result set.
        results: List[Tuple[Function, Set[Node]]] = []
        allInts = Int + Uint

        # Loop for each function and modifier.
        for function in contract.functions_declared:  # pylint: disable=too-many-nested-blocks
            f_results = set()

            for node in function.nodes:
                for ir in node.irs:
                    if isinstance(ir, Binary) and ir.type in self.flip_table:
                        # If neither side is a constant, we can't do much
                        if isinstance(ir.variable_left, Constant):
                            cval = ir.variable_left.value
                            rtype = str(ir.variable_right.type)
                            if rtype in allInts:
                                (low, high) = typeRange(rtype)
                                if _detect_tautology_or_contradiction(
                                    low, high, cval, self.flip_table[ir.type]
                                ):
                                    f_results.add(node)

                        if isinstance(ir.variable_right, Constant):
                            cval = ir.variable_right.value
                            ltype = str(ir.variable_left.type)
                            if ltype in allInts:
                                (low, high) = typeRange(ltype)
                                if _detect_tautology_or_contradiction(low, high, cval, ir.type):
                                    f_results.add(node)
            results.append((function, f_results))

        # Return the resulting set of nodes with tautologies and contradictions
        return results

    def _detect(self) -> List[Output]:
        """
        Detect tautological (or contradictory) comparisons
        """
        results = []
        for contract in self.contracts:
            tautologies = self.detect_type_based_tautologies(contract)
            if tautologies:
                for (func, nodes) in tautologies:
                    for node in nodes:
                        info = [func, " contains a tautology or contradiction:\n"]
                        info += ["\t- ", node, "\n"]

                        res = self.generate_result(info)
                        results.append(res)

        return results
