"""
Nintendo DS .nds game file parser

File format references:
- http://www.bottledlight.com/ds/index.php/FileFormats/NDSFormat
- http://imrannazar.com/The-Smallest-NDS-File
- http://darkfader.net/ds/files/ndstool.cpp
- http://crackerscrap.com/docs/dsromstructure.html
- http://nocash.emubase.de/gbatek.htm
"""

from hachoir.parser import Parser
from hachoir.field import (UInt8, UInt16, UInt32, UInt64, String, RawBytes, SubFile, FieldSet, NullBits, Bit, Bits, Bytes,
                           SeekableFieldSet, RootSeekableFieldSet)
from hachoir.core.text_handler import textHandler, hexadecimal
from hachoir.core.endian import LITTLE_ENDIAN


"""
CRC16 Calculation

Modified from:
http://www.mail-archive.com/python-list@python.org/msg47844.html

Original License:
crc16.py by Bryan G. Olson, 2005
This module is free software and may be used and
distributed under the same terms as Python itself.
"""


class CRC16:
    _table = None

    def _initTable(self):
        from array import array

        # CRC-16 poly: p(x) = x**16 + x**15 + x**2 + 1
        # top bit implicit, reflected
        poly = 0xa001
        CRC16._table = array('H')
        for byte in range(256):
            crc = 0
            for bit in range(8):
                if (byte ^ crc) & 1:
                    crc = (crc >> 1) ^ poly
                else:
                    crc >>= 1
                byte >>= 1
            CRC16._table.append(crc)

    def checksum(self, string, value):
        if CRC16._table is None:
            self._initTable()

        for ch in string:
            value = self._table[ord(ch) ^ (value & 0xff)] ^ (value >> 8)
        return value


class Crc16(UInt16):
    "16 bit field for calculating and comparing CRC-16 of specified string"

    def __init__(self, parent, name, targetBytes):
        UInt16.__init__(self, parent, name)
        self.targetBytes = targetBytes

    def createDescription(self):
        crc = CRC16().checksum(self.targetBytes, 0xffff)
        if crc == self.value:
            return "matches CRC of %d bytes" % len(self.targetBytes)
        else:
            return "mismatch (calculated CRC %d for %d bytes)" % (crc, len(self.targetBytes))


class FileNameDirTable(FieldSet):
    static_size = (4 + 2 + 2) * 8

    def createFields(self):
        yield UInt32(self, "entry_start")
        yield UInt16(self, "entry_file_id")
        yield UInt16(self, "parent_id")

    def createDescription(self):
        return "first file id: %d; parent directory id: %d (%d)" % (self["entry_file_id"].value, self["parent_id"].value, self["parent_id"].value & 0xFFF)


class FileNameEntry(FieldSet):

    def createFields(self):
        yield Bits(self, "name_len", 7)
        yield Bit(self, "is_directory")
        yield String(self, "name", self["name_len"].value)
        if self["is_directory"].value:
            yield UInt16(self, "dir_id")

    def createDescription(self):
        s = ""
        if self["is_directory"].value:
            s = "[D] "
        return s + self["name"].value


class Directory(FieldSet):

    def createFields(self):
        while True:
            fne = FileNameEntry(self, "entry[]")
            if fne["name_len"].value == 0:
                yield UInt8(self, "end_marker")
                break
            yield fne


class FileNameTable(SeekableFieldSet):

    def createFields(self):
        self.startOffset = self.absolute_address // 8

        # parent_id of first FileNameDirTable contains number of directories:
        dt = FileNameDirTable(self, "dir_table[]")
        numDirs = dt["parent_id"].value
        yield dt

        for i in range(1, numDirs):
            yield FileNameDirTable(self, "dir_table[]")

        for i in range(0, numDirs):
            dt = self["dir_table[%d]" % i]
            offset = self.startOffset + dt["entry_start"].value
            self.seekByte(offset, relative=False)
            yield Directory(self, "directory[]")


class FATFileEntry(FieldSet):
    static_size = 2 * 4 * 8

    def createFields(self):
        yield UInt32(self, "start")
        yield UInt32(self, "end")

    def createDescription(self):
        return "start: %d; size: %d" % (self["start"].value, self["end"].value - self["start"].value)


class FATContent(FieldSet):

    def createFields(self):
        num_entries = self.parent["header"]["fat_size"].value // 8
        for i in range(0, num_entries):
            yield FATFileEntry(self, "entry[]")


class BannerTile(FieldSet):
    static_size = 32 * 8

    def createFields(self):
        for y in range(8):
            for x in range(8):
                yield Bits(self, "pixel[%d,%d]" % (x, y), 4)


class BannerIcon(FieldSet):
    static_size = 16 * 32 * 8

    def createFields(self):
        for y in range(4):
            for x in range(4):
                yield BannerTile(self, "tile[%d,%d]" % (x, y))


