import shellphish_afl
import subprocess
import distutils.spawn #pylint:disable=no-name-in-module,import-error
import logging
import signal
import shutil
import time
import angr
import sys
import os

l = logging.getLogger("phuzzer.phuzzers")

class Phuzzer:
    """ Phuzzer object, spins up a fuzzing job on a binary """

    def __init__(self, target, seeds=None, dictionary=None, create_dictionary=False, timeout=None):
        """
        :param target: the target (i.e., path to the binary to fuzz, or a docker target)
        :param seeds: list of inputs to seed fuzzing with
        :param dictionary: a list of bytes objects to seed the dictionary with
        :param create_dictionary: create a dictionary from the string references in the binary
        """

        self.target = target
        self.seeds = seeds or [ ]

        # processes spun up
        self.processes            = [ ]

        self.start_time = None
        self.end_time = None
        self.timeout = timeout

        self.check_environment()

        # token dictionary
        self.dictionary = dictionary or (self.create_dictionary() if create_dictionary else [])

    #
    # Some convenience functionality.
    #

    def found_crash(self):
        return len(self.crashes()) > 0

    def add_cores(self, n):
        for _ in range(n):
            self.add_core()

    def remove_cores(self, n):
        """
        remove multiple fuzzers
        """
        for _ in range(n):
            self.remove_core()

    def timed_out(self):
        if self.timeout is None:
            return False
        return time.time() - self.start_time > self.timeout

    def start(self):
        self.start_time = int(time.time())
        return self
    __enter__ = start

    def stop(self):
        self.end_time = int(time.time())
        if self.start_time is not None:
            l.info("Phuzzer %s shut down after %d seconds.", self, self.end_time - self.start_time)
        for p in self.processes:
            p.terminate()
            p.wait()
    __exit__ = stop

    @classmethod
    def check_environment(cls):
        try:
            cls._check_environment()
        except InstallError as e:
            tmp = ""
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += "############# ATTENTION: YOUR SYSTEM IS MISCONFIGURED FOR FUZZING #############\n"
            tmp += "############# ATTENTION: YOUR SYSTEM IS MISCONFIGURED FOR FUZZING #############\n"
            tmp += "############# ATTENTION: YOUR SYSTEM IS MISCONFIGURED FOR FUZZING #############\n"
            tmp += "############# ATTENTION: YOUR SYSTEM IS MISCONFIGURED FOR FUZZING #############\n"
            tmp += "############# ATTENTION: YOUR SYSTEM IS MISCONFIGURED FOR FUZZING #############\n"
            tmp += "############# ATTENTION: YOUR SYSTEM IS MISCONFIGURED FOR FUZZING #############\n"
            tmp += "############# ATTENTION: YOUR SYSTEM IS MISCONFIGURED FOR FUZZING #############\n"
            tmp += "############# ATTENTION: YOUR SYSTEM IS MISCONFIGURED FOR FUZZING #############\n"
            tmp += "############# ATTENTION: YOUR SYSTEM IS MISCONFIGURED FOR FUZZING #############\n"
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += "####### THE FUZZER WILL NOT RUN. AND IT IS ***YOUR FAULT***!!!!!!!!!!!!  ######\n"
            tmp += "####### DIRECTLY BELOW THIS, THERE ARE CONCRETE REASONS FOR WHY THIS IS  ######\n"
            tmp += "####### IF YOU COMPLAIN TO US ON GITHUB ABOUT THIS NOT WORKING, AND YOU  ######\n"
            tmp += "####### DON'T RESOLVE THESE ISSUES FIRST, WE WILL NOT HELP YOU!!!!!!!!!  ######\n"
            tmp += "####### PLEASE RESOLVE THE ISSUES BELOW.    THEY LITERALLY TELL YOU WHAT ######\n"
            tmp += "####### YOU HAVE TO EXECUTE. DO NOT ASK FOR HELP IF YOU ARE SEEING THIS  ######\n"
            tmp += "####### MESSAGE; JUST FIX THE PROBLEM WITH YOUR SYSTEM!!!!!!!!!!!!!!!!!  ######\n"
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += e.args[0]
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += "####### FIX THE ABOVE ISSUES BEFORE ASKING FOR HELP. THE TEXT LITERALLY  ######\n"
            tmp += "####### TELLS YOU HOW TO DO IT. DO NOT ASK FOR HELP ABOUT THIS BEFORE    ######\n"
            tmp += "####### FIXING THE ABOVE ISSUES. IF YOU ARE SEEING THIS MESSAGE, YOUR    ######\n"
            tmp += "####### SYSTEM MISCONFIGURATION IS *******YOUR FAULT*********!!!!!!!!!!! ######\n"
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            tmp += "#######                                                                  ######\n"
            tmp += "#######                                                                  ######\n"
            tmp += "#######                GET YOUR SYSTEM SETUP FIXED!!!!!!!!!!             ######\n"
            tmp += "#######                                                                  ######\n"
            tmp += "#######                                                                  ######\n"
            tmp += "###############################################################################\n"
            tmp += "###############################################################################\n"
            e.args = (tmp,)
            xmsg = distutils.spawn.find_executable("xmessage") #pylint:disable=no-member
            if xmsg:
                subprocess.Popen([xmsg, tmp]).wait()
            l.critical(tmp)
            print(tmp)
            sys.stderr.write(tmp)
            sys.stdout.write(tmp)
            raise


    #
    # Dictionary creation
    #

    def create_dictionary(self):
        l.warning("creating a dictionary of string references within target \"%s\"", self.target)

        b = angr.Project(self.target, load_options={'auto_load_libs': False})
        cfg = b.analyses.CFG(resolve_indirect_jumps=True, collect_data_references=True)
        state = b.factory.blank_state()

        string_references = []
        for v in cfg._memory_data.values():
            if v.sort == "string" and v.size > 1:
                st = state.solver.eval(state.memory.load(v.address, v.size), cast_to=bytes)
                string_references.append((v.address, st))

        strings = [] if len(string_references) == 0 else list(list(zip(*string_references))[1])
        return strings


    #
    # Subclasses should override this.
    #

    @staticmethod
    def _check_environment():
        raise NotImplementedError()

    def crashes(self, signals=(signal.SIGSEGV, signal.SIGILL)):
        """
        Retrieve the crashes discovered by AFL. Since we are now detecting flag
        page leaks (via SIGUSR1) we will not return these leaks as crashes.
        Instead, these 'crashes' can be found with the leaks function.

        :param signals: list of valid kill signal numbers to override the default (SIGSEGV and SIGILL)
        :return: a list of strings which are crashing inputs
        """
        raise NotImplementedError()

    def queue(self, fuzzer='fuzzer-master'):
        """
        retrieve the current queue of inputs from a fuzzer
        :return: a list of strings which represent a fuzzer's queue
        """
        raise NotImplementedError()

    def pollenate(self, *testcases):
        """
        pollenate a fuzzing job with new testcases

        :param testcases: list of bytes objects representing new inputs to introduce
        """
        raise NotImplementedError()

    def add_core(self):
        raise NotImplementedError()

    def remove_core(self):
        raise NotImplementedError()

    def __del__(self):
        self.stop()

from ..errors import InstallError
from .afl import AFL
from .afl_multicb import AFLMultiCB