"""
Module detecting uninitialized local variables

Recursively explore the CFG to only report uninitialized local variables that are
read before being written
"""

from slither.core.cfg.node import Node, NodeType
from slither.core.declarations.function_contract import FunctionContract
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.utils.output import Output


class UninitializedLocalVars(AbstractDetector):
    ARGUMENT = "uninitialized-local"
    HELP = "Uninitialized local variables"
    IMPACT = DetectorClassification.MEDIUM
    CONFIDENCE = DetectorClassification.MEDIUM

    WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#uninitialized-local-variables"

    WIKI_TITLE = "Uninitialized local variables"
    WIKI_DESCRIPTION = "Uninitialized local variables."

    # region wiki_exploit_scenario
    WIKI_EXPLOIT_SCENARIO = """
```solidity
contract Uninitialized is Owner{
    function withdraw() payable public onlyOwner{
        address to;
        to.transfer(this.balance)
    }
}
```
Bob calls `transfer`. As a result, all Ether is sent to the address `0x0` and is lost."""
    # endregion wiki_exploit_scenario

    WIKI_RECOMMENDATION = "Initialize all the variables. If a variable is meant to be initialized to zero, explicitly set it to zero to improve code readability."

    key = "UNINITIALIZEDLOCAL"

    def _detect_uninitialized(
        self, function: FunctionContract, node: Node, visited: list[Node]
    ) -> None:
        if node in visited:
            return

        visited = visited + [node]

        fathers_context = []

        for father in node.fathers:
            if self.key in father.context:
                fathers_context += father.context[self.key]

        # Exclude path that dont bring further information
        if node in self.visited_all_paths:
            if all(f_c in self.visited_all_paths[node] for f_c in fathers_context):
                return
        else:
            self.visited_all_paths[node] = []

        self.visited_all_paths[node] = list(set(self.visited_all_paths[node] + fathers_context))

        # Remove a local variable declared in a for loop header
        if (
            node.type == NodeType.VARIABLE
            and len(node.sons) == 1  # Should always be true for a node that has a STARTLOOP son
            and node.sons[0].type == NodeType.STARTLOOP
        ):
            if node.variable_declaration in fathers_context:
                fathers_context.remove(node.variable_declaration)

        if self.key in node.context:
            fathers_context += node.context[self.key]

        variables_read = node.variables_read
        for uninitialized_local_variable in fathers_context:
            if uninitialized_local_variable in variables_read:
                self.results.append((function, uninitialized_local_variable))

        # Only save the local variables that are not yet written
        uninitialized_local_variables = list(set(fathers_context) - set(node.variables_written))
        node.context[self.key] = uninitialized_local_variables

        for son in node.sons:
            self._detect_uninitialized(function, son, visited)

    def _detect(self) -> list[Output]:
        """Detect uninitialized local variables

        Recursively visit the calls
        Returns:
            dict: [contract name] = set(local variable uninitialized)
        """
        results = []

        self.results = []
        self.visited_all_paths = {}

        for contract in self.compilation_unit.contracts:
            for function in contract.functions:
                if (
                    function.is_implemented
                    and function.contract_declarer == contract
                    and function.entry_point
                ):
                    if function.contains_assembly:
                        continue
                    # dont consider storage variable, as they are detected by another detector
                    uninitialized_local_variables = [
                        v for v in function.local_variables if not v.is_storage and v.uninitialized
                    ]
                    function.entry_point.context[self.key] = uninitialized_local_variables
                    self._detect_uninitialized(function, function.entry_point, [])
        all_results = list(set(self.results))
        for function, uninitialized_local_variable in all_results:
            info = [
                uninitialized_local_variable,
                " is a local variable never initialized\n",
            ]
            json = self.generate_result(info)
            results.append(json)

        return results
