#!/usr/bin/env python3
# 
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
# Built on top of Unicorn emulator (www.unicorn-engine.org) 


import io
from elftools.elf.elffile import ELFFile

from qiling.const import *
from qiling.core import Qiling
from .loader import QlLoader


class IhexParser:
    def __init__(self, path):
        self.mem  = []
        self.segments = []

        self.base = 0
        with open(path, 'r') as f:
            for line in f.read().splitlines():
                self.parse_line(line.strip())

            begin, stream = 0, b''
            for addr, data in self.mem:
                if addr != begin + len(stream):
                    self.segments.append((begin, stream))
                    begin, stream = addr, data
                
                else:
                    stream += data

            self.segments.append((begin, stream))

    def parse_line(self, line):
        if len(line) < 9:
            return
        
        desc = line[7: 9]
        size = int(line[1: 3], 16)        
        
        addr = bytes.fromhex(line[3: 7])
        data = bytes.fromhex(line[9: 9 + size * 2])        
        
        if   desc == '00': # Data
            offset = int.from_bytes(addr, byteorder='big')
            self.mem.append((self.base + offset, data))

        elif desc == '02': # Extended Segment Address
            self.base = int.from_bytes(data, byteorder='big') * 0x10

        elif desc == '04': # Extended Linear Address
            self.base = int.from_bytes(data, byteorder='big') * 0x10000
        

class QlLoaderMCU(QlLoader):
    def __init__(self, ql:Qiling):
        super().__init__(ql)   
        
        self.entry_point = 0
        self.load_address = 0
        self.filetype = self.guess_filetype()
        
        if self.filetype == 'elf':
            with open(self.ql.path, 'rb') as infile:
                self.elf = ELFFile(io.BytesIO(infile.read()))
            
        elif self.filetype == 'bin':
            self.map_address = self.argv[1]

        else: # self.filetype == 'hex':
            self.ihex = IhexParser(self.ql.path)

    def guess_filetype(self):
        if self.ql.path.endswith('.elf'):
            return 'elf'            
            
        if self.ql.path.endswith('.bin'):
            return 'bin'

        if self.ql.path.endswith('.hex'):
            return 'hex'

        return 'elf'
    
    def reset(self):
        if self.filetype == 'elf':
            for segment in self.elf.iter_segments(type='PT_LOAD'):
                self.ql.mem.write(segment['p_paddr'], segment.data())

            # TODO: load symbol table

        elif self.filetype == 'bin':
            with open(self.ql.path, 'rb') as f:
                self.ql.mem.write(self.map_address, f.read())

        else: # self.filetype == 'hex':
            for begin, data in self.ihex.segments:
                self.ql.mem.write(begin, data)

        
        self.ql.arch.init_context()
        self.entry_point = self.ql.arch.regs.read('pc')

    def load_profile(self):
        self.ql.env.update(self.ql.profile)

    def load_env(self):
        for name, args in self.env.items():
            memtype = args['type']
            if memtype == 'memory':
                size = args['size']
                base = args['base']
                self.ql.mem.map(base, size, info=f'[{name}]')
            
            if memtype == 'remap':
                size = args['size']
                base = args['base']
                alias = args['alias']
                self.ql.hw.setup_remap(alias, base, size, info=f'[{name}]')

            if memtype == 'mmio':
                size = args['size']
                base = args['base']
                self.ql.hw.setup_mmio(base, size, info=f'[{name}]')

            if memtype == 'core':
                self.ql.hw.create(name.lower())

    def run(self):
        self.load_profile()
        self.load_env()
        
        ## Handle interrupt from instruction execution
        self.ql.hook_intr(self.ql.arch.unicorn_exception_handler)
                
        self.reset()
