"""
InfoTech Storage Format (ITSF) parser, used by Microsoft's HTML Help (.chm)

Document:
- Microsoft's HTML Help (.chm) format
  http://www.wotsit.org (search "chm")
- chmlib library
  http://www.jedrea.com/chmlib/
- Unofficial CHM Spec
  http://savannah.nongnu.org/projects/chmspec
- Microsoft's HTML Help (.chm) format
  http://www.speakeasy.org/~russotto/chm/chmformat.html

Author: Victor Stinner
Creation date: 2007-03-04
"""

from hachoir.field import (Field, FieldSet, ParserError, RootSeekableFieldSet,
                           Int32, UInt16, UInt32, UInt64,
                           RawBytes, PaddingBytes,
                           Enum, String)
from hachoir.core.endian import LITTLE_ENDIAN
from hachoir.parser import HachoirParser
from hachoir.parser.common.win32 import GUID
from hachoir.parser.common.win32_lang_id import LANGUAGE_ID
from hachoir.core.text_handler import textHandler, hexadecimal, filesizeHandler


class CWord(Field):
    """
    Compressed double-word
    """

    def __init__(self, parent, name, description=None):
        Field.__init__(self, parent, name, 8, description)

        endian = self._parent.endian
        stream = self._parent.stream
        addr = self.absolute_address

        value = 0
        byte = stream.readBits(addr, 8, endian)
        while byte & 0x80:
            value <<= 7
            value += (byte & 0x7f)
            self._size += 8
            if 64 < self._size:
                raise ParserError("CHM: CWord is limited to 64 bits")
            addr += 8
            byte = stream.readBits(addr, 8, endian)
        value <<= 7
        value += byte
        self.createValue = lambda: value


class Filesize_Header(FieldSet):

    def createFields(self):
        yield textHandler(UInt32(self, "unknown[]", "0x01FE"), hexadecimal)
        yield textHandler(UInt32(self, "unknown[]", "0x0"), hexadecimal)
        yield filesizeHandler(UInt64(self, "file_size"))
        yield textHandler(UInt32(self, "unknown[]", "0x0"), hexadecimal)
        yield textHandler(UInt32(self, "unknown[]", "0x0"), hexadecimal)


class ITSP(FieldSet):

    def __init__(self, *args):
        FieldSet.__init__(self, *args)
        self._size = self["size"].value * 8

    def createFields(self):
        yield String(self, "magic", 4, "ITSP", charset="ASCII")
        yield UInt32(self, "version", "Version (=1)")
        yield filesizeHandler(UInt32(self, "size", "Length (in bytes) of the directory header (84)"))
        yield UInt32(self, "unknown[]", "(=10)")
        yield filesizeHandler(UInt32(self, "block_size", "Directory block size"))
        yield UInt32(self, "density", "Density of quickref section, usually 2")
        yield UInt32(self, "index_depth", "Depth of the index tree")
        yield Int32(self, "nb_dir", "Chunk number of root index chunk")
        yield UInt32(self, "first_pmgl", "Chunk number of first PMGL (listing) chunk")
        yield UInt32(self, "last_pmgl", "Chunk number of last PMGL (listing) chunk")
        yield Int32(self, "unknown[]", "-1")
        yield UInt32(self, "nb_dir_chunk", "Number of directory chunks (total)")
        yield Enum(UInt32(self, "lang_id", "Windows language ID"), LANGUAGE_ID)
        yield GUID(self, "system_uuid", "{5D02926A-212E-11D0-9DF9-00A0C922E6EC}")
        yield filesizeHandler(UInt32(self, "size2", "Same value than size"))
        yield Int32(self, "unknown[]", "-1")
        yield Int32(self, "unknown[]", "-1")
        yield Int32(self, "unknown[]", "-1")


class ITSF(FieldSet):

    def createFields(self):
        yield String(self, "magic", 4, "ITSF", charset="ASCII")
        yield UInt32(self, "version")
        yield UInt32(self, "header_size", "Total header length (in bytes)")
        yield UInt32(self, "one")
        yield UInt32(self, "last_modified", "Lower 32 bits of the time expressed in units of 0.1 us")
        yield Enum(UInt32(self, "lang_id", "Windows Language ID"), LANGUAGE_ID)
        yield GUID(self, "dir_uuid", "{7C01FD10-7BAA-11D0-9E0C-00A0-C922-E6EC}")
        yield GUID(self, "stream_uuid", "{7C01FD11-7BAA-11D0-9E0C-00A0-C922-E6EC}")
        yield UInt64(self, "filesize_offset")
        yield filesizeHandler(UInt64(self, "filesize_len"))
        yield UInt64(self, "dir_offset")
        yield filesizeHandler(UInt64(self, "dir_len"))
        if 3 <= self["version"].value:
            yield UInt64(self, "data_offset")


