"""
Parser for resource of Microsoft Windows Portable Executable (PE).

Documentation:
- Wine project
  VS_FIXEDFILEINFO structure, file include/winver.h

Author: Victor Stinner
Creation date: 2007-01-19
"""

from hachoir.field import (FieldSet, ParserError, Enum,
                           Bit, Bits, SeekableFieldSet,
                           UInt16, UInt32, TimestampUnix32,
                           Bytes, RawBytes, PaddingBytes, NullBytes, NullBits,
                           CString, String)
from hachoir.core.text_handler import textHandler, filesizeHandler, hexadecimal
from hachoir.core.tools import createDict, paddingSize, alignValue, makePrintable
from hachoir.parser.common.win32 import BitmapInfoHeader

MAX_DEPTH = 5
MAX_INDEX_PER_HEADER = 300
MAX_NAME_PER_HEADER = MAX_INDEX_PER_HEADER


class Version(FieldSet):
    static_size = 32

    def createFields(self):
        yield textHandler(UInt16(self, "minor", "Minor version number"), hexadecimal)
        yield textHandler(UInt16(self, "major", "Major version number"), hexadecimal)

    def createValue(self):
        return self["major"].value + float(self["minor"].value) / 10000


MAJOR_OS_NAME = {
    1: "DOS",
    2: "OS/2 16-bit",
    3: "OS/2 32-bit",
    4: "Windows NT",
}

MINOR_OS_BASE = 0
MINOR_OS_NAME = {
    0: "Base",
    1: "Windows 16-bit",
    2: "Presentation Manager 16-bit",
    3: "Presentation Manager 32-bit",
    4: "Windows 32-bit",
}

FILETYPE_DRIVER = 3
FILETYPE_FONT = 4
FILETYPE_NAME = {
    1: "Application",
    2: "DLL",
    3: "Driver",
    4: "Font",
    5: "VXD",
    7: "Static library",
}

DRIVER_SUBTYPE_NAME = {
    1: "Printer",
    2: "Keyboard",
    3: "Language",
    4: "Display",
    5: "Mouse",
    6: "Network",
    7: "System",
    8: "Installable",
    9: "Sound",
    10: "Communications",
}

FONT_SUBTYPE_NAME = {
    1: "Raster",
    2: "Vector",
    3: "TrueType",
}


class VersionInfoBinary(FieldSet):

    def createFields(self):
        yield textHandler(UInt32(self, "magic", "File information magic (0xFEEF04BD)"), hexadecimal)
        if self["magic"].value != 0xFEEF04BD:
            raise ParserError("EXE resource: invalid file info magic")
        yield Version(self, "struct_ver", "Structure version (1.0)")
        yield Version(self, "file_ver_ms", "File version MS")
        yield Version(self, "file_ver_ls", "File version LS")
        yield Version(self, "product_ver_ms", "Product version MS")
        yield Version(self, "product_ver_ls", "Product version LS")
        yield textHandler(UInt32(self, "file_flags_mask"), hexadecimal)

        yield Bit(self, "debug")
        yield Bit(self, "prerelease")
        yield Bit(self, "patched")
        yield Bit(self, "private_build")
        yield Bit(self, "info_inferred")
        yield Bit(self, "special_build")
        yield NullBits(self, "reserved", 26)

        yield Enum(textHandler(UInt16(self, "file_os_major"), hexadecimal), MAJOR_OS_NAME)
        yield Enum(textHandler(UInt16(self, "file_os_minor"), hexadecimal), MINOR_OS_NAME)
        yield Enum(textHandler(UInt32(self, "file_type"), hexadecimal), FILETYPE_NAME)
        field = textHandler(UInt32(self, "file_subfile"), hexadecimal)
        if field.value == FILETYPE_DRIVER:
            field = Enum(field, DRIVER_SUBTYPE_NAME)
        elif field.value == FILETYPE_FONT:
            field = Enum(field, FONT_SUBTYPE_NAME)
        yield field
        yield TimestampUnix32(self, "date_ms")
        yield TimestampUnix32(self, "date_ls")