class NdsColor(FieldSet):
    static_size = 16

    def createFields(self):
        yield Bits(self, "red", 5)
        yield Bits(self, "green", 5)
        yield Bits(self, "blue", 5)
        yield NullBits(self, "pad", 1)

    def createDescription(self):
        return "#%02x%02x%02x" % (self["red"].value << 3, self["green"].value << 3, self["blue"].value << 3)


class Banner(FieldSet):
    static_size = 2112 * 8

    def createFields(self):
        yield UInt16(self, "version")
        # CRC of this structure, excluding first 32 bytes:
        yield Crc16(self, "crc", self.stream.readBytes(self.absolute_address + (32 * 8), (2112 - 32)))
        yield RawBytes(self, "reserved", 28)
        yield BannerIcon(self, "icon_data")
        for i in range(0, 16):
            yield NdsColor(self, "palette_color[]")
        yield String(self, "title_jp", 256, charset="UTF-16-LE", truncate="\0")
        yield String(self, "title_en", 256, charset="UTF-16-LE", truncate="\0")
        yield String(self, "title_fr", 256, charset="UTF-16-LE", truncate="\0")
        yield String(self, "title_de", 256, charset="UTF-16-LE", truncate="\0")
        yield String(self, "title_it", 256, charset="UTF-16-LE", truncate="\0")
        yield String(self, "title_es", 256, charset="UTF-16-LE", truncate="\0")


class Overlay(FieldSet):
    static_size = 8 * 4 * 8

    def createFields(self):
        yield UInt32(self, "id")
        yield textHandler(UInt32(self, "ram_address"), hexadecimal)
        yield UInt32(self, "ram_size")
        yield UInt32(self, "bss_size")
        yield textHandler(UInt32(self, "init_start_address"), hexadecimal)
        yield textHandler(UInt32(self, "init_end_address"), hexadecimal)
        yield UInt32(self, "file_id")
        yield RawBytes(self, "reserved[]", 4)

    def createDescription(self):
        return "file #%d, %d (+%d) bytes to 0x%08x" % (
            self["file_id"].value, self["ram_size"].value, self["bss_size"].value, self["ram_address"].value)


class SecureArea(FieldSet):
    static_size = 2048 * 8

    def createFields(self):
        yield textHandler(UInt64(self, "id"), hexadecimal)
        if self["id"].value == 0xe7ffdeffe7ffdeff:  # indicates that secure area is decrypted
            yield Bytes(self, "fixed[]", 6)  # always \xff\xde\xff\xe7\xff\xde
            yield Crc16(self, "header_crc16", self.stream.readBytes(self.absolute_address + (16 * 8), 2048 - 16))
            yield RawBytes(self, "unknown[]", 2048 - 16 - 2)
            yield Bytes(self, "fixed[]", 2)  # always \0\0
        else:
            yield RawBytes(self, "encrypted[]", 2048 - 8)


