#!/usr/local/bin/python
#
# Module for reading and examining saved games.
#

import os
import struct
import StringIO
import binfile

# ----------------------------------------------------------------------
# Some constants and things
# ----------------------------------------------------------------------

TAG_MAJOR_VERSION = 5
TAG_MINOR_VERSION = 4

NUM_MONSTER_SPELL_SLOTS = 6
NUM_MONSTER_SLOTS = 10
MONS_PLAYER_GHOST = 400
MONS_PANDEMONIUM_DEMON = 401

class enum(object):
    def __init__(self, values):
        self.s2i = {}
        self.i2s = {}
        cur = 0
        for val in values.split():
            if '=' in val:
                val,cur = val.split('=')
                cur = int(cur)
            self.s2i[val] = cur
            self.i2s[cur] = val
            cur += 1
    def s(self, i): return self.i2s.get(i, str(i))

TAGS = """
    TAG_NO_TAG
    TAG_YOU
    TAG_YOU_ITEMS
    TAG_YOU_DUNGEON
    TAG_LEVEL
    TAG_LEVEL_ITEMS
    TAG_LEVEL_MONSTERS
    TAG_GHOST
    TAG_LEVEL_ATTITUDE
    TAG_LOST_MONSTERS
    TAG_LEVEL_TILES"""
TAGS_NAMES = dict( enumerate(TAGS.split()) )
TAGS_NUMS = dict( (b,a) for (a,b) in enumerate(TAGS.split()) )

tags_enum = enum(TAGS)

object_class_type = enum("""WEAPONS MISSILES ARMOUR WANDS FOOD UNKNOWN_I SCROLLS
    JEWELLERY POTIONS UNKNOWN_II BOOKS STAVES ORBS MISC CORPSE GOLD GEM
    UNASSIGNED=100 RANDOM=255""")
weapon_type = enum("""club mace flail dagger morningstar sling=13 bow crossbow handxbow
    blowgun=42 longbow=45""")
missile_type = enum("""MI_STONE MI_ARROW MI_BOLT MI_DART MI_NEEDLE MI_LARGE_ROCK
    MI_SLING_BULLET MI_JAVELIN MI_THROWING_NET""")
ammo_t = enum("THROW BOW SLING CROSSBOW HANDXBOW BLOWGUN")

# ----------------------------------------------------------------------
# unmarshall helpers
# ----------------------------------------------------------------------

def stream_container(f, reader):
    return [ reader(f) for x in xrange(f.stream1('I')) ]

def stream_array(f, count_type='B', item='B', limit=None):
    num = f.stream1(count_type)
    if limit is not None and num > limit:
        print "Warning: big array! (%d > %d)" % (num, limit)
    if len(item) == 1:
        return f.stream('%d%s' % (num, item))
    else:
        return [ f.stream(item) for x in xrange(num) ]

def stream_map(f, key_type, value_type):
    length = f.stream1('I')

    assert type(key_type)==str
    if len(key_type) == 1:
        def key_reader(): return f.stream1(key_type)
    else:
        def key_reader(): return f.stream(key_type)

    if type(value_type) == str:
        if len(value_type) == 1:
            def value_reader(): return f.stream1(value_type)
        else:
            def value_reader(): return f.stream(value_type)
    else:
        def value_reader(): return value_type()

    return dict( (key_reader(), value_reader())
                 for i in xrange(length) )

def assert_end(f):
    cur = f.tell()
    f.seek(0, os.SEEK_END)
    end = f.tell()
    if cur != end:
        print "  !! cur %d != end %d" % (cur,end)

# ----------------------------------------------------------------------
# Misc sub data structures
# ----------------------------------------------------------------------

def CrawlValue(f):
    type_, flags = f.stream('BB')
    if type_ == 1: return bool(f.stream1('B')) # BOOL
    elif type_ == 2: return f.stream1('B')     # BYTE
    elif type_ == 3: return f.stream1('H')     # SHORT
    elif type_ == 4: return f.stream1('I')     # LONG
    elif type_ == 5: return f.stream1('f')     # FLOAT
    elif type_ == 6: return f.streamString2('f') # STRING
    elif type_ == 7: return f.stream('HH')     # COORD
    elif type_ == 8: return Item(f)            # ITEM
    elif type_ == 9: return CrawlHashTable(f)  # HASH
    elif type_ == 10: return CrawlVector(f)    # VEC