class VersionInfoNode(FieldSet):
    TYPE_STRING = 1
    TYPE_NAME = {
        0: "binary",
        1: "string",
    }

    def __init__(self, parent, name, is_32bit=True):
        FieldSet.__init__(self, parent, name)
        self._size = alignValue(self["size"].value, 4) * 8
        self.is_32bit = is_32bit

    def createFields(self):
        yield UInt16(self, "size", "Node size (in bytes)")
        yield UInt16(self, "data_size")
        yield Enum(UInt16(self, "type"), self.TYPE_NAME)
        yield CString(self, "name", charset="UTF-16-LE")

        size = paddingSize(self.current_size // 8, 4)
        if size:
            yield NullBytes(self, "padding[]", size)
        size = self["data_size"].value
        if size:
            if self["type"].value == self.TYPE_STRING:
                if self.is_32bit:
                    size *= 2
                yield String(self, "value", size, charset="UTF-16-LE", truncate="\0")
            elif self["name"].value == "VS_VERSION_INFO":
                yield VersionInfoBinary(self, "value", size=size * 8)
                if self["value/file_flags_mask"].value == 0:
                    self.is_32bit = False
            else:
                yield RawBytes(self, "value", size)
        while 12 <= (self.size - self.current_size) // 8:
            yield VersionInfoNode(self, "node[]", self.is_32bit)
        size = (self.size - self.current_size) // 8
        if size:
            yield NullBytes(self, "padding[]", size)

    def createDescription(self):
        text = "Version info node: %s" % self["name"].value
        if self["type"].value == self.TYPE_STRING and "value" in self:
            text += "=%s" % self["value"].value
        return text


def parseVersionInfo(parent):
    yield VersionInfoNode(parent, "node[]")


def parseIcon(parent):
    yield BitmapInfoHeader(parent, "bmp_header")
    size = (parent.size - parent.current_size) // 8
    if size:
        yield RawBytes(parent, "raw", size)


class WindowsString(FieldSet):

    def createFields(self):
        yield UInt16(self, "length", "Number of 16-bit characters")
        size = self["length"].value * 2
        if size:
            yield String(self, "text", size, charset="UTF-16-LE")

    def createValue(self):
        if "text" in self:
            return self["text"].value
        else:
            return ""

    def createDisplay(self):
        return makePrintable(self.value, "UTF-8", quote='"')


def parseStringTable(parent):
    while not parent.eof:
        yield WindowsString(parent, "string[]")


RESOURCE_TYPE = {
    1: ("cursor[]", "Cursor", None),
    2: ("bitmap[]", "Bitmap", None),
    3: ("icon[]", "Icon", parseIcon),
    4: ("menu[]", "Menu", None),
    5: ("dialog[]", "Dialog", None),
    6: ("string_table[]", "String table", parseStringTable),
    7: ("font_dir[]", "Font directory", None),
    8: ("font[]", "Font", None),
    9: ("accelerators[]", "Accelerators", None),
    10: ("raw_res[]", "Unformatted resource data", None),
    11: ("message_table[]", "Message table", None),
    12: ("group_cursor[]", "Group cursor", None),
    14: ("group_icon[]", "Group icon", None),
    16: ("version_info", "Version information", parseVersionInfo),
}


class Entry(FieldSet):
    static_size = 16 * 8

    def __init__(self, parent, name, inode=None):
        FieldSet.__init__(self, parent, name)
        self.inode = inode

    def createFields(self):
        yield textHandler(UInt32(self, "rva"), hexadecimal)
        yield filesizeHandler(UInt32(self, "size"))
        yield UInt32(self, "codepage")
        yield NullBytes(self, "reserved", 4)

    def createDescription(self):
        return "Entry #%u: offset=%s size=%s" % (
            self.inode["offset"].value, self["rva"].display, self["size"].display)


class NameOffset(FieldSet):

    def __init__(self, parent, name):
        FieldSet.__init__(self, parent, name)
        self.name_field = None

    def createFields(self):
        yield Bits(self, "name_offset", 31)
        yield Bit(self, "is_name")
        yield Bits(self, "offset", 31)
        yield Bit(self, "is_subdir")

    def getResType(self):
        return self.name_field.value

    def createDescription(self):
        if self["is_subdir"].value:
            return "Sub-directory: %s at %s" % (self.name_field.display, self["offset"].value)
        else:
            return "Index: %s at %s" % (self.name_field.display, self["offset"].value)


class IndexOffset(FieldSet):
    TYPE_DESC = createDict(RESOURCE_TYPE, 1)

    def __init__(self, parent, name, res_type=None):
        FieldSet.__init__(self, parent, name)
        self.res_type = res_type

    def createFields(self):
        if self.res_type is None:
            # immediate subdirectory of the root
            yield Enum(UInt32(self, "type"), self.TYPE_DESC)
        else:
            # sub-subdirectory, "type" field is just an ID
            yield textHandler(UInt32(self, "type"), lambda field: "ID %d" % field.value)
        yield Bits(self, "offset", 31)
        yield Bit(self, "is_subdir")

    def getResType(self):
        return self["type"].value

    def createDescription(self):
        if self["is_subdir"].value:
            return "Sub-directory: %s at %s" % (self["type"].display, self["offset"].value)
        else:
            return "Index: %s at %s" % (self["type"].display, self["offset"].value)


class ResourceContent(FieldSet):

    def __init__(self, parent, name, entry, size=None):
        FieldSet.__init__(self, parent, name, size=entry["size"].value * 8)
        self.entry = entry
        res_type = self.getResType()
        if res_type in RESOURCE_TYPE:
            self._name, description, self._parser = RESOURCE_TYPE[res_type]
        else:
            self._parser = None

    def getResID(self):
        return self.entry.inode["offset"].value

    def getResType(self):
        return self.entry.inode.res_type

    def createFields(self):
        if self._parser:
            yield from self._parser(self)
        else:
            yield RawBytes(self, "content", self.size // 8)

    def createDescription(self):
        return "Resource #%u content: type=%s" % (
            self.getResID(), self.getResType())


class Header(FieldSet):
    static_size = 16 * 8

    def createFields(self):
        yield NullBytes(self, "options", 4)
        yield TimestampUnix32(self, "creation_date")
        yield UInt16(self, "maj_ver", "Major version")
        yield UInt16(self, "min_ver", "Minor version")
        yield UInt16(self, "nb_name", "Number of named entries")
        yield UInt16(self, "nb_index", "Number of indexed entries")

    def createDescription(self):
        text = "Resource header"
        info = []
        if self["nb_name"].value:
            info.append("%u name" % self["nb_name"].value)
        if self["nb_index"].value:
            info.append("%u index" % self["nb_index"].value)
        if self["creation_date"].value:
            info.append(self["creation_date"].display)
        if info:
            return "%s: %s" % (text, ", ".join(info))
        else:
            return text


class WidePascalString16(String):

    def __init__(self, parent, name, description=None,
                 strip=None, nbytes=None, truncate=None):
        Bytes.__init__(self, parent, name, 1, description)

        self._format = "WidePascalString16"
        self._strip = strip
        self._truncate = truncate
        self._character_size = 2
        self._charset = "UTF-16-LE"
        self._content_offset = 2
        self._content_size = self._character_size * self._parent.stream.readBits(
            self.absolute_address, self._content_offset * 8, self._parent.endian)
        self._size = (self._content_size + self.content_offset) * 8


class Directory(FieldSet):

    def __init__(self, parent, name, res_type=None):
        FieldSet.__init__(self, parent, name)
        nb_entries = self["header/nb_name"].value + \
            self["header/nb_index"].value
        self._size = Header.static_size + nb_entries * 64
        self.res_type = res_type

    def createFields(self):
        yield Header(self, "header")

        if MAX_NAME_PER_HEADER < self["header/nb_name"].value:
            raise ParserError("EXE resource: invalid number of name (%s)"
                              % self["header/nb_name"].value)
        if MAX_INDEX_PER_HEADER < self["header/nb_index"].value:
            raise ParserError("EXE resource: invalid number of index (%s)"
                              % self["header/nb_index"].value)

        hdr = self["header"]
        for index in range(hdr["nb_name"].value):
            yield NameOffset(self, "name[]")
        for index in range(hdr["nb_index"].value):
            yield IndexOffset(self, "index[]", self.res_type)

    def createDescription(self):
        return self["header"].description


class PE_Resource(SeekableFieldSet):

    def __init__(self, parent, name, section, size):
        SeekableFieldSet.__init__(self, parent, name, size=size)
        self.section = section

    def parseSub(self, directory, name, depth):
        indexes = []
        for index in directory.array("name"):
            if index["is_subdir"].value:
                indexes.append(index)
            self.seekByte(index["name_offset"].value)
            field = WidePascalString16(self, name.replace("directory", "name"))
            index.name_field = field
            yield field

        for index in directory.array("index"):
            if index["is_subdir"].value:
                indexes.append(index)

        # indexes.sort(key=lambda index: index["offset"].value)
        for index in indexes:
            self.seekByte(index["offset"].value)
            if depth == 1:
                res_type = index.getResType()
            else:
                res_type = directory.res_type
            yield Directory(self, name, res_type)

    def createFields(self):
        # Parse directories
        depth = 0
        subdir = Directory(self, "root")
        yield subdir
        subdirs = [subdir]
        alldirs = [subdir]
        while subdirs:
            depth += 1
            if MAX_DEPTH < depth:
                self.error(
                    "EXE resource: depth too high (%s), stop parsing directories" % depth)
                break
            newsubdirs = []
            for index, subdir in enumerate(subdirs):
                name = "directory[%u][%u][]" % (depth, index)
                try:
                    for field in self.parseSub(subdir, name, depth):
                        if field.__class__ == Directory:
                            newsubdirs.append(field)
                        yield field
                except Exception as err:
                    self.error("Unable to create directory %s: %s" %
                               (name, err))
            subdirs = newsubdirs
            alldirs.extend(subdirs)

        # Create resource list
        resources = []
        for directory in alldirs:
            for index in directory.array("index"):
                if not index["is_subdir"].value:
                    resources.append(index)

        # Parse entries
        entries = []
        for resource in resources:
            offset = resource["offset"].value
            if offset is None:
                continue
            self.seekByte(offset)
            entry = Entry(self, "entry[]", inode=resource)
            yield entry
            entries.append(entry)
        entries.sort(key=lambda entry: entry["rva"].value)

        # Parse resource content
        for entry in entries:
            try:
                offset = self.section.rva2file(entry["rva"].value)
                padding = self.seekByte(offset, relative=False)
                if padding:
                    yield padding
                yield ResourceContent(self, "content[]", entry)
            except Exception as err:
                self.warning("Error when parsing entry %s: %s" %
                             (entry.path, err))

        size = (self.size - self.current_size) // 8
        if size:
            yield PaddingBytes(self, "padding_end", size)


class NE_VersionInfoNode(FieldSet):
    TYPE_STRING = 1
    TYPE_NAME = {
        0: "binary",
        1: "string",
    }

    def __init__(self, parent, name):
        FieldSet.__init__(self, parent, name)
        self._size = alignValue(self["size"].value, 4) * 8

    def createFields(self):
        yield UInt16(self, "size", "Node size (in bytes)")
        yield UInt16(self, "data_size")
        yield CString(self, "name", charset="ISO-8859-1")

        size = paddingSize(self.current_size // 8, 4)
        if size:
            yield NullBytes(self, "padding[]", size)
        size = self["data_size"].value
        if size:
            if self["name"].value == "VS_VERSION_INFO":
                yield VersionInfoBinary(self, "value", size=size * 8)
            else:
                yield String(self, "value", size, charset="ISO-8859-1")
        while 12 <= (self.size - self.current_size) // 8:
            yield NE_VersionInfoNode(self, "node[]")
        size = (self.size - self.current_size) // 8
        if size:
            yield NullBytes(self, "padding[]", size)

    def createDescription(self):
        text = "Version info node: %s" % self["name"].value
#        if self["type"].value == self.TYPE_STRING and "value" in self:
#            text += "=%s" % self["value"].value
        return text
