#!/usr/bin/env python

from collections import OrderedDict
import errno
import os
from struct import Struct
import sys

HDR = Struct('<4sII')
ENTRY = Struct('<8sIII')

class SXXException(Exception):
    pass

class SXX(object):
    def __init__(self):
        self.entries = OrderedDict()
    def add(self, name, raw):
        if name in self.entries:
            raise SXXException('duplicate entry %r' % name)
        self.entries[name] = raw
    def add_from_path(self, name, path):
        with open(path, 'rb') as f:
            raw = f.read()
        self.add(name, raw)
    def replace(self, name, raw):
        if name not in self.entries:
            raise SXXException('no entry %r' % name)
        self.entries[name] = raw
    def replace_from_path(self, name, path):
        with open(path, 'rb') as f:
            raw = f.read()
        self.replace(name, raw)
    def remove(self, name):
        if name not in self.entries:
            raise SXXException('no entry %r' % name)
        del self.entries[name]
    def write(self, f):
        count = len(self.entries)
        f.write(HDR.pack('DSND', 1, count))
        offsets = {}
        offset = 0
        for name, raw in self.entries.iteritems():
            offsets[name] = offset
            offset += len(raw)
        for name, raw in self.entries.iteritems():
            size = len(raw)
            f.write(ENTRY.pack(name, size, size, offsets[name]))
        for raw in self.entries.itervalues():
            f.write(raw)
    def write_to_path(self, path):
        with open(path, 'wb') as f:
            self.write(f)
    def extract(self, directory):
        try:
            os.mkdir(directory)
        except OSError as e:
            if e.errno != errno.EEXIST or not os.path.isdir(directory):
                raise
        for name, raw in self.entries.iteritems():
            path = os.path.join(directory, '%s.raw' % name)
            with open(path, 'wb') as f:
                f.write(raw)
            sys.stderr.write('%s\n' % path)

def read(buf):
    magic, version, count = HDR.unpack_from(buf)
    if magic != 'DSND':
        raise SXXException('File not a Descent sound file')
    if version != 1:
        raise SXXException('Unknown file version')
    sxx = SXX()
    body_offset = HDR.size + ENTRY.size * count
    for i in xrange(count):
        hdr_offset = HDR.size + ENTRY.size * i
        name, size, size2, offset = ENTRY.unpack_from(buf, hdr_offset)
        name = name.split('\x00')[0]
        if size != size2:
            # i don't know why this redundancy exists, so let's just fail
            raise SXXException('%s: sizes do not match' % name)
        raw = buf[body_offset + offset:body_offset + offset + size]
        sxx.add(name, raw)
    return sxx

def read_from_path(path):
    with open(path, 'rb') as f:
        buf = f.read()
    return read(buf)

def example_usage():
    s11 = read_from_path('descent2.s11')
    s11.extract('descent2.s11_FILES')
    s11.add_from_path('test', 'descent2.s11_FILES/exlife.raw')
    s11.write_to_path('new.s11')
    s22 = read_from_path('descent2.s22')
    s22.extract('descent2.s22_FILES')
    s22.add_from_path('test', 'descent2.s22_FILES/exlife.raw')
    s22.write_to_path('new.s22')

if __name__ == '__main__':
    example_usage()
