#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#

from abc import abstractmethod

from unicorn import Uc
from unicorn.unicorn import UcContext
from capstone import Cs
from keystone import Ks

from qiling import Qiling
from qiling.const import QL_ARCH, QL_ENDIAN
from .register import QlRegisterManager
from .utils import QlArchUtils


class QlArch:
    type: QL_ARCH
    bits: int

    def __init__(self, ql: Qiling):
        self.ql = ql
        self.utils = QlArchUtils(ql)

    @property
    @abstractmethod
    def uc(self) -> Uc:
        """Get unicorn instance bound to arch.
        """

        pass

    @property
    @abstractmethod
    def regs(self) -> QlRegisterManager:
        """Architectural registers.
        """

        pass

    @property
    def pointersize(self) -> int:
        return self.bits // 8

    def stack_push(self, value: int) -> int:
        """Push a value onto the architectural stack.

        Args:
            value: a numeric value to push

        Returns: the top of stack after pushing the value
        """

        self.regs.arch_sp -= self.pointersize
        self.ql.mem.write_ptr(self.regs.arch_sp, value)

        return self.regs.arch_sp

    def stack_pop(self) -> int:
        """Pop a value from the architectural stack.

        Returns: the value at the top of stack
        """

        data = self.ql.mem.read_ptr(self.regs.arch_sp)
        self.regs.arch_sp += self.pointersize

        return data

    def stack_read(self, offset: int) -> int:
        """Peek the architectural stack at a specified offset from its top, without affecting
        the top of the stack.

        Note that this operation violates the FIFO property of the stack and may be used cautiously.

        Args:
            offset: offset in bytes from the top of the stack, not necessarily aligned to the
                    native stack item size. the offset may be either positive or netagive, where
                    a 0 value means retrieving the value at the top of the stack

        Returns: the value at the specified address
        """

        return self.ql.mem.read_ptr(self.regs.arch_sp + offset)

    def stack_write(self, offset: int, value: int) -> None:
        """Write a value to the architectural stack at a specified offset from its top, without
        affecting the top of the stack.

        Note that this operation violates the FIFO property of the stack and may be used cautiously.

        Args:
            offset: offset in bytes from the top of the stack, not necessarily aligned to the
                    native stack item size. the offset may be either positive or netagive, where
                    a 0 value means overwriting the value at the top of the stack
        """

        self.ql.mem.write_ptr(self.regs.arch_sp + offset, value)

    # Unicorn's CPU state save
    def save(self) -> UcContext:
        return self.uc.context_save()

    # Unicorn's CPU state restore method
    def restore(self, saved_context: UcContext):
        self.uc.context_restore(saved_context)

    @property
    @abstractmethod
    def disassembler(self) -> Cs:
        """Get disassembler instance bound to arch.
        """

        pass

    @property
    @abstractmethod
    def assembler(self) -> Ks:
        """Get assembler instance bound to arch.
        """

        pass

    @property
    @abstractmethod
    def endian(self) -> QL_ENDIAN:
        """Get processor endianess.
        """

        pass
