import abc
import re

from slither.utils.colors import green, yellow, red

from collections import OrderedDict

class IncorrectDetectorInitialization(Exception):
    pass


class DetectorClassification:
    HIGH = 0
    MEDIUM = 1
    LOW = 2
    INFORMATIONAL = 3


classification_colors = {
    DetectorClassification.INFORMATIONAL: green,
    DetectorClassification.LOW: green,
    DetectorClassification.MEDIUM: yellow,
    DetectorClassification.HIGH: red,
}

classification_txt = {
    DetectorClassification.INFORMATIONAL: 'Informational',
    DetectorClassification.LOW: 'Low',
    DetectorClassification.MEDIUM: 'Medium',
    DetectorClassification.HIGH: 'High',
}

class AbstractDetector(metaclass=abc.ABCMeta):
    ARGUMENT = ''  # run the detector with slither.py --ARGUMENT
    HELP = ''  # help information
    IMPACT = None
    CONFIDENCE = None

    WIKI = ''

    def __init__(self, slither, logger):
        self.slither = slither
        self.contracts = slither.contracts
        self.filename = slither.filename
        self.logger = logger

        if not self.HELP:
            raise IncorrectDetectorInitialization('HELP is not initialized {}'.format(self.__class__.__name__))

        if not self.ARGUMENT:
            raise IncorrectDetectorInitialization('ARGUMENT is not initialized {}'.format(self.__class__.__name__))

        if re.match('^[a-zA-Z0-9_-]*$', self.ARGUMENT) is None:
            raise IncorrectDetectorInitialization('ARGUMENT has illegal character {}'.format(self.__class__.__name__))

        if self.IMPACT not in [DetectorClassification.LOW,
                                       DetectorClassification.MEDIUM,
                                       DetectorClassification.HIGH,
                                       DetectorClassification.INFORMATIONAL]:
            raise IncorrectDetectorInitialization('IMPACT is not initialized {}'.format(self.__class__.__name__))

        if self.CONFIDENCE not in [DetectorClassification.LOW,
                                       DetectorClassification.MEDIUM,
                                       DetectorClassification.HIGH,
                                       DetectorClassification.INFORMATIONAL]:
            raise IncorrectDetectorInitialization('CONFIDENCE is not initialized {}'.format(self.__class__.__name__))

    def log(self, info):
        if self.logger:
            info = "\n"+info
            if self.WIKI != '':
                info += 'Reference: {}'.format(self.WIKI)
            self.logger.info(self.color(info))

    @abc.abstractmethod
    def detect(self):
        """TODO Documentation"""
        return

    @property
    def color(self):
        return classification_colors[self.IMPACT]

    def generate_json_result(self, info):
        d = OrderedDict()
        d['check'] = self.ARGUMENT
        d['impact'] = classification_txt[self.IMPACT]
        d['confidence'] = classification_txt[self.CONFIDENCE]
        d['description'] = info
        d['elements'] = []
        return d

    @staticmethod
    def add_variable_to_json(variable, d):
        d['elements'].append({'type': 'variable',
                              'name': variable.name,
                              'source_mapping': variable.source_mapping})

    @staticmethod
    def add_variables_to_json(variables, d):
        for variable in sorted(variables, key=lambda x:x.name):
            AbstractDetector.add_variable_to_json(variable, d)

    @staticmethod
    def add_contract_to_json(contract, d):
        d['elements'].append({'type': 'contract',
                              'name': contract.name,
                              'source_mapping': contract.source_mapping})

    @staticmethod
    def add_function_to_json(function, d):
        contract = {'elements':[]}
        AbstractDetector.add_contract_to_json(function.contract, contract)
        d['elements'].append({'type': 'function',
                              'name': function.name,
                              'source_mapping': function.source_mapping,
                              'contract': contract['elements'][0]})

    @staticmethod
    def add_functions_to_json(functions, d):
        for function in sorted(functions, key=lambda x: x.name):
            AbstractDetector.add_function_to_json(function, d)

    @staticmethod
    def add_nodes_to_json(nodes, d):
        for node in sorted(nodes, key=lambda x: x.node_id):
            d['elements'].append({'type': 'expression',
                                  'expression': str(node.expression),
                                  'source_mapping': node.source_mapping})

