#!/usr/bin/env python3
"""
Python ZPackage parser for Valheim .db files.
Extracts cartography table data (explored map bitmap + pins).
"""
import struct, gzip, io, sys, os
from PIL import Image

DB_PATH = "/tmp/Nerdhalla.db"
OUTPUT_DIR = "/tmp/valheim_map_output"

# Known prefab hashes for cartography table
# Valheim uses fnv-1a 64-bit hashes for prefab names
CARTOGRAPHY_TABLE_HASH = None  # We'll search for it

def fnv1a_64(data):
    """FNV-1a 64-bit hash (Valheim's prefab hashing)."""
    h = 0xcbf29ce484222325
    for b in data:
        h ^= b
        h = (h * 0x100000001b3) & 0xffffffffffffffff
    return h

# Pre-calculate common prefab hashes
PREFAB_HASHES = {
    'CartographyTable': None,
}

class ZPackage:
    """Read-only ZPackage parser (Valheim binary serialization format)."""
    
    def __init__(self, data):
        self.data = data
        self.pos = 0
    
    def read_int32(self):
        v = struct.unpack_from('<i', self.data, self.pos)[0]
        self.pos += 4
        return v
    
    def read_uint32(self):
        v = struct.unpack_from('<I', self.data, self.pos)[0]
        self.pos += 4
        return v
    
    def read_int64(self):
        v = struct.unpack_from('<q', self.data, self.pos)[0]
        self.pos += 8
        return v
    
    def read_uint64(self):
        v = struct.unpack_from('<Q', self.data, self.pos)[0]
        self.pos += 8
        return v
    
    def read_float(self):
        v = struct.unpack_from('<f', self.data, self.pos)[0]
        self.pos += 4
        return v
    
    def read_double(self):
        v = struct.unpack_from('<d', self.data, self.pos)[0]
        self.pos += 8
        return v
    
    def read_byte(self):
        v = self.data[self.pos]
        self.pos += 1
        return v
    
    def read_bool(self):
        return self.read_byte() != 0
    
    def read_bytes(self, n):
        v = self.data[self.pos:self.pos+n]
        self.pos += n
        return v
    
    def read_string_length(self):
        """Variable-length integer (7-bit per byte, LEB128-like)."""
        result = 0
        shift = 0
        while True:
            byte = self.read_byte()
            result |= (byte & 0x7F) << shift
            shift += 7
            if (byte & 0x80) == 0:
                break
        return result
    
    def read_string(self):
        length = self.read_string_length()
        if length == 0:
            return ""
        s = self.data[self.pos:self.pos+length].decode('utf-8', errors='replace')
        self.pos += length
        return s
    
    def read_short(self):
        v = struct.unpack_from('<h', self.data, self.pos)[0]
        self.pos += 2
        return v
    
    def read_ushort(self):
        v = struct.unpack_from('<H', self.data, self.pos)[0]
        self.pos += 2
        return v
    
    def read_compressed(self):
        """Read a compressed ZPackage section."""
        length = self.read_int32()
        compressed = self.read_bytes(length)
        try:
            decompressed = gzip.decompress(compressed)
            return ZPackage(decompressed)
        except:
            return None
    
    def read_length_prefixed_bytes(self):
        length = self.read_int32()
        return self.read_bytes(length)
    
    def skip(self, n):
        self.pos += n
    
    def remaining(self):
        return len(self.data) - self.pos
    
    def tell(self):
        return self.pos


