#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define IXA_GET_BITS(var, nbits) do {                                     \
  if (bits_left >= nbits) {                                               \
    (var) = bit_buffer >> (32 - nbits);                                   \
    bits_left -= nbits; bit_buffer <<= nbits;                             \
  }                                                                       \
  else {                                                                  \
    (var) = bit_buffer >> (32 - nbits);                                   \
    bit_buffer = fgetc(in)|(fgetc(in)<<8)|(fgetc(in)<<16)|(fgetc(in)<<24);\
    (var) |= bit_buffer >> (bits_left + 32 - nbits);                      \
    bit_buffer <<= (nbits - bits_left);                                   \
    bits_left += 32 - nbits;                                              \
  }                                                                       \
} while (0)

#define IXA_BYTE_OUT(byte) do {                                           \
  switch (rle_state) {                                                    \
  case 0: if (!--rle_len) rle_state = 1;                       break;     \
  case 1: if (byte > 127) rle_state = 2, rle_len = byte - 127;            \
          else            rle_state = 3, rle_len = byte + 1;   break;     \
  case 2: while (rle_len--) fputc(byte, out); rle_state = 1;   break;     \
  case 3: fputc(byte, out); if (!--rle_len) rle_state = 1;     break;     \
  }                                                                       \
} while (0)

void ixa_unpack(FILE *in, FILE *out) {
  unsigned char window[1024], *w = &window[1], *wend = &window[1024];
  unsigned char x, rle_state=0, rle_len=4, bits_left=0, *src;
  unsigned int pos, len, bit_buffer;

  window[0] = 0;
  while (1) {
    IXA_GET_BITS(x, 1);
    if (x) {
      IXA_GET_BITS(x, 8); IXA_BYTE_OUT(x);
      *w++ = x; if (w == wend) w = &window[0];
    }
    else {
      IXA_GET_BITS(pos, 10); src = &window[pos];
      if (pos == 0) break; /* end of stream */
      IXA_GET_BITS(len, 4); len += 2;
      while (len--) {
	*w++ = x = *src++; IXA_BYTE_OUT(x);
	if (src == wend) src = &window[0];
	if (w == wend) w = &window[0];
      }
    }
  }
}

/* IXA file format:
 *
 * all offsets/sizes/counts are in little-endian order
 *
 * iXalance file header:
 * 8 bytes:  "IXALANCE"
 * 32 bytes: name of demo (ASCII, may be null terminated)
 * 4 bytes:  offset of directory header
 * 4 bytes:  unknown ("type")
 *
 * iXalance directory header:
 * 4 bytes: number of directory entries
 * x bytes: the directory entries, 12 bytes each
 *
 * iXalance directory entry:
 * 4 bytes: offset of entry in file
 * 4 bytes: compressed size
 * 4 bytes: uncompressed size
 *
 * first directory entry (entry 0) is the demo script. All other entries
 * are LZSS/RLE compressed.
 *
 * iXalance script bytes:
 * 1: "push executable" (next byte = executable dir entry id)
 * 2: "pop part"        (remove current picture/code from execution stack)
 * 3: "play music"      (next byte = music dir entry id)
 * 4: "push picture"    (next byte = picture dir entry id)
 * 5: "wait music"      (next two bytes = position, line)
 */

#define EndGetI32(a) ((((a)[3])<<24)|(((a)[2])<<16)|(((a)[1])<<8)|((a)[0]))

struct ixafile {
  long offset;
  unsigned int comp, ucomp;
  unsigned char type;
};

void dump_ixa(char *name, FILE *fh) {
  unsigned char buf[48], dname[33], fname[40], c, *p;
  char *types[4] = {"unk", "exe", "xm", "raw"};
  unsigned int i, len;
  struct ixafile *f;
  FILE *out;

  /* read main header */
  if ((fread(buf,1,48,fh) < 48) || (memcmp(&buf[0],"IXALANCE",8) != 0)) return;
  printf("%s: type %u demo «%-32.32s»\n",name, EndGetI32(&buf[44]), &buf[8]);

  /* copy demo name, remove spaces */
  p=dname; for (i = 0; i < 32; i++) if (buf[8+i]!=' ') *p++=buf[8+i]; *p='\0';

  /* read dir header */
  if (fseek(fh, EndGetI32(&buf[40]), SEEK_SET) ||
      (fread(buf, 1, 4, fh) < 4) ||
      ((len = EndGetI32(&buf[0])) < 1) ||
      !(f = calloc(len, sizeof(struct ixafile)))) return;
  printf("%s: %d files in directory\n", name, len);
  
  /* read dir entries */
  for (i = 0; i < len; i++) {
    if (fread(buf, 1, 12, fh) < 12) return;
    f[i].offset = EndGetI32(&buf[0]);
    f[i].comp   = EndGetI32(&buf[4]);
    f[i].ucomp  = EndGetI32(&buf[8]);
    f[i].type   = 0;
  }

  /* dump script */
  printf("%s: DEMO SCRIPT\n", name);
  if (fseek(fh, f[0].offset, SEEK_SET)) return;
  for (i = 0; i < f[0].comp; i++) {
    switch (c = fgetc(fh)) {
    case 1: c = fgetc(fh); i++; f[c].type = 1;
            printf("%s:  PUSH EXE %d\n", name, c); break;
    case 2: printf("%s:  POP\n", name);            break;
    case 3: c = fgetc(fh); i++; f[c].type = 2;
            printf("%s:  PLAY MUS %d\n", name, c); break;
    case 4: c = fgetc(fh); i++; f[c].type = 3;
            printf("%s:  PUSH PIC %d\n", name, c); break;
    case 5: printf("%s:  WAIT MUS POS=%d ROW=%d\n", name,
		   fgetc(fh), fgetc(fh)); i += 2; break;
    default: printf("%s: *** BAD SCRIPT BYTE %d\n", name, c); break;
    }
  }

  /* dump parts */
  for (i = 1; i < len; i++) {
    if (f[i].ucomp == 0) continue;
    if (fseek(fh, f[i].offset, SEEK_SET)) return;
    sprintf(fname, "%s%03d.%s", dname, i, types[f[i].type]);
    printf("%s: unpacking %-39s (%u bytes)\n", name, fname, f[i].ucomp);
    if ((out = fopen(fname, "wb"))) {
      ixa_unpack(fh, out);
      fclose(out);
    }
  }
}

int main(int argc, char *argv[]) {
  if (argc < 2) {
    printf("Usage: %s <ixa file(s)>\n", argv[0]);
  }
  else {
    for (argv++; *argv; argv++) {
      FILE *fh;
      if ((fh = fopen(*argv, "rb"))) {
	dump_ixa(*argv, fh);
	fclose(fh);
      }
    }
  }
  return 0;
}  