class CrawlHashTable(object):
    def __init__(self, f):
        self.size = f.stream1('B')
        if self.size == 0: return
        self.typeflags = f.stream('BB')
        contents = [ (f.streamString2(), CrawlValue(f)) for x in xrange(self.size) ]


class CrawlVector(object):
    def __init__(self, f):
        self.size = f.stream1('B')
        if self.size == 0: return
        self.max, self.type, self.flags = f.stream('BBB')
        contents = [ CrawlValue(f) for x in xrange(self.size) ]

class Item(object): 
    def __init__(self, f):
        self.base_type, self.sub_type = f.stream('BB')
        self.plusses = f.stream('HH')
        self.special, self.quantity = f.stream('IH')
        self.data2 = f.stream('BHHI')
        self.unused = f.stream('HH')
        self.slot = f.stream1('B')
        self.origs = f.stream('HH')
        self.inscrip = f.streamString2()
        self.props = CrawlHashTable(f)
    def __str__(self):
        ret = "%s" % object_class_type.s(self.base_type)
        if ret == 'WEAPONS':
            ret += '/%s' % weapon_type.s(self.sub_type)
        elif ret == 'MISSILES':
            ret += '/%s' % missile_type.s(self.sub_type)
        else:
            ret += '/%d' % self.sub_type
        return ret


KC_NCATEGORIES = 3
class PlaceInfo(object):
    def __init__(self, f):
        self.data1 = f.stream('IIII')
        self.mon_kill_exp = f.stream('II')
        self.mon_kill_num = f.stream('%dI' % KC_NCATEGORIES)
        self.data2 = f.stream('6I')
        self.data3 = f.stream('6f')


def mon_spells(f): return f.stream('%dH' % NUM_MONSTER_SPELL_SLOTS)
def mon_resist(f): return f.stream('12B')

class Ghost(object):
    def __init__(self, f):
        self.name = f.streamString2()
        self.data1 = f.stream('10HBH')
        self.resist = mon_resist(f)
        self.data2 = f.stream('BBH')
        self.spells = mon_spells(f)


class Monster(object):
    def __init__(self, f):
        def mon_enchant(f): return f.stream('5H')
        self.data1 = f.stream('6B')
        self.pos = f.stream('BB')
        self.targ = f.stream('BB')
        self.data2 = f.stream('II')
        self.ench = [ mon_enchant(f) for x in xrange(f.stream1('H')) ]
        self.ench_count = f.stream1('B')
        self.type = f.stream1('H')
        self.data3 = f.stream('HHHH')
        self.inv = f.stream('%dH' % NUM_MONSTER_SLOTS)
        self.spells = spells(f)
        self.god = f.stream1('B')
        if self.type in (MONS_PLAYER_GHOST, MONS_PANDEMONIUM_DEMON):
            self.ghost = Ghost(f)


class Quiver(object):
    def __init__(self, f):
        self.cooky = f.stream1('H')
        self.last_weapon = Item(f)
        self.last_used_type = f.stream1('I')
        num_last_used = f.stream1('I')
        self.last_used_of_type = [ Item(f) for i in range(num_last_used) ]

    def dump(self):
        print 'last weapon:', self.last_weapon
        for i,item in enumerate(self.last_used_of_type):
            print ' %s: %d %s' % (ammo_t.s(i), item.quantity, item)


def Coord(f): return f.stream('HH')

class MapMarker(object):
    def __init__(self, f, minor):
        if minor >= 4:
            expected_size = f.stream1('I')

        num_read = -f.file.tell()
        self.mark_type = mark_type = f.stream1('H')
        if mark_type == 0:      # map_feature_marker
            self.read_base(f)
            self.feat = f.stream1('H')
        elif mark_type == 1:    # map_lua_marker
            self.read_base(f)
            self.initialized = f.stream1('B')
            if self.initialized:
                chunk = f.streamString2()
                assert False, "Don't know how much else to read"
        elif mark_type == 2:    # map_corruption_marker
            self.read_base(f)
            self.duration, self.radius = f.stream('HH')
        elif mark_type == 3:    # map_wiz_props_marker
            self.read_base(f)
            self.props = [ (f.streamString2(), f.streamString2())
                           for x in xrange(f.stream1('H')) ]
        else:
            assert "Unknown map marker type %d" % mark_type

        num_read += f.file.tell()

        if minor >= 4:
            assert num_read == expected_size

    def read_base(self, f):
        self.coord = Coord(f)