class PMGL_Entry(FieldSet):

    def createFields(self):
        yield CWord(self, "name_len")
        yield String(self, "name", self["name_len"].value, charset="UTF-8")
        yield CWord(self, "section", "Section number that the entry data is in.")
        yield CWord(self, "start", "Start offset of the data")
        yield filesizeHandler(CWord(self, "length", "Length of the data"))

    def createDescription(self):
        return "%s (%s)" % (self["name"].value, self["length"].display)


class PMGL(FieldSet):

    def createFields(self):
        # Header
        yield String(self, "magic", 4, "PMGL", charset="ASCII")
        yield filesizeHandler(Int32(self, "free_space",
                                    "Length of free space and/or quickref area at end of directory chunk"))
        yield Int32(self, "unknown")
        yield Int32(self, "previous", "Chunk number of previous listing chunk")
        yield Int32(self, "next", "Chunk number of previous listing chunk")

        # Entries
        stop = self.size - self["free_space"].value * 8
        entry_count = 0
        while self.current_size < stop:
            yield PMGL_Entry(self, "entry[]")
            entry_count += 1

        # Padding
        quickref_frequency = 1 + (1 << self["/dir/itsp/density"].value)
        num_quickref = (entry_count // quickref_frequency)
        if entry_count % quickref_frequency == 0:
            num_quickref -= 1
        print(self.current_size // 8, quickref_frequency, num_quickref)
        padding = (self["free_space"].value - (num_quickref * 2 + 2))
        if padding:
            yield PaddingBytes(self, "padding", padding)
        for i in range(num_quickref * quickref_frequency, 0, -quickref_frequency):
            yield UInt16(self, "quickref[%i]" % i)
        yield UInt16(self, "entry_count")


class PMGI_Entry(FieldSet):

    def createFields(self):
        yield CWord(self, "name_len")
        yield String(self, "name", self["name_len"].value, charset="UTF-8")
        yield CWord(self, "page")

    def createDescription(self):
        return "%s (page #%u)" % (self["name"].value, self["page"].value)


class PMGI(FieldSet):

    def createFields(self):
        yield String(self, "magic", 4, "PMGI", charset="ASCII")
        yield filesizeHandler(UInt32(self, "free_space",
                                     "Length of free space and/or quickref area at end of directory chunk"))

        stop = self.size - self["free_space"].value * 8
        while self.current_size < stop:
            yield PMGI_Entry(self, "entry[]")

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


class Directory(FieldSet):

    def createFields(self):
        yield ITSP(self, "itsp")
        block_size = self["itsp/block_size"].value * 8

        nb_dir = self["itsp/nb_dir"].value

        if nb_dir < 0:
            nb_dir = 1
        for index in range(nb_dir):
            yield PMGL(self, "pmgl[]", size=block_size)

        if self.current_size < self.size:
            yield PMGI(self, "pmgi", size=block_size)


class NameList(FieldSet):

    def createFields(self):
        yield UInt16(self, "length", "Length of name list in 2-byte blocks")
        yield UInt16(self, "count", "Number of entries in name list")
        for index in range(self["count"].value):
            length = UInt16(
                self, "name_len[]", "Length of name in 2-byte blocks, excluding terminating null")
            yield length
            yield String(self, "name[]", length.value * 2 + 2, charset="UTF-16-LE")


class ControlData(FieldSet):

    def createFields(self):
        yield UInt32(self, "count", "Number of DWORDS in this struct")
        yield String(self, "type", 4, "Type of compression")
        if self["type"].value != 'LZXC':
            return
        yield UInt32(self, "version", "Compression version")
        version = self["version"].value
        if version == 1:
            block = 'bytes'
        else:
            block = '32KB blocks'
        yield UInt32(self, "reset_interval", "LZX: Reset interval in %s" % block)
        yield UInt32(self, "window_size", "LZX: Window size in %s" % block)
        yield UInt32(self, "cache_size", "LZX: Cache size in %s" % block)
        yield UInt32(self, "unknown[]")


class ResetTable(FieldSet):

    def createFields(self):
        yield UInt32(self, "unknown[]", "Version number?")
        yield UInt32(self, "count", "Number of entries")
        yield UInt32(self, "entry_size", "Size of each entry")
        yield UInt32(self, "header_size", "Size of this header")
        yield UInt64(self, "uncompressed_size")
        yield UInt64(self, "compressed_size")
        yield UInt64(self, "block_size", "Block size in bytes")
        for i in range(self["count"].value):
            yield UInt64(self, "block_location[]", "location in compressed data of 1st block boundary in uncompressed data")


class SystemEntry(FieldSet):
    ENTRY_TYPE = {0: "HHP: [OPTIONS]: Contents File",
                  1: "HHP: [OPTIONS]: Index File",
                  2: "HHP: [OPTIONS]: Default Topic",
                  3: "HHP: [OPTIONS]: Title",
                  4: "File Metadata",
                  5: "HHP: [OPTIONS]: Default Window",
                  6: "HHP: [OPTIONS]: Compiled file",
                  # 7 present only in files with Binary Index; unknown function
                  # 8 unknown function
                  9: "Version",
                  10: "Timestamp",
                  # 11 only in Binary TOC files
                  12: "Number of Info Types",
                  13: "#IDXHDR file",
                  # 14 unknown function
                  # 15 checksum??
                  16: "HHP: [OPTIONS]: Default Font",
                  }

    def createFields(self):
        yield Enum(UInt16(self, "type", "Type of entry"), self.ENTRY_TYPE)
        yield UInt16(self, "length", "Length of entry")
        yield RawBytes(self, "data", self["length"].value)

    def createDescription(self):
        return '#SYSTEM Entry, Type %s' % self["type"].display


class SystemFile(FieldSet):

    def createFields(self):
        yield UInt32(self, "version", "Either 2 or 3")
        while self.current_size < self.size:
            yield SystemEntry(self, "entry[]")


class ChmFile(HachoirParser, RootSeekableFieldSet):
    MAGIC = b"ITSF\3\0\0\0"
    PARSER_TAGS = {
        "id": "chm",
        "category": "misc",
        "file_ext": ("chm",),
        "min_size": 4 * 8,
        "magic": ((MAGIC, 0),),
        "description": "Microsoft's HTML Help (.chm)",
    }
    endian = LITTLE_ENDIAN

    def __init__(self, stream, **args):
        RootSeekableFieldSet.__init__(
            self, None, "root", stream, None, stream.askSize(self))
        HachoirParser.__init__(self, stream, **args)

    def validate(self):
        if self.stream.readBytes(0, len(self.MAGIC)) != self.MAGIC:
            return "Invalid magic"
        return True

    def createFields(self):
        yield ITSF(self, "itsf")
        yield Filesize_Header(self, "file_size", size=self["itsf/filesize_len"].value * 8)

        self.seekByte(self["itsf/dir_offset"].value)
        directory = Directory(self, "dir", size=self["itsf/dir_len"].value * 8)
        yield directory

        otherentries = {}
        for pmgl in directory.array("pmgl"):
            for entry in pmgl.array("entry"):
                if entry["section"].value != 0:
                    otherentries.setdefault(
                        entry["section"].value, []).append(entry)
                    continue
                if entry["length"].value == 0:
                    continue
                self.seekByte(
                    self["itsf/data_offset"].value + entry["start"].value)
                name = entry["name"].value
                if name == "::DataSpace/NameList":
                    yield NameList(self, "name_list")
                elif name.startswith('::DataSpace/Storage/'):
                    sectname = str(name.split('/')[2])
                    if name.endswith('/SpanInfo'):
                        yield UInt64(self, "%s_spaninfo" % sectname, "Size of uncompressed data in the %s section" % sectname)
                    elif name.endswith('/ControlData'):
                        yield ControlData(self, "%s_controldata" % sectname, "Data about the compression scheme", size=entry["length"].value * 8)
                    elif name.endswith('/Transform/List'):
                        yield String(self, "%s_transform_list" % sectname, 38, description="Transform/List element", charset="UTF-16-LE")
                    elif name.endswith('/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/ResetTable'):
                        yield ResetTable(self, "%s_reset_table" % sectname, "LZX Reset Table", size=entry["length"].value * 8)
                    elif name.endswith('/Content'):
                        # eventually, a LZX wrapper will appear here, we hope!
                        yield RawBytes(self, "%s_content" % sectname, entry["length"].value, "Content for the %s section" % sectname)
                    else:
                        yield RawBytes(self, "entry_data[]", entry["length"].value, name)
                elif name == "/#SYSTEM":
                    yield SystemFile(self, "system_file", size=entry["length"].value * 8)
                else:
                    yield RawBytes(self, "entry_data[]", entry["length"].value, name)

    def getFile(self, filename):
        page = 0
        if 'pmgi' in self['/dir']:
            for entry in self['/dir/pmgi'].array('entry'):
                if entry['name'].value <= filename:
                    page = entry['page'].value
        pmgl = self['/dir/pmgl[%i]' % page]
        for entry in pmgl.array('entry'):
            if entry['name'].value == filename:
                return entry
        raise ParserError("File '%s' not found!" % filename)

    def createContentSize(self):
        return self["file_size/file_size"].value * 8
