import abc
import re

from slither.utils.colors import green, yellow, red
from slither.formatters.exceptions import FormatImpossible
from slither.formatters.utils.patches import apply_patch, create_diff
from slither.utils.output import Output


class IncorrectDetectorInitialization(Exception):
    pass


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


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

classification_txt = {
    DetectorClassification.INFORMATIONAL: 'Informational',
    DetectorClassification.OPTIMIZATION: 'Optimization',
    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 = ''

    WIKI_TITLE = ''
    WIKI_DESCRIPTION = ''
    WIKI_EXPLOIT_SCENARIO = ''
    WIKI_RECOMMENDATION = ''

    STANDARD_JSON = True

    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 not self.WIKI:
            raise IncorrectDetectorInitialization('WIKI is not initialized {}'.format(self.__class__.__name__))

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

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

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

        if not self.WIKI_RECOMMENDATION:
            raise IncorrectDetectorInitialization('WIKI_RECOMMENDATION 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,
                                       DetectorClassification.OPTIMIZATION]:
            raise IncorrectDetectorInitialization('IMPACT is not initialized {}'.format(self.__class__.__name__))

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

    def _log(self, info):
        if self.logger:
            self.logger.info(self.color(info))

    @abc.abstractmethod
    def _detect(self):
        """TODO Documentation"""
        return []

    def detect(self):
        all_results = self._detect()
        # Keep only dictionaries
        all_results = [r.data for r in all_results]
        results = []
        # only keep valid result, and remove dupplicate
        [results.append(r) for r in all_results if self.slither.valid_result(r) and r not in results]
        if results:
            if self.logger:
                info = '\n'
                for idx, result in enumerate(results):
                    if self.slither.triage_mode:
                        info += '{}: '.format(idx)
                    info += result['description']
                info += 'Reference: {}'.format(self.WIKI)
                self._log(info)
        if self.slither.generate_patches:
            for result in results:
                try:
                    self._format(self.slither, result)
                    if not 'patches' in result:
                        continue
                    result['patches_diff'] = dict()
                    for file in result['patches']:
                        original_txt = self.slither.source_code[file].encode('utf8')
                        patched_txt = original_txt
                        offset = 0
                        patches = result['patches'][file]
                        patches.sort(key=lambda x: x['start'])
                        if not all(patches[i]['end'] <= patches[i + 1]['end'] for i in range(len(patches) - 1)):
                            self._log(f'Impossible to generate patch; patches collisions: {patches}')
                            continue
                        for patch in patches:
                            patched_txt, offset = apply_patch(patched_txt, patch, offset)
                        diff = create_diff(self.slither, original_txt, patched_txt, file)
                        if not diff:
                            self._log(f'Impossible to generate patch; empty {result}')
                        else:
                            result['patches_diff'][file] = diff

                except FormatImpossible as e:
                        self._log(f'\nImpossible to patch:\n\t{result["description"]}\t{e}')

        if results and self.slither.triage_mode:
            while True:
                indexes = input('Results to hide during next runs: "0,1,..." or "All" (enter to not hide results): '.format(len(results)))
                if indexes == 'All':
                    self.slither.save_results_to_hide(results)
                    return []
                if indexes == '':
                    return results
                if indexes.startswith('['):
                    indexes = indexes[1:]
                if indexes.endswith(']'):
                    indexes = indexes[:-1]
                try:
                    indexes = [int(i) for i in indexes.split(',')]
                    self.slither.save_results_to_hide([r for (idx, r) in enumerate(results) if idx in indexes])
                    return [r for (idx, r) in enumerate(results) if idx not in indexes]
                except ValueError:
                    self.logger.error(yellow('Malformed input. Example of valid input: 0,1,2,3'))
        return results

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

    def generate_result(self, info, additional_fields=None):
        output = Output(info,
                        additional_fields,
                        standard_format=self.STANDARD_JSON,
                        markdown_root=self.slither.markdown_root)

        output.data['check'] = self.ARGUMENT
        output.data['impact'] = classification_txt[self.IMPACT]
        output.data['confidence'] = classification_txt[self.CONFIDENCE]

        return output

    @staticmethod
    def _format(slither, result):
        """Implement format"""
        return