"""Extrae ItemSparse.db2 (WDC4, tabla sparse) usando las definiciones oficiales
de WoWDBDefs para el layout_hash del propio fichero.

Por qué hace falta el DBD: en una tabla sparse los locstring van INLINE y
terminados en \0, así que desplazan todo lo que va detrás. Los offsets de
field_storage_info asumen 4 bytes por string y NO valen; hay que recorrer los
campos en orden. El fichero no dice qué campos son strings: eso está en el DBD.
"""
import re
import struct
import sys


def parse_dbd(path, layout_hash):
    txt = open(path, encoding='utf-8').read()
    # Tipos por columna (COLUMNS ... hasta línea en blanco)
    cols = {}
    block = txt.split('COLUMNS', 1)[1].split('\n\n', 1)[0]
    for line in block.strip().split('\n'):
        parts = line.split()
        if len(parts) >= 2:
            cols[parts[1].split('<')[0].rstrip('?')] = parts[0].split('<')[0]

    # Bloque del layout pedido
    blocks = txt.split('\n\n')
    for b in blocks:
        if f'LAYOUT {layout_hash}' in b:
            fields = []
            for line in b.strip().split('\n'):
                if line.startswith(('LAYOUT', 'BUILD', 'COMMENT')):
                    continue
                m = re.match(r'^(\$[^$]*\$)?([A-Za-z_0-9]+)(<[^>]*>)?(\[(\d+)\])?', line)
                if not m:
                    continue
                ann, name, size, _, arr = m.groups()
                fields.append(dict(name=name, noninline='noninline' in (ann or ''),
                                   array=int(arr) if arr else 1,
                                   type=cols.get(name, 'int')))
            return fields
    raise SystemExit(f'layout {layout_hash} no encontrado')


class ItemSparse:
    def __init__(self, db2_path, dbd_path):
        d = self.d = open(db2_path, 'rb').read()
        hdr = struct.unpack_from('<IIIIIIIIIHHIIIIIII', d, 4)
        (self.record_count, self.field_count, self.record_size, _sts, _th, layout_hash,
         self.min_id, self.max_id, _loc, self.flags, self.id_index, _tfc, _bdo,
         _lcc, fsi_size, common_size, pallet_size, self.section_count) = hdr

        self.fields = [f for f in parse_dbd(dbd_path, '%08X' % layout_hash) if not f['noninline']]
        if len(self.fields) != self.field_count:
            print(f'aviso: DBD tiene {len(self.fields)} campos y el fichero {self.field_count}',
                  file=sys.stderr)

        o = 4 + struct.calcsize('<IIIIIIIIIHHIIIIIII')
        self.sections = []
        for _ in range(self.section_count):
            (_tact, fo, rc, sts, ore, ils, rds, omic, ctc) = struct.unpack_from('<QIIIIIIII', d, o)
            o += 40
            self.sections.append(dict(file_offset=fo, record_count=rc, string_table_size=sts,
                                      offset_records_end=ore, id_list_size=ils,
                                      relationship_data_size=rds, offset_map_id_count=omic,
                                      copy_table_count=ctc))
        o += self.field_count * 4  # field_structure: no hace falta
        self.storage = []
        for _ in range(self.field_count):
            (ob, sb, extra, t, c1, c2, c3) = struct.unpack_from('<HHIIIII', d, o); o += 24
            self.storage.append(dict(size_bits=sb, type=t))

        self.rows = []
        for s in self.sections:
            self._read(s)

    def _read(self, s):
        d = self.d
        # Orden en disco: datos variables, id_list, copy_table, offset_map
        p = s['offset_records_end']
        ids = list(struct.unpack_from(f'<{s["id_list_size"] // 4}I', d, p))
        p += s['id_list_size']
        p += s['copy_table_count'] * 8
        omap = [struct.unpack_from('<IH', d, p + i * 6) for i in range(s['offset_map_id_count'])]

        for i, (off, size) in enumerate(omap):
            if not size:
                continue
            self.rows.append((ids[i], self._record(d[off:off + size])))

    def _record(self, rec):
        out = {}
        pos = 0  # en bytes: los campos de una sparse van alineados a byte
        for fi, f in enumerate(self.fields):
            if f['type'] == 'locstring':
                end = rec.find(b'\x00', pos)
                out[f['name']] = rec[pos:end].decode('utf-8', 'replace')
                pos = end + 1
                continue
            # OJO: size_bits ya es el TOTAL del campo, arrays incluidos
            # (StatPercentageOfSocket[10] -> 320 bits), no el de un elemento.
            nbytes = self.storage[fi]['size_bits'] // 8
            if not nbytes:
                continue
            if f['array'] == 1:
                out[f['name']] = int.from_bytes(rec[pos:pos + nbytes], 'little')
            pos += nbytes
        out['__consumed'] = pos
        out['__len'] = len(rec)
        return out
