/*
 *  SF3KtoProT - Converts Star Fighter 3000 music to Amiga ProTracker format
 *  Fednet file decompression
 *  Copyright (C) 2009  Chris Bazley
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public Licence as published by
 *  the Free Software Foundation; either version 2 of the Licence, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public Licence for more details.
 *
 *  You should have received a copy of the GNU General Public Licence
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/* ISO library header files */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <inttypes.h>

/* Local header files */
#include "decomp.h"
#include "platform.h"

static unsigned long r6;

static bool loadbits(FILE *f, unsigned long en, unsigned long *out)
{
  /* Returns false to indicate read error or end of file */
  unsigned long result = 0, addshift, maskshift = r6;

  assert(f != NULL);

  for (addshift = 0; addshift < en; addshift++) {
    unsigned long msb;

    if ((maskshift & 0xff) == 8) {
      int nextbyte = fgetc(f);
      if (nextbyte == EOF)
        return false; /* read error or end of file */

      maskshift = (long)nextbyte << 8;
    }
    msb = maskshift >> 8;
    maskshift &= 0xff;
    maskshift += msb << 8;
    {
      long int onebit = msb & (1l << maskshift);
      if (onebit > 0)
        onebit = 1;
      result += onebit << addshift;
    }
    maskshift++;
  }
  *out = result;
  r6 = maskshift;
  return true; /* success */
}

void *load_compressed(bool verbose, const char *file_path)
{
  char *buf;
  char len_buf[4];
  unsigned long type;
  long int len, dptr = 0;
  FILE *f;

  assert(file_path != NULL);

  r6 = 8;

  /* Open file */
  if (verbose)
    printf("Opening compressed input file '%s'\n", file_path);

  f = fopen(file_path, "rb"); /* open for reading */
  if (f == NULL) {
    fprintf(stderr,"Failed to open input file '%s'\n", file_path);
    return NULL;
  }

  /* Get size of decompressed data */
  if (fread(len_buf, sizeof(len_buf), 1, f) != 1) {
    fclose(f);
    fprintf(stderr, "Failed to read %u bytes from file '%s'\n",
                    sizeof(len_buf), file_path);
    return NULL;
  }
  len = read_int32le(len_buf);
  if (verbose)
    printf("Size of decompressed data will be %ld bytes\n", len);

  if (len < 0) {
    fprintf(stderr, "Bad size %ld in compressed file '%s'\n", len, file_path);
    return NULL;
  }

  /* Allocate buffer for data */
  buf = malloc((size_t)len);
  if (buf == NULL) {
    fclose(f);
    fprintf(stderr, "Failed to allocate %ld bytes to decompress file "
                    "'%s'\n", len, file_path);
    return NULL;
  }

  while (loadbits(f, 1, &type)) {
    if (type != 1) {
      unsigned long result;
      if (!loadbits(f, 8, &result))
        break; /* EOF or error */
      if (dptr < len)
        buf[dptr] = (char)result;
      dptr++;
    } else {
      unsigned long chunk_len, read_pos, chunk_offset;
      long int read_offset;

      if (!loadbits(f, 9, &read_pos))
        break; /* EOF or error */
      if (read_pos >= 256) {
        if (!loadbits(f, 8, &chunk_len))
          break; /* EOF or error */
      } else {
        if (!loadbits(f, 9, &chunk_len))
          break; /* EOF or error */
      }

      read_offset = (dptr - 512) + read_pos;
      chunk_offset = 0;
      do {
        char output_byte;
        if (read_offset < 0)
          output_byte = 0;
        else
          output_byte = buf[read_offset];

        if ((dptr + chunk_offset) >= len) {
          fclose(f);
          fprintf(stderr, "Error in compressed bitstream of file '%s'\n",
                  file_path);
          free(buf);
          return NULL;
        }
        buf[dptr + chunk_offset] = output_byte;

        read_offset++;
        chunk_offset++;
      } while (chunk_offset < chunk_len);
      dptr += chunk_len;
    }
  }

  if (ferror(f)) {
    /* File error on fgetc() */
    fprintf(stderr, "Error reading data from file '%s'\n", file_path);
    free(buf);
    buf = NULL;
  }

  if (verbose)
    puts("Closing input file");
  fclose(f);

  return buf;
}