class DeviceSize(UInt8):

    def createDescription(self):
        return "%d Mbit" % ((2**(20 + self.value)) // (1024 * 1024))


class Header(FieldSet):

    def createFields(self):
        yield String(self, "game_title", 12, truncate="\0")
        yield String(self, "game_code", 4)
        yield String(self, "maker_code", 2)
        yield UInt8(self, "unit_code")
        yield UInt8(self, "device_code")

        yield DeviceSize(self, "card_size")
        yield String(self, "card_info", 9)
        yield UInt8(self, "rom_version")
        yield Bits(self, "unknown_flags[]", 2)
        yield Bit(self, "autostart_flag")
        yield Bits(self, "unknown_flags[]", 5)

        yield UInt32(self, "arm9_source", "ARM9 ROM offset")
        yield textHandler(UInt32(self, "arm9_execute_addr", "ARM9 entry address"), hexadecimal)
        yield textHandler(UInt32(self, "arm9_copy_to_addr", "ARM9 RAM address"), hexadecimal)
        yield UInt32(self, "arm9_bin_size", "ARM9 code size")

        yield UInt32(self, "arm7_source", "ARM7 ROM offset")
        yield textHandler(UInt32(self, "arm7_execute_addr", "ARM7 entry address"), hexadecimal)
        yield textHandler(UInt32(self, "arm7_copy_to_addr", "ARM7 RAM address"), hexadecimal)
        yield UInt32(self, "arm7_bin_size", "ARM7 code size")

        yield UInt32(self, "filename_table_offset")
        yield UInt32(self, "filename_table_size")
        yield UInt32(self, "fat_offset")
        yield UInt32(self, "fat_size")

        yield UInt32(self, "arm9_overlay_src")
        yield UInt32(self, "arm9_overlay_size")
        yield UInt32(self, "arm7_overlay_src")
        yield UInt32(self, "arm7_overlay_size")

        yield textHandler(UInt32(self, "ctl_read_flags"), hexadecimal)
        yield textHandler(UInt32(self, "ctl_init_flags"), hexadecimal)
        yield UInt32(self, "banner_offset")
        yield Crc16(self, "secure_crc16", self.stream.readBytes(0x4000 * 8, 0x4000))
        yield UInt16(self, "rom_timeout")

        yield UInt32(self, "arm9_unk_addr")
        yield UInt32(self, "arm7_unk_addr")
        yield UInt64(self, "unenc_mode_magic")

        yield UInt32(self, "rom_size")
        yield UInt32(self, "header_size")

        yield RawBytes(self, "unknown[]", 36)
        yield String(self, "passme_autoboot_detect", 4)
        yield RawBytes(self, "unknown[]", 16)

        yield RawBytes(self, "gba_logo", 156)
        yield Crc16(self, "logo_crc16", self.stream.readBytes(0xc0 * 8, 156))
        yield Crc16(self, "header_crc16", self.stream.readBytes(0, 350))

        yield UInt32(self, "debug_rom_offset")
        yield UInt32(self, "debug_size")
        yield textHandler(UInt32(self, "debug_ram_address"), hexadecimal)


class NdsFile(Parser, RootSeekableFieldSet):
    PARSER_TAGS = {
        "id": "nds_file",
        "category": "program",
        "file_ext": ("nds",),
        "mime": ("application/octet-stream",),
        "min_size": 352 * 8,  # just a minimal header
        "description": "Nintendo DS game file",
    }

    endian = LITTLE_ENDIAN

    def validate(self):
        try:
            header = self["header"]
        except Exception:
            return False

        return (self.stream.readBytes(0, 1) != b"\0"
                and (header["device_code"].value & 7) == 0
                and header["header_size"].value >= 352
                and header["card_size"].value < 15  # arbitrary limit at 32Gbit
                and header["arm9_bin_size"].value > 0 and header["arm9_bin_size"].value <= 0x3bfe00
                and header["arm7_bin_size"].value > 0 and header["arm7_bin_size"].value <= 0x3bfe00
                and header["arm9_source"].value + header["arm9_bin_size"].value < self._size
                and header["arm7_source"].value + header["arm7_bin_size"].value < self._size
                and header["arm9_execute_addr"].value >= 0x02000000 and header["arm9_execute_addr"].value <= 0x023bfe00
                and header["arm9_copy_to_addr"].value >= 0x02000000 and header["arm9_copy_to_addr"].value <= 0x023bfe00
                and header["arm7_execute_addr"].value >= 0x02000000 and header["arm7_execute_addr"].value <= 0x03807e00
                and header["arm7_copy_to_addr"].value >= 0x02000000 and header["arm7_copy_to_addr"].value <= 0x03807e00
                )

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

        # Secure Area
        if self["header"]["arm9_source"].value >= 0x4000 and self["header"]["arm9_source"].value < 0x8000:
            secStart = self["header"]["arm9_source"].value & 0xfffff000
            self.seekByte(secStart, relative=False)
            yield SecureArea(self, "secure_area", size=0x8000 - secStart)

        # ARM9 binary
        self.seekByte(self["header"]["arm9_source"].value, relative=False)
        yield RawBytes(self, "arm9_bin", self["header"]["arm9_bin_size"].value)

        # ARM7 binary
        self.seekByte(self["header"]["arm7_source"].value, relative=False)
        yield RawBytes(self, "arm7_bin", self["header"]["arm7_bin_size"].value)

        # File Name Table
        if self["header"]["filename_table_size"].value > 0:
            self.seekByte(
                self["header"]["filename_table_offset"].value, relative=False)
            yield FileNameTable(self, "filename_table", size=self["header"]["filename_table_size"].value * 8)

        # FAT
        if self["header"]["fat_size"].value > 0:
            self.seekByte(self["header"]["fat_offset"].value, relative=False)
            yield FATContent(self, "fat_content", size=self["header"]["fat_size"].value * 8)

        # banner
        if self["header"]["banner_offset"].value > 0:
            self.seekByte(
                self["header"]["banner_offset"].value, relative=False)
            yield Banner(self, "banner")

        # ARM9 overlays
        if self["header"]["arm9_overlay_src"].value > 0:
            self.seekByte(
                self["header"]["arm9_overlay_src"].value, relative=False)
            numOvls = self["header"]["arm9_overlay_size"].value // (8 * 4)
            for i in range(numOvls):
                yield Overlay(self, "arm9_overlay[]")

        # files
        if self["header"]["fat_size"].value > 0:
            for field in self["fat_content"]:
                if field["end"].value > field["start"].value:
                    self.seekByte(field["start"].value, relative=False)
                    yield SubFile(self, "file[]", field["end"].value - field["start"].value)