def parse_db(path):
    """Parse the .db file and extract cartography table data."""
    with open(path, 'rb') as f:
        raw = f.read()
    
    print(f"File size: {len(raw)} bytes")
    zp = ZPackage(raw)
    
    # Header
    world_version = zp.read_int32()
    print(f"World version: {world_version}")
    
    net_time = zp.read_double()
    print(f"Net time: {net_time}")
    
    my_id = zp.read_int64()
    next_uid = zp.read_uint32()
    num_zdos = zp.read_int32()
    print(f"My ID: {my_id}, Next UID: {next_uid}, ZDOs: {num_zdos}")
    
    # Calculate the hash for CartographyTable
    cart_hash = fnv1a_64(b'CartographyTable')
    print(f"CartographyTable hash: {cart_hash}")
    
    # Also check for 'piece_cartographytable' (the actual in-game name)
    piece_hash = fnv1a_64(b'piece_cartographytable')
    print(f"piece_cartographytable hash: {piece_hash}")
    
    # Skip through ZDOs looking for cartography table
    cart_found = None
    zdo_count = 0
    
    print(f"\nScanning {num_zdos} ZDOs for cartography table...")
    
    for i in range(num_zdos):
        if i % 100000 == 0 and i > 0:
            print(f"  Scanned {i}/{num_zdos}...")
        
        zdo_start = zp.tell()
        
        # ZDO header
        uid = zp.read_int64()
        prefab = zp.read_int32()
        owner = zp.read_int32()
        data_type = zp.read_int32()
        revision = zp.read_int32()
        
        # Check if this is a cartography table
        if prefab == cart_hash or prefab == piece_hash:
            print(f"\n  *** FOUND Cartography Table at ZDO {i} ***")
            print(f"  UID: {uid}, Prefab hash: {prefab}, Owner: {owner}")
            print(f"  Data type: {data_type}, Revision: {revision}")
            cart_found = (i, uid, prefab, zp.tell())
            break
        
        # Skip ZDO data based on data_type
        if data_type == 0:
            # No data
            pass
        elif data_type == 1:
            # Has persistent data
            _ = zp.read_int32()  # data length
            # Skip the data
            # Actually we need to parse it properly
            # For now, just skip based on remaining data
            pass
        elif data_type == 2:
            # Has ZDO data (key-value pairs)
            num_keys = zp.read_byte()
            for _ in range(num_keys):
                key_hash = zp.read_int32()
                value_type = zp.read_byte()
                if value_type == 0:  # float
                    zp.skip(4)
                elif value_type == 1:  # Vector3
                    zp.skip(12)
                elif value_type == 2:  # Quaternion
                    zp.skip(16)
                elif value_type == 3:  # int
                    zp.skip(4)
                elif value_type == 4:  # long
                    zp.skip(8)
                elif value_type == 5:  # string
                    _ = zp.read_string()
                elif value_type == 6:  # byte array
                    length = zp.read_int32()
                    zp.skip(length)
                else:
                    print(f"  Unknown value type {value_type} at {zp.tell()}")
                    break
        elif data_type == 3:
            # Both persistent and ZDO data
            _ = zp.read_int32()  # persistent data length
            num_keys = zp.read_byte()
            for _ in range(num_keys):
                key_hash = zp.read_int32()
                value_type = zp.read_byte()
                if value_type == 0:
                    zp.skip(4)
                elif value_type == 1:
                    zp.skip(12)
                elif value_type == 2:
                    zp.skip(16)
                elif value_type == 3:
                    zp.skip(4)
                elif value_type == 4:
                    zp.skip(8)
                elif value_type == 5:
                    _ = zp.read_string()
                elif value_type == 6:
                    length = zp.read_int32()
                    zp.skip(length)
                else:
                    break
        
        # Skip sector data
        # Each ZDO has sector info: 2 shorts (sector x, y) + 3 floats (position) + 4 floats (rotation)
        # Actually this varies by version. Let's just skip to next ZDO by finding it.
        
        # For world version >= 33, ZDOs have a compact format
        # Let's just try to find the next ZDO by looking for the pattern
    
    if cart_found:
        print(f"\nCartography table found at ZDO index {cart_found[0]}")
    else:
        print(f"\nCartography table not found in {num_zdos} ZDOs")
        print("The explored map data might be stored differently.")
        print("Let me check the character save format instead...")
    
    return cart_found


def main():
    print("=" * 60)
    print("Valheim Cartography Table Extractor")
    print("=" * 60)
    
    result = parse_db(DB_PATH)
    
    if result:
        print("\nNext steps: extract the explored bitmap from the ZDO data")
    else:
        print("\nThe cartography table data may be stored in a different format.")
        print("Let me check the Zones section of the .db file...")
        
        # The explored map data might be in the Zones section
        # Or it might be stored per-player in .fch files
        print("\nAlternative approach: parse the .db Zones section for explored data")

if __name__ == '__main__':
    main()
