from logging import error
from ptrace.disasm import HAS_DISASSEMBLER
from signal import SIGFPE, SIGSEGV, SIGABRT
try:
    from signal import SIGCHLD
except ImportError:
    SIGCHLD = None
try:
    from signal import SIGBUS
except ImportError:
    SIGBUS = None
from ptrace.os_tools import RUNNING_LINUX
from ptrace.cpu_info import CPU_64BITS
from ptrace.debugger import ProcessEvent
from ptrace.error import PtraceError
from ptrace import signalName
from ptrace.debugger.signal_reason import (
    DivisionByZero, Abort, StackOverflow,
    InvalidMemoryAccess, InvalidRead, InvalidWrite,
    InstructionError, ChildExit)
from ptrace.debugger.parse_expr import parseExpression
import re

# Match a pointer dereference (e.g. "DWORD [EDX+0x8]")
DEREF_REGEX = r'(?P<deref_size>(BYTE|WORD|DWORD|DQWORD) )?\[(?P<deref>[^]]+)\]'

NAMED_WORD_SIZE = {
    'BYTE': 1,
    'WORD': 2,
    'DWORD': 4,
    'DQWORD': 8,
}

# Match any Intel instruction (e.g. "ADD")
INSTR_REGEX = '(?:[A-Z]{3,10})'


def findDerefSize(match):
    name = match.group("deref_size")
    try:
        return NAMED_WORD_SIZE[name]
    except KeyError:
        return None


def evalFaultAddress(process, match):
    expr = match.group('deref')
    if not expr:
        return None
    try:
        return parseExpression(process, expr)
    except ValueError as err:
        error("err: %s" % err)
        return None


class ProcessSignal(ProcessEvent):

    def __init__(self, signum, process):
        # Initialize attributes
        self.name = signalName(signum)
        ProcessEvent.__init__(self, process, "Signal %s" % self.name)
        self.signum = signum
        self.reason = None

    def _analyze(self):
        if self.signum in (SIGSEGV, SIGBUS):
            self.memoryFault()
        elif self.signum == SIGFPE:
            self.mathError()
        elif self.signum == SIGCHLD:
            self.childExit()
        elif self.signum == SIGABRT:
            self.reason = Abort()
        return self.reason

    def getInstruction(self):
        if not HAS_DISASSEMBLER:
            return None
        try:
            return self.process.disassembleOne()
        except PtraceError:
            return None

    def memoryFaultInstr(self, instr, fault_address):
        asm = instr.text

        # Invalid write (e.g. "MOV [...], value")
        match = re.search(r"^(?:MOV|TEST)[A-Z]* %s," % DEREF_REGEX, asm)
        if match:
            if fault_address is None:
                fault_address = evalFaultAddress(self.process, match)
            self.reason = InvalidWrite(fault_address, size=findDerefSize(match),
                                       instr=instr, process=self.process)
            return

        # Invalid read (e.g. "CMP BYTE [EAX+EDX-0x1], 0x0")
        match = re.search(r"^%s %s," % (INSTR_REGEX, DEREF_REGEX), asm)
        if match:
            if fault_address is None:
                fault_address = evalFaultAddress(self.process, match)
            self.reason = InvalidRead(fault_address, size=findDerefSize(match),
                                      instr=instr, process=self.process)
            return

        # Invalid read (e.g. "MOV reg, [...]")
        match = re.match(r"%s [^,]+, %s" % (INSTR_REGEX, DEREF_REGEX), asm)
        if match:
            if fault_address is None:
                fault_address = evalFaultAddress(self.process, match)
            self.reason = InvalidRead(fault_address, size=findDerefSize(match),
                                      instr=instr, process=self.process)
            return

        # MOVS* and SCAS* instructions (e.g. "MOVSB" or "REP SCASD")
        match = re.search(
            r"^(?:REP(?:NZ)? )?(?P<operator>MOVS|SCAS)(?P<suffix>[BWD])?", asm)
        if match:
            self.reason = self.movsInstr(fault_address, instr, match)
            return

    def movsInstr(self, fault_address, instr, match):
        operator = match.group("operator")
        suffix = match.group("suffix")
        size = {'B': 1, 'W': 2, 'D': 4}.get(suffix)
        error_cls = InvalidMemoryAccess
        try:
            process = self.process
            if CPU_64BITS:
                source_reg = 'rsi'
                dest_reg = 'rdi'
            else:
                source_reg = 'esi'
                dest_reg = 'edi'
            source_addr = process.getreg(source_reg)
            registers = {source_reg: source_addr}
            write = (operator == 'MOVS')
            if write:
                dest_addr = process.getreg(dest_reg)
                registers[dest_reg] = dest_addr

            if fault_address is not None:
                if fault_address == source_addr:
                    error_cls = InvalidRead
                if write and fault_address == dest_addr:
                    error_cls = InvalidWrite
            else:
                if write:
                    fault_address = (source_addr, dest_addr)
                else:
                    fault_address = (source_addr,)
        except PtraceError:
            registers = {}
        return error_cls(fault_address, size=size, instr=instr,
                         registers=registers, process=self.process)

    def getSignalInfo(self):
        if RUNNING_LINUX:
            return self.process.getsiginfo()
        else:
            return None

    def memoryFault(self):
        # Get fault
        siginfo = self.getSignalInfo()
        if siginfo:
            fault_address = siginfo._sigfault._addr
            if not fault_address:
                fault_address = 0
        else:
            fault_address = None

        # Get current instruction
        instr = self.getInstruction()

        # Call to invalid address?
        if fault_address is not None:
            try:
                ip = self.process.getInstrPointer()
                if ip == fault_address:
                    self.reason = InstructionError(ip, process=self.process)
                    return
            except PtraceError:
                pass

        # Stack overflow?
        stack = self.process.findStack()
        if stack:
            sp = self.process.getStackPointer()
            if not (stack.start <= sp <= stack.end):
                self.reason = StackOverflow(
                    sp, stack, instr=instr, process=self.process)
                return

        # Guess error type using the assembler instruction
        if instr:
            self.memoryFaultInstr(instr, fault_address)
            if self.reason:
                return

        # Last chance: use generic invalid memory access error
        self.reason = InvalidMemoryAccess(
            fault_address, instr=instr, process=self.process)

    def mathError(self):
        instr = self.getInstruction()
        if not instr:
            return
        match = re.match(r"I?DIV (.*)", instr.text)
        if not match:
            return
        self.reason = DivisionByZero(instr=instr, process=self.process)

    def childExit(self):
        siginfo = self.getSignalInfo()
        if siginfo:
            child = siginfo._sigchld
            self.reason = ChildExit(child.pid, child.status, child.uid)
        else:
            self.reason = ChildExit()

    def display(self, log=None):
        self._analyze()
        if not log:
            log = error
        log("-" * 60)
        log("PID: %s" % self.process.pid)
        log("Signal: %s" % self.name)
        if self.reason:
            self.reason.display(log)
        log("-" * 60)
