# This file is used to process section data generated by `objdump -s`
import re


class Section(object):
    """
    One Section of section table. contains info about section name, address and raw data
    """
    SECTION_START_PATTERN = re.compile(b'Contents of section (.+?):')
    DATA_PATTERN = re.compile(b'([0-9a-f]{4,8})')

    def __init__(self, name, start_address, data):
        self.name = name
        self.start_address = start_address
        self.data = data

    def __contains__(self, item):
        """ check if the section name and address match this section """
        if (item['section'] == self.name or item['section'] == 'any') \
                and (self.start_address <= item['address'] < (self.start_address + len(self.data))):
            return True
        else:
            return False

    def __getitem__(self, item):
        """
        process slice.
        convert absolute address to relative address in current section and return slice result
        """
        if isinstance(item, int):
            return self.data[item - self.start_address]
        elif isinstance(item, slice):
            start = item.start if item.start is None else item.start - self.start_address
            stop = item.stop if item.stop is None else item.stop - self.start_address
            return self.data[start:stop]
        return self.data[item]

    def __str__(self):
        return '%s [%08x - %08x]' % (self.name, self.start_address, self.start_address + len(self.data))

    __repr__ = __str__

    @classmethod
    def parse_raw_data(cls, raw_data):
        """
        process raw data generated by `objdump -s`, create section and return un-processed lines
        :param raw_data: lines of raw data generated by `objdump -s`
        :return: one section, un-processed lines
        """
        name = ''
        data = ''
        start_address = 0
        # first find start line
        for i, line in enumerate(raw_data):
            if b'Contents of section ' in line:  # do strcmp first to speed up
                match = cls.SECTION_START_PATTERN.search(line)
                if match is not None:
                    name = match.group(1)
                    raw_data = raw_data[i + 1:]
                    break
        else:
            # do some error handling
            raw_data = [b'']  # add a dummy first data line

        def process_data_line(line_to_process):
            # first remove the ascii part
            hex_part = line_to_process.split(b'  ')[0]
            # process rest part
            data_list = cls.DATA_PATTERN.findall(hex_part)
            try:
                _address = int(data_list[0], base=16)
            except IndexError:
                _address = -1

            def hex_to_str(hex_data):
                if len(hex_data) % 2 == 1:
                    hex_data = b'0' + hex_data  # append zero at the beginning
                _length = len(hex_data)
                return ''.join([chr(int(hex_data[_i:_i + 2], base=16))
                                for _i in range(0, _length, 2)])

            return _address, ''.join([hex_to_str(x) for x in data_list[1:]])

        # handle first line:
        address, _data = process_data_line(raw_data[0])
        if address != -1:
            start_address = address
            data += _data
            raw_data = raw_data[1:]
            for i, line in enumerate(raw_data):
                address, _data = process_data_line(line)
                if address == -1:
                    raw_data = raw_data[i:]
                    break
                else:
                    data += _data
        else:
            # do error handling
            raw_data = []

        section = cls(name, start_address, data) if start_address != -1 else None
        unprocessed_data = None if len(raw_data) == 0 else raw_data
        return section, unprocessed_data


class SectionTable(object):
    """ elf section table """

    def __init__(self, file_name):
        with open(file_name, 'rb') as f:
            raw_data = f.readlines()
        self.table = []
        while raw_data:
            section, raw_data = Section.parse_raw_data(raw_data)
            self.table.append(section)

    def get_unsigned_int(self, section, address, size=4, endian='LE'):
        """
        get unsigned int from section table
        :param section: section name; use "any" will only match with address
        :param address: start address
        :param size: size in bytes
        :param endian: LE or BE
        :return: int or None
        """
        if address % 4 != 0 or size % 4 != 0:
            print('warning: try to access without 4 bytes aligned')
        key = {'address': address, 'section': section}
        for section in self.table:
            if key in section:
                tmp = section[address:address + size]
                value = 0
                for i in range(size):
                    if endian == 'LE':
                        value += ord(tmp[i]) << (i * 8)
                    elif endian == 'BE':
                        value += ord(tmp[i]) << ((size - i - 1) * 8)
                    else:
                        print('only support LE or BE for parameter endian')
                        assert False
                break
        else:
            value = None
        return value

    def get_string(self, section, address):
        """
        get string ('\0' terminated) from section table
        :param section: section name; use "any" will only match with address
        :param address: start address
        :return: string or None
        """
        value = None
        key = {'address': address, 'section': section}
        for section in self.table:
            if key in section:
                value = section[address:]
                for i, c in enumerate(value):
                    if c == '\0':
                        value = value[:i]
                        break
                break
        return value