# ----------------------------------------------------------------------
# Generic tagged file
# ----------------------------------------------------------------------

class TaggedFile(object):
    def __init__(self, fn):
        f = binfile.reader(fn)
        f.byteorder = '>'
        self.f = f
        self.major, self.minor = f.stream('bb')
        print '  version %d.%d' % (self.major, self.minor)
        if (self.major != TAG_MAJOR_VERSION or
            self.minor > TAG_MINOR_VERSION):
            print "  WARNING: Cannot handle this version!"
        self.tags = dict( self._gen_tags() )

    def _gen_tags(self):
        while True:
            try:
                tag_id, size = self.f.stream('HI')
            except struct.error:
                return
            data = self.f.file.read(size)
            tag_name = TAGS_NAMES[tag_id]
            try:  constructor = TAG_TO_CLASS[tag_name]
            except KeyError:
                print "  Skipping %s" % tag_name
            else:
                print "  Parsing %s" % tag_name
                sub_reader = binfile.reader(StringIO.StringIO(data))
                sub_reader.byteorder = '>'
                data = constructor(sub_reader, self.minor)
            yield (tag_id, data)


class TagBase(object): pass


class TagLEVEL_MONSTERS(TagBase):
    def __init__(self, f, minor):
        self.mons_alloc = stream_array(f, 'B', 'H')
    

def TagGHOST(f, minor):
    return [ Ghost(f) for x in xrange(f.stream1('H')) ]


class TagLOST_MONSTERS(TagBase):
    def __init__(self, f, minor):
        def follower():
            return (Monster(f), [Item(f) for x in xrange(NUM_MONSTER_SLOTS)])
        def follower_list():
            return [follower() for x in xrange(f.stream1('H'))]
        def item_list():
            return [Item(f) for x in xrange(f.stream1('H'))]
        # level_id -> follower_list
        self.lost_monst = stream_map(f, 'BIB', follower_list)
        self.lost_item = stream_map(f, 'BIB', item_list)
        assert_end(f.file)


class TagYOU_DUNGEON(TagBase):
    def __init__(self, f, minor):
        self.ucreatures = stream_array(f, 'H', 'B')
        self.c_things = stream_array(f, 'B', 'II')
        self.s_things = stream_array(f, 'H', '%dB' % len(self.c_things))
        # branch_type -> level_id(branchtype, depth, leveltype)
        self.stair_level = stream_map(f, 'I', 'BIB')
        # level_pos -> shop_type
        self.shops_present = stream_map(f, 'IIBIB', 'I')
        # level_pos -> god_type
        self.altars_present = stream_map(f, 'IIBIB', 'I')
        # level_pos -> portal_type
        self.portals_present = stream_map(f, 'IIBIB', 'I')
        # level_id -> string
        self.level_annotations = stream_map(f, 'BIB', f.streamString2)

        self.place_info = PlaceInfo(f)
        self.more_place_info = [PlaceInfo(f) for x in xrange(f.stream1('H'))]
        
        self.uniq_map_tags  = stream_container(f, f.__class__.streamString2)
        self.uniq_map_names = stream_container(f, f.__class__.streamString2)

        assert_end(f.file)


class TagYOU_ITEMS(TagBase):
    def __init__(self, f, minor):
        ninv = f.stream1('B')
        self.inv = [Item(f) for x in range(ninv)]
        ntype, nsubtype = f.stream('BB')
        self.item_desc =  [ f.stream('%dB' % nsubtype) for x in range(ntype) ]
        ident_w, ident_h = f.stream('BB')
        self.identified = [ f.stream('%dB' % ident_h) for x in range(ident_w) ]
        self.uniques = stream_array(f)
        self.books = stream_array(f)
        self.unrandart = stream_array(f, 'H', 'B')
        assert_end(f.file)


class TagYOU(TagBase):
    def __init__(self, f, minor):
        self.name = f.streamString2()
        self.data1 = f.stream('BBBBBH')  # last is pet_target
        self.data2 = f.stream('8B') # last is level_type
        self.level_type_name = f.streamString2()
        self.data3 = f.stream('5B') # species
        self.hp, self.hunger = f.stream('HH')
        self.equip = stream_array(f)
        self.magic = f.stream('BB')
        self.stats = f.stream('BBB')
        self.regen = f.stream('BBH')
        self.xp, self.gold = f.stream('II')
        self.data4 = f.stream('BBI')
        self.max_stats = f.stream('BBB')
        self.hp_magic2 = f.stream('HHHH')
        self.pos = f.stream('HH')
        self.class_name = f.streamString2()
        self.burden = f.stream1('H')
        self.spells = stream_array(f)
        self.spell_letters = stream_array(f)
        self.abil_letters = stream_array(f, 'B', 'H')
        self.skills = stream_array(f, 'B', 'BBIB')
        self.durations = stream_array(f, 'B', 'I')
        self.attributes = stream_array(f)
        self.quiver_old = stream_array(f)
        self.sacrifice = stream_array(f, 'B', 'I')
        self.mutation = stream_array(f, 'H', 'BB')
        self.penance = stream_array(f)
        self.worshipped = stream_array(f)
        self.gifts = f.stream('%dH' % len(self.worshipped))
        self.data5 = f.stream('4B')
        self.elapsed_time = f.stream1('f')
        self.wizard = f.stream1('B')
        self.game_start = f.streamString2()
        self.real_time, self.num_turns = f.stream('II')
        self.data6 = f.stream('HHB')
        self.beheldby = f.stream('%dB' % f.stream1('B'))
        if minor >= 2:
            self.piety_hysteresis = f.stream1('B')
        if minor >= 3:
            self.quiver = Quiver(f)

        cur = f.file.tell()
        f.file.seek(0, os.SEEK_END)
        cur -= f.file.tell()
        assert cur == 0, "%d bytes off" % cur


class TagLEVEL_ATTITUDE(TagBase):
    def __init__(self, f, minor):
        self.monsters = stream_array(f, 'H', 'BH')
        assert_end(f.file)

        
class TagLEVEL_ITEMS(TagBase):
    def __init__(self, f, minor):
        self.traps = stream_array(f, 'H', 'BBB')
        num_items = f.stream1('H')
        self.items = [Item(f) for x in range(num_items)]
        assert_end(f.file)


def gen_run_length_decode(f, value_type, expected):
    while expected > 0:
        run = f.stream1('B')
        value = f.stream1(value_type)
        expected -= run
        assert expected >= 0
        for i in xrange(run):
            yield value

class TagLEVEL(TagBase):
    def __init__(self, f, minor):
        self.colours = f.stream('BB')
        self.level_flags = f.stream('I')
        self.time = f.stream('f')
        self.gx,self.gy = f.stream('HH')
        self.turns = f.stream('I')
        self.grid = [ f.stream('BHHHHH')
                      for i in xrange(self.gx)
                      for j in xrange(self.gy) ]
        expected = self.gx * self.gy
        self.grid_colours = list(gen_run_length_decode(f, 'B', expected))

        self.cloud_no = f.stream1('H')
        self.clouds = stream_array(f, 'H', 'BBBHBH', limit=1000)
        self.shops = stream_array(f, 'B', 'BBBBBBBB', limit=15)
        self.sanctuary = Coord(f)
        self.sanctuary_time = f.stream1('B')
        if minor >= 4:
            cooky = f.stream1('I')
            assert cooky == 0x17742C32
        self.markers = [ MapMarker(f, minor) for x in xrange(f.stream1('H')) ]
        assert_end(f.file)

        
TAG_TO_CLASS = {
    'TAG_YOU' : TagYOU,
    'TAG_YOU_ITEMS': TagYOU_ITEMS,
    'TAG_YOU_DUNGEON': TagYOU_DUNGEON,
    'TAG_LEVEL': TagLEVEL,
    'TAG_LEVEL_ITEMS': TagLEVEL_ITEMS,
    'TAG_LEVEL_MONSTERS': TagLEVEL_MONSTERS,
    'TAG_GHOST': TagGHOST,
    'TAG_LEVEL_ATTITUDE': TagLEVEL_ATTITUDE,
    'TAG_LOST_MONSTERS': TagLOST_MONSTERS,
    # TAG_LEVEL_TILES
}


if __name__ == '__main__':
    testfile = '/Users/pld/src/crawl-ref/play/saves/Hunter-501.sav'
    x = TaggedFile(testfile)
    q = x.tags[TAGS_NUMS['TAG_YOU']].quiver
    q.dump()
