/* szx.c: Routines for .szx snapshots
   Copyright (c) 1998,2003 Philip Kendall
   Copyright (c) 2004 Fredrick Meunier

   $Id: szx.c,v 1.43 2007/02/02 16:35:43 pak21 Exp $

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, 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 License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

   Author contact information:

   E-mail: philip-fuse@shadowmagic.org.uk

*/

#include <config.h>

#include <string.h>

#include "internals.h"

/* I've had to make some assumptions about the 'right' way to do some
   things in the .szx format due to the documentation
   <http://www.spectaculator.com/docs/zx-state/intro.shtml> not being
   up to scratch:

   * http://www.spectaculator.com/docs/zx-state/header.shtml says
     "chMinorVersion
      Minor version number of the file format. Currently 1."

     despite the current version of the format being 1.2. libspectrum
     writes .szx files with a minor version number of 3 as they contain
     blocks not in the v1.2 specification.

   * The ZXSTSPECREGS block says that the ch1ffd member should be set
     to zero for machines other than the +2A/+3. libspectrum makes
     this field non-zero for Scorpion emulation.

   * In a ZXSTRAMPAGE block, the Timex machines should save the same
     pages as the 48K machine, and RAM pages 8-15 are valid for
     Scorpion emulation.

   Places where these points are used in the code are marked with
   [Assumption].

   I've mentioned these points to Jonathan Needle ('maintainer' of the
   .szx format), but he hasn't replied :-(

*/

/* The machine numbers used in the .szx format */

typedef enum szx_machine_type {

  SZX_MACHINE_16 = 0,
  SZX_MACHINE_48,
  SZX_MACHINE_128,
  SZX_MACHINE_PLUS2,
  SZX_MACHINE_PLUS2A,
  SZX_MACHINE_PLUS3,
  SZX_MACHINE_PLUS3E,
  SZX_MACHINE_PENTAGON,
  SZX_MACHINE_TC2048,
  SZX_MACHINE_TC2068,
  SZX_MACHINE_SCORPION,
  SZX_MACHINE_SE,
  SZX_MACHINE_TS2068,

} szx_machine_type;

static const char *signature = "ZXST";
static const size_t signature_length = 4;

static const libspectrum_byte SZX_VERSION_MAJOR = 1;
static const libspectrum_byte SZX_VERSION_MINOR = 3;

/* Constants etc for each chunk type */

#define ZXSTBID_CREATOR "CRTR"

#define ZXSTBID_Z80REGS "Z80R"
static const libspectrum_byte ZXSTZF_EILAST = 1;
static const libspectrum_byte ZXSTZF_HALTED = 2;

#define ZXSTBID_SPECREGS "SPCR"

#define ZXSTBID_RAMPAGE "RAMP"
static const libspectrum_word ZXSTRF_COMPRESSED = 1;

#define ZXSTBID_AY "AY\0\0"
#define ZXSTBID_MULTIFACE "MFCE"
#define ZXSTBID_USPEECH "USPE"
#define ZXSTBID_SPECDRUM "DRUM"
#define ZXSTBID_ZXTAPE "TAPE"

#define ZXSTBID_KEYBOARD "KEYB"
static const libspectrum_dword ZXSTKF_ISSUE2 = 1;

#define ZXSTBID_JOYSTICK "JOY\0"
static const libspectrum_dword ZXSTJOYF_ALWAYSPORT31 = 1;

typedef enum szx_joystick_type {

  ZXJT_KEMPSTON = 0,
  ZXJT_FULLER,
  ZXJT_CURSOR,
  ZXJT_SINCLAIR1,
  ZXJT_SINCLAIR2,
  ZXJT_SPECTRUMPLUS,
  ZXJT_TIMEX1,
  ZXJT_TIMEX2,
  ZXJT_NONE,

} szx_joystick_type;

#define ZXSTBID_IF2ROM "IF2R"
#define ZXSTBID_MOUSE "AMXM"
#define ZXSTBID_ROM "ROM\0"
#define ZXSTBID_ZXPRINTER "ZXPR"
#define ZXSTBID_IF1 "IF1\0"
#define ZXSTBID_MICRODRIVE "MDRV"
#define ZXSTBID_PLUS3DISK "+3\0\0"
#define ZXSTBID_DSKFILE "DSK\0"
#define ZXSTBID_TIMEXREGS "SCLD"

#define ZXSTBID_BETA128 "B128"
static const libspectrum_dword ZXSTBETAF_CONNECTED = 1;
static const libspectrum_dword ZXSTBETAF_PAGED = 4;
static const libspectrum_dword ZXSTBETAF_SEEKLOWER = 16;

#define ZXSTBID_BETADISK "BDSK"
#define ZXSTBID_GS "GS\0\0"
#define ZXSTBID_GSRAMPAGE "GSRP"
#define ZXSTBID_COVOX "COVX"

#define ZXSTBID_DOCK "DOCK"
static const libspectrum_word ZXSTDOCKF_RAM = 2;
static const libspectrum_word ZXSTDOCKF_EXROMDOCK = 4;

#define ZXSTBID_ZXATASP "ZXAT"
static const libspectrum_word ZXSTZXATF_UPLOAD = 1;
static const libspectrum_word ZXSTZXATF_WRITEPROTECT = 2;

#define ZXSTBID_ZXATASPRAMPAGE "ATRP"

#define ZXSTBID_ZXCF "ZXCF"
static const libspectrum_word ZXSTZXCFF_UPLOAD = 1;

#define ZXSTBID_ZXCFRAMPAGE "CFRP"

static libspectrum_error
read_chunk( libspectrum_snap *snap, libspectrum_word version,
	    const libspectrum_byte **buffer, const libspectrum_byte *end );

typedef libspectrum_error (*read_chunk_fn)( libspectrum_snap *snap,
					    libspectrum_word version,
					    const libspectrum_byte **buffer,
					    const libspectrum_byte *end,
					    size_t data_length );

static libspectrum_error
write_file_header( libspectrum_byte **buffer, libspectrum_byte **ptr,
	      size_t *length, int *out_flags, libspectrum_snap *snap );

static libspectrum_error
write_crtr_chunk( libspectrum_byte **bufer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_creator *creator );
static libspectrum_error
write_z80r_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap );
static libspectrum_error
write_spcr_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap );
static libspectrum_error
write_joy_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, int *out_flags, libspectrum_snap *snap );
static libspectrum_error
write_keyb_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, int *out_flags, libspectrum_snap *snap );
static libspectrum_error
write_ram_pages( libspectrum_byte **buffer, libspectrum_byte **ptr,
		 size_t *length, libspectrum_snap *snap, int compress );
static libspectrum_error
write_ramp_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap, int page,
		  int compress );
static libspectrum_error
write_ram_page( libspectrum_byte **buffer, libspectrum_byte **ptr,
		size_t *length, const char *id, const libspectrum_byte *data,
		size_t data_length, int page, int compress, int extra_flags );
static libspectrum_error
write_ay_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		size_t *length, libspectrum_snap *snap );
static libspectrum_error
write_scld_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		size_t *length, libspectrum_snap *snap );
static libspectrum_error
write_b128_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap );
static libspectrum_error
write_zxat_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap );
static libspectrum_error
write_atrp_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap, int page,
		  int compress );
static libspectrum_error
write_zxcf_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap );
static libspectrum_error
write_cfrp_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap, int page,
		  int compress );

#ifdef HAVE_ZLIB_H

static libspectrum_error
write_if2r_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		size_t *length, libspectrum_snap *snap );

#endif				/* #ifdef HAVE_ZLIB_H */

static libspectrum_error
write_dock_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap, int exrom_dock,
                  const libspectrum_byte *data, int page, int writeable,
                  int compress );

static libspectrum_error
write_chunk_header( libspectrum_byte **buffer, libspectrum_byte **ptr,
		    size_t *length, const char *id,
		    libspectrum_dword block_length );

static libspectrum_error
read_ram_page( libspectrum_byte **data, size_t *page,
	       const libspectrum_byte **buffer, size_t data_length,
	       size_t uncompressed_length, libspectrum_word *flags )
{
#ifdef HAVE_ZLIB_H

  libspectrum_error error;

#endif			/* #ifdef HAVE_ZLIB_H */

  if( data_length < 3 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "%s:read_ram_page: length %lu too short",
			     __FILE__, (unsigned long)data_length );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  *flags = libspectrum_read_word( buffer );

  *page = **buffer; (*buffer)++;

  if( *flags & ZXSTRF_COMPRESSED ) {

#ifdef HAVE_ZLIB_H

    error = libspectrum_zlib_inflate( *buffer, data_length - 3, data,
				      &uncompressed_length );
    if( error ) return error;

    *buffer += data_length - 3;

#else			/* #ifdef HAVE_ZLIB_H */

    libspectrum_print_error(
      LIBSPECTRUM_ERROR_UNKNOWN,
      "%s:read_ram_page: zlib needed for decompression\n",
      __FILE__
    );
    return LIBSPECTRUM_ERROR_UNKNOWN;

#endif			/* #ifdef HAVE_ZLIB_H */

  } else {

    if( data_length < 3 + uncompressed_length ) {
      libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			       "%s:read_ram_page: length %lu too short",
			       __FILE__, (unsigned long)data_length );
      return LIBSPECTRUM_ERROR_UNKNOWN;
    }

    *data = malloc( uncompressed_length * sizeof( libspectrum_byte ) );
    if( !( *data ) ) {
      libspectrum_print_error( LIBSPECTRUM_ERROR_MEMORY,
			       "%s:read_ram_page: out of memory at %d",
			       __FILE__, __LINE__ );
      return LIBSPECTRUM_ERROR_MEMORY;
    }

    memcpy( *data, *buffer, uncompressed_length );
    *buffer += uncompressed_length;

  }

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_atrp_chunk( libspectrum_snap *snap, libspectrum_word version GCC_UNUSED,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  libspectrum_byte *data;
  size_t page;
  libspectrum_error error;
  libspectrum_word flags;

  error = read_ram_page( &data, &page, buffer, data_length, 0x4000, &flags );
  if( error ) return error;

  if( page >= SNAPSHOT_ZXATASP_PAGES ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT,
			     "%s:read_atrp_chunk: unknown page number %lu",
			     __FILE__, (unsigned long)page );
    free( data );
    return LIBSPECTRUM_ERROR_CORRUPT;
  }

  libspectrum_snap_set_zxatasp_ram( snap, page, data );

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_ay_chunk( libspectrum_snap *snap, libspectrum_word version GCC_UNUSED,
	       const libspectrum_byte **buffer,
	       const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  size_t i;

  if( data_length != 18 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "szx_read_ay_chunk: unknown length %lu",
			     (unsigned long)data_length );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  (*buffer)++;			/* Skip the flags */

  libspectrum_snap_set_out_ay_registerport( snap, **buffer ); (*buffer)++;

  for( i = 0; i < 16; i++ ) {
    libspectrum_snap_set_ay_registers( snap, i, **buffer ); (*buffer)++;
  }

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_b128_chunk( libspectrum_snap *snap, libspectrum_word version GCC_UNUSED,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  libspectrum_dword flags;

  if( data_length < 10 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "szx_read_b128_chunk: length %lu too short",
			     (unsigned long)data_length );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  flags = libspectrum_read_dword( buffer );
  libspectrum_snap_set_beta_paged( snap, flags & ZXSTBETAF_PAGED );
  libspectrum_snap_set_beta_direction( snap,
				       !( flags & ZXSTBETAF_SEEKLOWER ) );

  (*buffer)++;		/* Skip the number of drives */
  libspectrum_snap_set_beta_system( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_beta_track ( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_beta_sector( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_beta_data  ( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_beta_status( snap, **buffer ); (*buffer)++;

  /* Skip any extra data (most likely a custom ROM) */
  *buffer += data_length - 10;

  return LIBSPECTRUM_ERROR_NONE;
}
     
static libspectrum_error
read_cfrp_chunk( libspectrum_snap *snap, libspectrum_word version GCC_UNUSED,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  libspectrum_byte *data;
  size_t page;
  libspectrum_error error;
  libspectrum_word flags;

  error = read_ram_page( &data, &page, buffer, data_length, 0x4000, &flags );
  if( error ) return error;

  if( page >= SNAPSHOT_ZXCF_PAGES ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT,
			     "%s:read_cfrp_chunk: unknown page number %lu",
			     __FILE__, (unsigned long)page );
    free( data );
    return LIBSPECTRUM_ERROR_CORRUPT;
  }

  libspectrum_snap_set_zxcf_ram( snap, page, data );

  return LIBSPECTRUM_ERROR_NONE;
}

static void
add_joystick( libspectrum_snap *snap, libspectrum_joystick type, int inputs )
{
  size_t i;
  size_t num_joysticks = libspectrum_snap_joystick_active_count( snap );

  for( i = 0; i < num_joysticks; i++ ) {
    if( libspectrum_snap_joystick_list( snap, i ) == type ) {
      libspectrum_snap_set_joystick_inputs( snap, i, inputs |
                                  libspectrum_snap_joystick_inputs( snap, i ) );
      return;
    }
  }

  libspectrum_snap_set_joystick_list( snap, num_joysticks, type );
  libspectrum_snap_set_joystick_inputs( snap, num_joysticks, inputs );
  libspectrum_snap_set_joystick_active_count( snap, num_joysticks + 1 );
}

static libspectrum_error
read_joy_chunk( libspectrum_snap *snap, libspectrum_word version,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  libspectrum_dword flags;

  if( data_length != 6 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "%s:read_joy_chunk: unknown length %lu",
			     __FILE__, (unsigned long)data_length );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  flags = libspectrum_read_dword( buffer );
  if( flags & ZXSTJOYF_ALWAYSPORT31 ) {
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_KEMPSTON,
                  LIBSPECTRUM_JOYSTICK_INPUT_NONE );
  }

  switch( **buffer ) {
  case ZXJT_KEMPSTON:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_KEMPSTON,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_1 );
    break;
  case ZXJT_FULLER:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_FULLER,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_1 );
    break;
  case ZXJT_CURSOR:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_CURSOR,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_1 );
    break;
  case ZXJT_SINCLAIR1:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_SINCLAIR_1,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_1 );
    break;
  case ZXJT_SINCLAIR2:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_SINCLAIR_2,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_1 );
    break;
  case ZXJT_TIMEX1:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_TIMEX_1,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_1 );
    break;
  case ZXJT_TIMEX2:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_TIMEX_2,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_1 );
    break;
  case ZXJT_NONE:
    break;
  }
  (*buffer)++;

  switch( **buffer ) {
  case ZXJT_KEMPSTON:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_KEMPSTON,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_2 );
    break;
  case ZXJT_FULLER:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_FULLER,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_2 );
    break;
  case ZXJT_CURSOR:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_CURSOR,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_2 );
    break;
  case ZXJT_SINCLAIR1:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_SINCLAIR_1,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_2 );
    break;
  case ZXJT_SINCLAIR2:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_SINCLAIR_2,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_2 );
    break;
  case ZXJT_TIMEX1:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_TIMEX_1,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_2 );
    break;
  case ZXJT_TIMEX2:
    add_joystick( snap, LIBSPECTRUM_JOYSTICK_TIMEX_2,
                  LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_2 );
    break;
  case ZXJT_NONE:
    break;
  }
  (*buffer)++;

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_keyb_chunk( libspectrum_snap *snap, libspectrum_word version,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  size_t expected_length;
  libspectrum_dword flags;

  expected_length = version >= 0x0101 ? 5 : 4;

  if( data_length != expected_length ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "%s:read_keyb_chunk: unknown length %lu",
			     __FILE__, (unsigned long)data_length );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  flags = libspectrum_read_dword( buffer );
  libspectrum_snap_set_issue2( snap, flags & ZXSTKF_ISSUE2 );

  if( expected_length >= 5 ) {
    switch( **buffer ) {
    case ZXJT_KEMPSTON:
      add_joystick( snap, LIBSPECTRUM_JOYSTICK_KEMPSTON,
                    LIBSPECTRUM_JOYSTICK_INPUT_KEYBOARD );
      break;
    case ZXJT_FULLER:
      add_joystick( snap, LIBSPECTRUM_JOYSTICK_FULLER,
                    LIBSPECTRUM_JOYSTICK_INPUT_KEYBOARD );
      break;
    case ZXJT_CURSOR:
      add_joystick( snap, LIBSPECTRUM_JOYSTICK_CURSOR,
                    LIBSPECTRUM_JOYSTICK_INPUT_KEYBOARD );
      break;
    case ZXJT_SINCLAIR1:
      add_joystick( snap, LIBSPECTRUM_JOYSTICK_SINCLAIR_1,
                    LIBSPECTRUM_JOYSTICK_INPUT_KEYBOARD );
      break;
    case ZXJT_SINCLAIR2:
      add_joystick( snap, LIBSPECTRUM_JOYSTICK_SINCLAIR_2,
                    LIBSPECTRUM_JOYSTICK_INPUT_KEYBOARD );
      break;
    case ZXJT_TIMEX1:
      add_joystick( snap, LIBSPECTRUM_JOYSTICK_TIMEX_1,
                    LIBSPECTRUM_JOYSTICK_INPUT_KEYBOARD );
      break;
    case ZXJT_TIMEX2:
      add_joystick( snap, LIBSPECTRUM_JOYSTICK_TIMEX_2,
                    LIBSPECTRUM_JOYSTICK_INPUT_KEYBOARD );
      break;
    case ZXJT_SPECTRUMPLUS: /* Actually, no joystick at all */
      break;
    }
    (*buffer)++;
  }

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_ramp_chunk( libspectrum_snap *snap, libspectrum_word version GCC_UNUSED,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  libspectrum_byte *data;
  size_t page;
  libspectrum_error error;
  libspectrum_word flags;


  error = read_ram_page( &data, &page, buffer, data_length, 0x4000, &flags );
  if( error ) return error;

  if( page > 15 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT,
			     "%s:read_ramp_chunk: unknown page number %lu",
			     __FILE__, (unsigned long)page );
    free( data );
    return LIBSPECTRUM_ERROR_CORRUPT;
  }

  libspectrum_snap_set_pages( snap, page, data );

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_scld_chunk( libspectrum_snap *snap, libspectrum_word version GCC_UNUSED,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  if( data_length != 2 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "szx_read_scld_chunk: unknown length %lu",
			     (unsigned long)data_length );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  libspectrum_snap_set_out_scld_hsr( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_out_scld_dec( snap, **buffer ); (*buffer)++;

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_spcr_chunk( libspectrum_snap *snap, libspectrum_word version,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  libspectrum_byte out_ula;

  if( data_length != 8 ) {
    libspectrum_print_error(
      LIBSPECTRUM_ERROR_UNKNOWN,
      "szx_read_spcr_chunk: unknown length %lu", (unsigned long)data_length
    );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  out_ula = **buffer & 0x07; (*buffer)++;

  libspectrum_snap_set_out_128_memoryport( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_out_plus3_memoryport( snap, **buffer ); (*buffer)++;

  if( version >= 0x0101 ) out_ula |= **buffer & 0xf8;
  (*buffer)++;

  libspectrum_snap_set_out_ula( snap, out_ula );

  *buffer += 4;			/* Skip 'reserved' data */

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_z80r_chunk( libspectrum_snap *snap, libspectrum_word version,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  if( data_length != 37 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "szx_read_z80r_chunk: unknown length %lu",
			     (unsigned long)data_length );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  libspectrum_snap_set_a   ( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_f   ( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_bc  ( snap, libspectrum_read_word( buffer ) );
  libspectrum_snap_set_de  ( snap, libspectrum_read_word( buffer ) );
  libspectrum_snap_set_hl  ( snap, libspectrum_read_word( buffer ) );

  libspectrum_snap_set_a_  ( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_f_  ( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_bc_ ( snap, libspectrum_read_word( buffer ) );
  libspectrum_snap_set_de_ ( snap, libspectrum_read_word( buffer ) );
  libspectrum_snap_set_hl_ ( snap, libspectrum_read_word( buffer ) );

  libspectrum_snap_set_ix  ( snap, libspectrum_read_word( buffer ) );
  libspectrum_snap_set_iy  ( snap, libspectrum_read_word( buffer ) );
  libspectrum_snap_set_sp  ( snap, libspectrum_read_word( buffer ) );
  libspectrum_snap_set_pc  ( snap, libspectrum_read_word( buffer ) );

  libspectrum_snap_set_i   ( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_r   ( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_iff1( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_iff2( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_im  ( snap, **buffer ); (*buffer)++;

  libspectrum_snap_set_tstates( snap, libspectrum_read_dword( buffer ) );

  if( version >= 0x0101 ) {
    (*buffer)++;		/* Skip dwHoldIntReqCycles */
    
    /* Flags; ignore the 'last instruction EI' flag for now */
    libspectrum_snap_set_halted( snap, **buffer & ZXSTZF_HALTED );
    (*buffer)++;

    (*buffer)++;		/* Skip the hidden register */
    (*buffer)++;		/* Skip the reserved byte */

  } else {
    *buffer += 4;		/* Skip the reserved dword */
  }

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_zxat_chunk( libspectrum_snap *snap, libspectrum_word version GCC_UNUSED,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  libspectrum_word flags;

  if( data_length != 8 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "%s:read_zxat_chunk: unknown length %lu",
			     __FILE__, (unsigned long)data_length );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  libspectrum_snap_set_zxatasp_active( snap, 1 );

  flags = libspectrum_read_word( buffer );
  libspectrum_snap_set_zxatasp_upload( snap, flags & ZXSTZXATF_UPLOAD );
  libspectrum_snap_set_zxatasp_writeprotect( snap,
					     flags & ZXSTZXATF_WRITEPROTECT );

  libspectrum_snap_set_zxatasp_port_a( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_zxatasp_port_b( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_zxatasp_port_c( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_zxatasp_control( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_zxatasp_pages( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_zxatasp_current_page( snap, **buffer ); (*buffer)++;

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_zxcf_chunk( libspectrum_snap *snap, libspectrum_word version GCC_UNUSED,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  libspectrum_word flags;

  if( data_length != 4 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "read_zxcf_chunk: unknown length %lu",
			     (unsigned long)data_length );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  libspectrum_snap_set_zxcf_active( snap, 1 );

  flags = libspectrum_read_word( buffer );
  libspectrum_snap_set_zxcf_upload( snap, flags & ZXSTZXCFF_UPLOAD );

  libspectrum_snap_set_zxcf_memctl( snap, **buffer ); (*buffer)++;
  libspectrum_snap_set_zxcf_pages( snap, **buffer ); (*buffer)++;

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_if2r_chunk( libspectrum_snap *snap, libspectrum_word version GCC_UNUSED,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{

#ifdef HAVE_ZLIB_H

  libspectrum_byte *buffer2;

  size_t compressed_length;
  size_t uncompressed_length;

  libspectrum_error error;

  if( data_length < 4 ) {
    libspectrum_print_error(
      LIBSPECTRUM_ERROR_UNKNOWN,
      "szx_read_if2r_chunk: length %lu too short", (unsigned long)data_length
    );
    return LIBSPECTRUM_ERROR_UNKNOWN;
  }

  compressed_length = libspectrum_read_dword( buffer );

  uncompressed_length = 0x4000;

  error = libspectrum_zlib_inflate( *buffer, data_length - 4, &buffer2,
                                    &uncompressed_length );
  if( error ) return error;

  *buffer += data_length - 4;

  libspectrum_snap_set_interface2_active( snap, 1 );

  libspectrum_snap_set_interface2_rom( snap, 0, buffer2 );

  return LIBSPECTRUM_ERROR_NONE;

#else			/* #ifdef HAVE_ZLIB_H */

  libspectrum_print_error(
    LIBSPECTRUM_ERROR_UNKNOWN,
    "%s:read_if2r_chunk: zlib needed for decompression\n",
    __FILE__
  );
  return LIBSPECTRUM_ERROR_UNKNOWN;

#endif			/* #ifdef HAVE_ZLIB_H */

}

static libspectrum_error
read_dock_chunk( libspectrum_snap *snap, libspectrum_word version GCC_UNUSED,
		 const libspectrum_byte **buffer,
		 const libspectrum_byte *end GCC_UNUSED, size_t data_length )
{
  libspectrum_byte *data;
  size_t page;
  libspectrum_error error;
  libspectrum_word flags;
  libspectrum_byte writeable;

  error = read_ram_page( &data, &page, buffer, data_length, 0x2000, &flags );
  if( error ) return error;

  if( page > 7 ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_CORRUPT,
			     "%s:read_dock_chunk: unknown page number %ld",
			     __FILE__, (unsigned long)page );
    free( data );
    return LIBSPECTRUM_ERROR_CORRUPT;
  }

  libspectrum_snap_set_dock_active( snap, 1 );

  writeable = flags & ZXSTDOCKF_RAM;

  if( flags & ZXSTDOCKF_EXROMDOCK ) {
    libspectrum_snap_set_dock_ram( snap, page, writeable );
    libspectrum_snap_set_dock_cart( snap, page, data );
  } else {
    libspectrum_snap_set_exrom_ram( snap, page, writeable );
    libspectrum_snap_set_exrom_cart( snap, page, data );
  }

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
skip_chunk( libspectrum_snap *snap GCC_UNUSED,
	    libspectrum_word version GCC_UNUSED,
	    const libspectrum_byte **buffer,
	    const libspectrum_byte *end GCC_UNUSED,
	    size_t data_length )
{
  *buffer += data_length;
  return LIBSPECTRUM_ERROR_NONE;
}

struct read_chunk_t {

  const char *id;
  read_chunk_fn function;

};

static struct read_chunk_t read_chunks[] = {

  { ZXSTBID_AY,		    read_ay_chunk   },
  { ZXSTBID_BETA128,	    read_b128_chunk },
  { ZXSTBID_BETADISK,	    skip_chunk      },
  { ZXSTBID_COVOX,	    skip_chunk      },
  { ZXSTBID_CREATOR,	    skip_chunk      },
  { ZXSTBID_DOCK,	    read_dock_chunk },
  { ZXSTBID_DSKFILE,	    skip_chunk      },
  { ZXSTBID_GS,		    skip_chunk      },
  { ZXSTBID_GSRAMPAGE,	    skip_chunk      },
  { ZXSTBID_IF1,	    skip_chunk      },
  { ZXSTBID_IF2ROM,	    read_if2r_chunk },
  { ZXSTBID_JOYSTICK,	    read_joy_chunk  },
  { ZXSTBID_KEYBOARD,	    read_keyb_chunk },
  { ZXSTBID_MICRODRIVE,	    skip_chunk      },
  { ZXSTBID_MOUSE,	    skip_chunk      },
  { ZXSTBID_MULTIFACE,	    skip_chunk      },
  { ZXSTBID_PLUS3DISK,	    skip_chunk      },
  { ZXSTBID_RAMPAGE,	    read_ramp_chunk },
  { ZXSTBID_ROM,	    skip_chunk      },
  { ZXSTBID_SPECDRUM,	    skip_chunk      },
  { ZXSTBID_SPECREGS,	    read_spcr_chunk },
  { ZXSTBID_TIMEXREGS,	    read_scld_chunk },
  { ZXSTBID_USPEECH,	    skip_chunk      },
  { ZXSTBID_Z80REGS,	    read_z80r_chunk },
  { ZXSTBID_ZXATASPRAMPAGE, read_atrp_chunk },
  { ZXSTBID_ZXATASP,	    read_zxat_chunk },
  { ZXSTBID_ZXCF,	    read_zxcf_chunk },
  { ZXSTBID_ZXCFRAMPAGE,    read_cfrp_chunk },
  { ZXSTBID_ZXPRINTER,	    skip_chunk      },
  { ZXSTBID_ZXTAPE,	    skip_chunk      },

};

static size_t read_chunks_count =
  sizeof( read_chunks ) / sizeof( struct read_chunk_t );

static libspectrum_error
read_chunk_header( char *id, libspectrum_dword *data_length, 
		   const libspectrum_byte **buffer,
		   const libspectrum_byte *end )
{
  if( end - *buffer < 8 ) {
    libspectrum_print_error(
      LIBSPECTRUM_ERROR_CORRUPT,
      "szx_read_chunk_header: not enough data for chunk header"
    );
    return LIBSPECTRUM_ERROR_CORRUPT;
  }

  memcpy( id, *buffer, 4 ); id[4] = '\0'; *buffer += 4;
  *data_length = libspectrum_read_dword( buffer );

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
read_chunk( libspectrum_snap *snap, libspectrum_word version,
	    const libspectrum_byte **buffer, const libspectrum_byte *end )
{
  char id[5];
  libspectrum_dword data_length;
  libspectrum_error error;
  size_t i; int done;

  error = read_chunk_header( id, &data_length, buffer, end );
  if( error ) return error;

  if( *buffer + data_length > end ) {
    libspectrum_print_error(
      LIBSPECTRUM_ERROR_CORRUPT,
      "szx_read_chunk: chunk length goes beyond end of file"
    );
    return LIBSPECTRUM_ERROR_CORRUPT;
  }

  done = 0;

  for( i = 0; !done && i < read_chunks_count; i++ ) {

    if( !memcmp( id, read_chunks[i].id, 4 ) ) {
      error = read_chunks[i].function( snap, version, buffer, end,
				       data_length );
      if( error ) return error;
      done = 1;
    }

  }

  if( !done ) {
    libspectrum_print_error( LIBSPECTRUM_ERROR_UNKNOWN,
			     "szx_read_chunk: unknown chunk id '%s'", id );
    *buffer += data_length;
  }

  return LIBSPECTRUM_ERROR_NONE;
}

libspectrum_error
libspectrum_szx_read( libspectrum_snap *snap, const libspectrum_byte *buffer,
		      size_t length )
{
  libspectrum_word version;

  libspectrum_error error;
  const libspectrum_byte *end = buffer + length;

  if( end - buffer < 8 ) {
    libspectrum_print_error(
      LIBSPECTRUM_ERROR_CORRUPT,
      "libspectrum_szx_read: not enough data for SZX header"
    );
    return LIBSPECTRUM_ERROR_CORRUPT;
  }

  if( memcmp( buffer, signature, signature_length ) ) {
    libspectrum_print_error(
      LIBSPECTRUM_ERROR_SIGNATURE,
      "libspectrum_szx_read: wrong signature"
    );
    return LIBSPECTRUM_ERROR_SIGNATURE;
  }
  buffer += signature_length;

  version = (*buffer++) << 8; version |= *buffer++;

  switch( *buffer ) {

  case SZX_MACHINE_16:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_16 );
    break;

  case SZX_MACHINE_48:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_48 );
    break;

  case SZX_MACHINE_128:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_128 );
    break;

  case SZX_MACHINE_PLUS2:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_PLUS2 );
    break;

  case SZX_MACHINE_PLUS2A:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_PLUS2A );
    break;

  case SZX_MACHINE_PLUS3:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_PLUS3 );
    break;

  case SZX_MACHINE_PLUS3E:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_PLUS3E );
    break;

  case SZX_MACHINE_PENTAGON:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_PENT );
    break;

  case SZX_MACHINE_TC2048:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_TC2048 );
    break;

  case SZX_MACHINE_TC2068:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_TC2068 );
    break;

  case SZX_MACHINE_TS2068:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_TS2068 );
    break;

  case SZX_MACHINE_SCORPION:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_SCORP );
    break;

  case SZX_MACHINE_SE:
    libspectrum_snap_set_machine( snap, LIBSPECTRUM_MACHINE_SE );
    break;

  default:
    libspectrum_print_error(
      LIBSPECTRUM_MACHINE_UNKNOWN,
      "libspectrum_szx_read: unknown machine type %d", (int)*buffer
    );
    return LIBSPECTRUM_MACHINE_UNKNOWN;
  }

  /* Skip to the end of the header */
  buffer += 2;

  while( buffer < end ) {
    error = read_chunk( snap, version, &buffer, end );
    if( error ) return error;
  }

  return LIBSPECTRUM_ERROR_NONE;
}

libspectrum_error
libspectrum_szx_write( libspectrum_byte **buffer, size_t *length,
		       int *out_flags, libspectrum_snap *snap,
		       libspectrum_creator *creator, int in_flags )
{
  libspectrum_byte *ptr = *buffer;
  int capabilities, compress;
  libspectrum_error error;
  size_t i;

  *out_flags = 0;

  capabilities =
    libspectrum_machine_capabilities( libspectrum_snap_machine( snap ) );

  compress = !( in_flags & LIBSPECTRUM_FLAG_SNAPSHOT_NO_COMPRESSION );

  error = write_file_header( buffer, &ptr, length, out_flags, snap );
  if( error ) return error;

  if( creator ) {
    error = write_crtr_chunk( buffer, &ptr, length, creator );
    if( error ) return error;
  }

  error = write_z80r_chunk( buffer, &ptr, length, snap );
  if( error ) return error;

  error = write_spcr_chunk( buffer, &ptr, length, snap );
  if( error ) return error;

  error = write_joy_chunk( buffer, &ptr, length, out_flags, snap );
  if( error ) return error;

  error = write_keyb_chunk( buffer, &ptr, length, out_flags, snap );
  if( error ) return error;

  error = write_ram_pages( buffer, &ptr, length, snap, compress );
  if( error ) return error;

  if( capabilities & LIBSPECTRUM_MACHINE_CAPABILITY_AY ) {
    error = write_ay_chunk( buffer, &ptr, length, snap );
    if( error ) return error;
  }

  if( capabilities & ( LIBSPECTRUM_MACHINE_CAPABILITY_TIMEX_MEMORY |
                       LIBSPECTRUM_MACHINE_CAPABILITY_SE_MEMORY ) ) {
    error = write_scld_chunk( buffer, &ptr, length, snap );
    if( error ) return error;
  }

  if( capabilities & LIBSPECTRUM_MACHINE_CAPABILITY_TRDOS_DISK ) {
    error = write_b128_chunk( buffer, &ptr, length, snap );
    if( error ) return error;
  }

  if( libspectrum_snap_zxatasp_active( snap ) ) {
    error = write_zxat_chunk( buffer, &ptr, length, snap );
    if( error ) return error;

    for( i = 0; i < libspectrum_snap_zxatasp_pages( snap ); i++ ) {
      error = write_atrp_chunk( buffer, &ptr, length, snap, i, compress );
      if( error ) return error;
    }
  }

  if( libspectrum_snap_zxcf_active( snap ) ) {
    error = write_zxcf_chunk( buffer, &ptr, length, snap );
    if( error ) return error;

    for( i = 0; i < libspectrum_snap_zxcf_pages( snap ); i++ ) {
      error = write_cfrp_chunk( buffer, &ptr, length, snap, i, compress );
      if( error ) return error;
    }
  }

  if( libspectrum_snap_interface2_active( snap ) ) {
#ifdef HAVE_ZLIB_H
    error = write_if2r_chunk( buffer, &ptr, length, snap );
    if( error ) return error;
#else
    /* IF2R blocks only support writing compressed images */
    *out_flags |= LIBSPECTRUM_FLAG_SNAPSHOT_MAJOR_INFO_LOSS;
#endif                         /* #ifdef HAVE_ZLIB_H */
  }

  if( libspectrum_snap_dock_active( snap ) ) {
    for( i = 0; i < 8; i++ ) {
      if( libspectrum_snap_exrom_cart( snap, i ) ) {
        error = write_dock_chunk( buffer, &ptr, length, snap, 0,
                                  libspectrum_snap_exrom_cart( snap, i ), i,
                                  libspectrum_snap_exrom_ram( snap, i ),
                                  compress );
        if( error ) return error;
      }
      if( libspectrum_snap_dock_cart( snap, i ) ) {
        error = write_dock_chunk( buffer, &ptr, length, snap, 1,
                                  libspectrum_snap_dock_cart( snap, i ), i,
                                  libspectrum_snap_dock_ram( snap, i ),
                                  compress );
        if( error ) return error;
      }
    }
  }

  /* Set length to be actual length, not allocated length */
  *length = ptr - *buffer;

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_file_header( libspectrum_byte **buffer, libspectrum_byte **ptr,
		   size_t *length, int *out_flags, libspectrum_snap *snap )
{
  libspectrum_error error;

  error = libspectrum_make_room( buffer, 8, ptr, length );
  if( error ) return error;

  memcpy( *ptr, signature, 4 ); *ptr += 4;
  
  /* [Assumption] We currently write version 1.3 files (major, minor) */
  *(*ptr)++ = SZX_VERSION_MAJOR; *(*ptr)++ = SZX_VERSION_MINOR;

  switch( libspectrum_snap_machine( snap ) ) {

  case LIBSPECTRUM_MACHINE_16:     **ptr = SZX_MACHINE_16; break;
  case LIBSPECTRUM_MACHINE_48:     **ptr = SZX_MACHINE_48; break;
  case LIBSPECTRUM_MACHINE_128:    **ptr = SZX_MACHINE_128; break;
  case LIBSPECTRUM_MACHINE_PLUS2:  **ptr = SZX_MACHINE_PLUS2; break;
  case LIBSPECTRUM_MACHINE_PLUS2A: **ptr = SZX_MACHINE_PLUS2A; break;
  case LIBSPECTRUM_MACHINE_PLUS3:  **ptr = SZX_MACHINE_PLUS3; break;
  case LIBSPECTRUM_MACHINE_PLUS3E: **ptr = SZX_MACHINE_PLUS3E; break;
  case LIBSPECTRUM_MACHINE_PENT:   **ptr = SZX_MACHINE_PENTAGON; break;
  case LIBSPECTRUM_MACHINE_TC2048: **ptr = SZX_MACHINE_TC2048; break;
  case LIBSPECTRUM_MACHINE_TC2068: **ptr = SZX_MACHINE_TC2068; break;
  case LIBSPECTRUM_MACHINE_TS2068: **ptr = SZX_MACHINE_TS2068; break;
  case LIBSPECTRUM_MACHINE_SCORP:  **ptr = SZX_MACHINE_SCORPION; break;
  case LIBSPECTRUM_MACHINE_SE: **ptr = SZX_MACHINE_SE; break;

  case LIBSPECTRUM_MACHINE_UNKNOWN:
    libspectrum_print_error( LIBSPECTRUM_ERROR_LOGIC,
			     "Emulated machine type is set to 'unknown'!" );
    return LIBSPECTRUM_ERROR_LOGIC;
  }
  (*ptr)++;

  /* Reserved byte */
  *(*ptr)++ = '\0';

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_crtr_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_creator *creator )
{
  libspectrum_error error;
  size_t custom_length;

  custom_length = libspectrum_creator_custom_length( creator );

  error = write_chunk_header( buffer, ptr, length, ZXSTBID_CREATOR,
			      36 + custom_length );
  if( error ) return error;

  memcpy( *ptr, libspectrum_creator_program( creator ), 32 ); *ptr += 32;
  libspectrum_write_word( ptr, libspectrum_creator_major( creator ) );
  libspectrum_write_word( ptr, libspectrum_creator_minor( creator ) );

  if( custom_length ) {
    memcpy( *ptr, libspectrum_creator_custom( creator ), custom_length );
    *ptr += custom_length;
  }

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_z80r_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap )
{
  libspectrum_dword tstates;
  libspectrum_byte flags;
  libspectrum_error error;

  error = write_chunk_header( buffer, ptr, length, ZXSTBID_Z80REGS, 37 );
  if( error ) return error;

  *(*ptr)++ = libspectrum_snap_a ( snap );
  *(*ptr)++ = libspectrum_snap_f ( snap );
  libspectrum_write_word( ptr, libspectrum_snap_bc  ( snap ) );
  libspectrum_write_word( ptr, libspectrum_snap_de  ( snap ) );
  libspectrum_write_word( ptr, libspectrum_snap_hl  ( snap ) );

  *(*ptr)++ = libspectrum_snap_a_( snap );
  *(*ptr)++ = libspectrum_snap_f_( snap );
  libspectrum_write_word( ptr, libspectrum_snap_bc_ ( snap ) );
  libspectrum_write_word( ptr, libspectrum_snap_de_ ( snap ) );
  libspectrum_write_word( ptr, libspectrum_snap_hl_ ( snap ) );

  libspectrum_write_word( ptr, libspectrum_snap_ix  ( snap ) );
  libspectrum_write_word( ptr, libspectrum_snap_iy  ( snap ) );
  libspectrum_write_word( ptr, libspectrum_snap_sp  ( snap ) );
  libspectrum_write_word( ptr, libspectrum_snap_pc  ( snap ) );

  *(*ptr)++ = libspectrum_snap_i   ( snap );
  *(*ptr)++ = libspectrum_snap_r   ( snap );
  *(*ptr)++ = libspectrum_snap_iff1( snap );
  *(*ptr)++ = libspectrum_snap_iff2( snap );
  *(*ptr)++ = libspectrum_snap_im  ( snap );

  tstates = libspectrum_snap_tstates( snap );

  libspectrum_write_dword( ptr, tstates );

  /* Number of tstates remaining in which an interrupt can occur */
  if( tstates < 48 ) {
    *(*ptr)++ = (unsigned char)(48 - tstates);
  } else {
    *(*ptr)++ = '\0';
  }

  flags = '\0';
  if( libspectrum_snap_last_instruction_ei( snap ) ) flags |= ZXSTZF_EILAST;
  if( libspectrum_snap_halted( snap ) ) flags |= ZXSTZF_HALTED;
  *(*ptr)++ = flags;

  /* Hidden register not supported */
  *(*ptr)++ = '\0';

  /* Reserved byte */
  *(*ptr)++ = '\0';

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_spcr_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap )
{
  libspectrum_error error;
  int capabilities;

  error = write_chunk_header( buffer, ptr, length, ZXSTBID_SPECREGS, 8 );
  if( error ) return error;

  capabilities =
    libspectrum_machine_capabilities( libspectrum_snap_machine( snap ) );

  /* Border colour */
  *(*ptr)++ = libspectrum_snap_out_ula( snap ) & 0x07;

  if( capabilities & LIBSPECTRUM_MACHINE_CAPABILITY_128_MEMORY ) {
    *(*ptr)++ = libspectrum_snap_out_128_memoryport( snap );
  } else {
    *(*ptr)++ = '\0';
  }
  
  /* [Assumption] The Scorpion port 0x1ffd should be written here */
  if( capabilities & LIBSPECTRUM_MACHINE_CAPABILITY_PLUS3_MEMORY || 
      capabilities & LIBSPECTRUM_MACHINE_CAPABILITY_SCORP_MEMORY    ) {
    *(*ptr)++ = libspectrum_snap_out_plus3_memoryport( snap );
  } else {
    *(*ptr)++ = '\0';
  }

  *(*ptr)++ = libspectrum_snap_out_ula( snap );

  /* Reserved bytes */
  libspectrum_write_dword( ptr, 0 );

  return LIBSPECTRUM_ERROR_NONE;
}

static void
write_joystick( libspectrum_byte **ptr, int *out_flags, libspectrum_snap *snap,
                const int connection )
{
  size_t num_joysticks = libspectrum_snap_joystick_active_count( snap );
  int found = 0;
  int i;

  for( i = 0; i < num_joysticks; i++ ) {
    if( libspectrum_snap_joystick_inputs( snap, i ) & connection ) {
      switch( libspectrum_snap_joystick_list( snap, i ) ) {
      case LIBSPECTRUM_JOYSTICK_CURSOR:
        if( !found ) { *(*ptr)++ = ZXJT_CURSOR; found = 1; }
        else *out_flags |= LIBSPECTRUM_FLAG_SNAPSHOT_MINOR_INFO_LOSS;
        break;
      case LIBSPECTRUM_JOYSTICK_KEMPSTON:
        if( !found ) { *(*ptr)++ = ZXJT_KEMPSTON; found = 1; }
        else *out_flags |= LIBSPECTRUM_FLAG_SNAPSHOT_MINOR_INFO_LOSS;
        break;
      case LIBSPECTRUM_JOYSTICK_SINCLAIR_1:
        if( !found ) { *(*ptr)++ = ZXJT_SINCLAIR1; found = 1; }
        else *out_flags |= LIBSPECTRUM_FLAG_SNAPSHOT_MINOR_INFO_LOSS;
        break;
      case LIBSPECTRUM_JOYSTICK_SINCLAIR_2:
        if( !found ) { *(*ptr)++ = ZXJT_SINCLAIR2; found = 1; }
        else *out_flags |= LIBSPECTRUM_FLAG_SNAPSHOT_MINOR_INFO_LOSS;
        break;
      case LIBSPECTRUM_JOYSTICK_TIMEX_1:
        if( !found ) { *(*ptr)++ = ZXJT_TIMEX1; found = 1; }
        else *out_flags |= LIBSPECTRUM_FLAG_SNAPSHOT_MINOR_INFO_LOSS;
        break;
      case LIBSPECTRUM_JOYSTICK_TIMEX_2:
        if( !found ) { *(*ptr)++ = ZXJT_TIMEX2; found = 1; }
        else *out_flags |= LIBSPECTRUM_FLAG_SNAPSHOT_MINOR_INFO_LOSS;
        break;
      case LIBSPECTRUM_JOYSTICK_FULLER:
        if( !found ) { *(*ptr)++ = ZXJT_FULLER; found = 1; }
        else *out_flags |= LIBSPECTRUM_FLAG_SNAPSHOT_MINOR_INFO_LOSS;
        break;
      case LIBSPECTRUM_JOYSTICK_NONE: /* Shouldn't happen */
      default:
        *(*ptr)++ = ZXJT_NONE;
        break;
      }
    }
  }

  if( !found ) *(*ptr)++ = ZXJT_NONE;
}

static libspectrum_error
write_joy_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, int *out_flags, libspectrum_snap *snap )
{
  libspectrum_error error;
  libspectrum_dword flags;
  size_t num_joysticks = libspectrum_snap_joystick_active_count( snap );
  int i;

  error = write_chunk_header( buffer, ptr, length, ZXSTBID_JOYSTICK, 6 );
  if( error ) return error;

  flags = 0;
  for( i = 0; i < num_joysticks; i++ ) {
    if( libspectrum_snap_joystick_list( snap, i ) ==
        LIBSPECTRUM_JOYSTICK_KEMPSTON )
      flags |= ZXSTJOYF_ALWAYSPORT31;
  }
  libspectrum_write_dword( ptr, flags );

  write_joystick( ptr, out_flags, snap, LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_1 );
  write_joystick( ptr, out_flags, snap, LIBSPECTRUM_JOYSTICK_INPUT_JOYSTICK_2 );

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_keyb_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, int *out_flags, libspectrum_snap *snap )
{
  libspectrum_error error;
  libspectrum_dword flags;

  error = write_chunk_header( buffer, ptr, length, ZXSTBID_KEYBOARD, 5 );
  if( error ) return error;

  flags = 0;
  if( libspectrum_snap_issue2( snap ) ) flags |= ZXSTKF_ISSUE2;

  libspectrum_write_dword( ptr, flags );

  write_joystick( ptr, out_flags, snap, LIBSPECTRUM_JOYSTICK_INPUT_KEYBOARD );

  return LIBSPECTRUM_ERROR_NONE;
}
  
static libspectrum_error
write_ram_pages( libspectrum_byte **buffer, libspectrum_byte **ptr,
		 size_t *length, libspectrum_snap *snap, int compress )
{
  libspectrum_machine machine;
  int capabilities; 
  libspectrum_error error;

  machine = libspectrum_snap_machine( snap );
  capabilities = libspectrum_machine_capabilities( machine );

  error = write_ramp_chunk( buffer, ptr, length, snap, 5, compress );
  if( error ) return error;

  /* [Assumption] This is the right way to write Timex machine RAM */
  if( machine != LIBSPECTRUM_MACHINE_16 ) {
    error = write_ramp_chunk( buffer, ptr, length, snap, 2, compress );
    if( error ) return error;
    error = write_ramp_chunk( buffer, ptr, length, snap, 0, compress );
    if( error ) return error;
  }

  if( capabilities & LIBSPECTRUM_MACHINE_CAPABILITY_128_MEMORY ) {
    error = write_ramp_chunk( buffer, ptr, length, snap, 1, compress );
    if( error ) return error;
    error = write_ramp_chunk( buffer, ptr, length, snap, 3, compress );
    if( error ) return error;
    error = write_ramp_chunk( buffer, ptr, length, snap, 4, compress );
    if( error ) return error;
    error = write_ramp_chunk( buffer, ptr, length, snap, 6, compress );
    if( error ) return error;
    error = write_ramp_chunk( buffer, ptr, length, snap, 7, compress );
    if( error ) return error;

    /* [Assumption] RAM pages 8-15 are valid here */
    if( capabilities & LIBSPECTRUM_MACHINE_CAPABILITY_SCORP_MEMORY ) {
      int i;
      for( i = 8; i < 16; i++ ) {
        error = write_ramp_chunk( buffer, ptr, length, snap, i, compress );
        if( error ) return error;
      }
    }

  }

  if( capabilities & LIBSPECTRUM_MACHINE_CAPABILITY_SE_MEMORY ) {
    error = write_ramp_chunk( buffer, ptr, length, snap, 8, compress );
    if( error ) return error;
  }

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_ramp_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap, int page,
		  int compress )
{
  libspectrum_error error;
  const libspectrum_byte *data;

  data = libspectrum_snap_pages( snap, page );

  error = write_ram_page( buffer, ptr, length, ZXSTBID_RAMPAGE, data, 0x4000,
			  page, compress, 0x00 );
  if( error ) return error;

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_ram_page( libspectrum_byte **buffer, libspectrum_byte **ptr,
		size_t *length, const char *id, const libspectrum_byte *data,
		size_t data_length, int page, int compress, int extra_flags )
{
  libspectrum_error error;
  libspectrum_byte *block_length, *flags, *compressed_data;
  int use_compression;

  if( !data ) return LIBSPECTRUM_ERROR_NONE;

  /* 8 for the chunk header, 3 for the flags and the page number */
  error = libspectrum_make_room( buffer, 8 + 3, ptr, length );
  if( error ) return error;

  memcpy( *ptr, id, 4 ); (*ptr) += 4;

  /* Store this location for later */
  block_length = *ptr; *ptr += 4;

  /* And this one */
  flags = *ptr; *ptr += 2;

  *(*ptr)++ = (libspectrum_byte)page;

  use_compression = 0;
  compressed_data = NULL;

#ifdef HAVE_ZLIB_H

  if( compress ) {

    size_t compressed_length;

    error = libspectrum_zlib_compress( data, data_length,
				       &compressed_data, &compressed_length );
    if( error ) return error;

    if( compress & LIBSPECTRUM_FLAG_SNAPSHOT_ALWAYS_COMPRESS ||
        compressed_length < data_length ) {
      use_compression = 1;
      data = compressed_data;
      data_length = compressed_length;
    }
  }

#endif				/* #ifdef HAVE_ZLIB_H */

  if( use_compression ) extra_flags |= ZXSTRF_COMPRESSED;

  libspectrum_write_dword( &block_length, 3 + data_length );
  libspectrum_write_word( &flags, extra_flags );

  error = libspectrum_make_room( buffer, data_length, ptr, length );
  if( error ) { if( compressed_data ) free( compressed_data ); return error; }

  memcpy( *ptr, data, data_length ); *ptr += data_length;

  if( compressed_data ) free( compressed_data );

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_ay_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		size_t *length, libspectrum_snap *snap )
{
  libspectrum_error error;
  size_t i;

  error = write_chunk_header( buffer, ptr, length, ZXSTBID_AY, 18 );
  if( error ) return error;

  *(*ptr)++ = '\0';			/* Flags */
  *(*ptr)++ = libspectrum_snap_out_ay_registerport( snap );

  for( i = 0; i < 16; i++ )
    *(*ptr)++ = libspectrum_snap_ay_registers( snap, i );

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_scld_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		size_t *length, libspectrum_snap *snap )
{
  libspectrum_error error;

  error = write_chunk_header( buffer, ptr, length, ZXSTBID_TIMEXREGS, 2 );
  if( error ) return error;

  *(*ptr)++ = libspectrum_snap_out_scld_hsr( snap );
  *(*ptr)++ = libspectrum_snap_out_scld_dec( snap );

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_b128_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap )
{
  libspectrum_error error;
  libspectrum_dword flags;

  error = write_chunk_header( buffer, ptr, length, ZXSTBID_BETA128, 10 );
  if( error ) return error;

  flags = ZXSTBETAF_CONNECTED;	/* Betadisk interface connected */
  if( libspectrum_snap_beta_paged( snap ) ) flags |= ZXSTBETAF_PAGED;
  if( !libspectrum_snap_beta_direction( snap ) ) flags |= ZXSTBETAF_SEEKLOWER;
  libspectrum_write_dword( ptr, flags );

  *(*ptr)++ = 2;	/* 2 drives connected */
  *(*ptr)++ = libspectrum_snap_beta_system( snap );
  *(*ptr)++ = libspectrum_snap_beta_track ( snap );
  *(*ptr)++ = libspectrum_snap_beta_sector( snap );
  *(*ptr)++ = libspectrum_snap_beta_data  ( snap );
  *(*ptr)++ = libspectrum_snap_beta_status( snap );

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_zxat_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap )
{
  libspectrum_word flags;
  libspectrum_error error;

  error = write_chunk_header( buffer, ptr, length, ZXSTBID_ZXATASP, 8 );

  flags = 0;
  if( libspectrum_snap_zxatasp_upload ( snap ) ) flags |= ZXSTZXATF_UPLOAD;
  if( libspectrum_snap_zxatasp_writeprotect( snap ) )
    flags |= ZXSTZXATF_WRITEPROTECT;
  libspectrum_write_word( ptr, flags );

  *(*ptr)++ = libspectrum_snap_zxatasp_port_a( snap );
  *(*ptr)++ = libspectrum_snap_zxatasp_port_b( snap );
  *(*ptr)++ = libspectrum_snap_zxatasp_port_c( snap );
  *(*ptr)++ = libspectrum_snap_zxatasp_control( snap );
  *(*ptr)++ = libspectrum_snap_zxatasp_pages( snap );
  *(*ptr)++ = libspectrum_snap_zxatasp_current_page( snap );

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_atrp_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap, int page,
		  int compress )
{
  libspectrum_error error;
  const libspectrum_byte *data;

  data = libspectrum_snap_zxatasp_ram( snap, page );

  error = write_ram_page( buffer, ptr, length, ZXSTBID_ZXATASPRAMPAGE, data,
			  0x4000, page, compress, 0x00 );
  if( error ) return error;

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_zxcf_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap )
{
  libspectrum_word flags;
  libspectrum_error error;

  error = write_chunk_header( buffer, ptr, length, ZXSTBID_ZXCF, 4 );

  flags = 0;
  if( libspectrum_snap_zxcf_upload( snap ) ) flags |= ZXSTZXCFF_UPLOAD;
  libspectrum_write_word( ptr, flags );

  *(*ptr)++ = libspectrum_snap_zxcf_memctl( snap );
  *(*ptr)++ = libspectrum_snap_zxcf_pages( snap );

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_cfrp_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap, int page,
		  int compress )
{
  libspectrum_error error;
  const libspectrum_byte *data;

  data = libspectrum_snap_zxcf_ram( snap, page );

  error = write_ram_page( buffer, ptr, length, ZXSTBID_ZXCFRAMPAGE, data,
			  0x4000, page, compress, 0x00 );
  if( error ) return error;

  return LIBSPECTRUM_ERROR_NONE;
}

#ifdef HAVE_ZLIB_H

static libspectrum_error
write_if2r_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		size_t *length, libspectrum_snap *snap )
{
  libspectrum_error error;
  libspectrum_byte *block_length, *data, *cart_size, *compressed_data;
  size_t data_length, compressed_length;

  /* 8 for the chunk header, 4 for the compressed cart size */
  error = libspectrum_make_room( buffer, 8 + 4, ptr, length );
  if( error ) return error;

  memcpy( *ptr, ZXSTBID_IF2ROM, 4 ); (*ptr) += 4;

  /* Store this location for later */
  block_length = *ptr; *ptr += 4;

  /* And this one */
  cart_size = *ptr; *ptr += 4;

  data = libspectrum_snap_interface2_rom( snap, 0 ); data_length = 0x4000;
  compressed_data = NULL;

  error = libspectrum_zlib_compress( data, data_length,
                                     &compressed_data, &compressed_length );
  if( error ) return error;

  libspectrum_write_dword( &block_length, 4 + compressed_length );
  libspectrum_write_dword( &cart_size, compressed_length );

  error = libspectrum_make_room( buffer, compressed_length, ptr, length );
  if( error ) { if( compressed_data ) free( compressed_data ); return error; }

  memcpy( *ptr, compressed_data, compressed_length ); *ptr += compressed_length;

  if( compressed_data ) free( compressed_data );

  return LIBSPECTRUM_ERROR_NONE;
}

#endif                         /* #ifdef HAVE_ZLIB_H */

static libspectrum_error
write_dock_chunk( libspectrum_byte **buffer, libspectrum_byte **ptr,
		  size_t *length, libspectrum_snap *snap, int exrom_dock,
                  const libspectrum_byte *data, int page, int writeable,
                  int compress )
{
  libspectrum_error error;
  libspectrum_byte extra_flags;

  extra_flags = 0;
  if( writeable  ) extra_flags |= ZXSTDOCKF_RAM;
  if( exrom_dock ) extra_flags |= ZXSTDOCKF_EXROMDOCK;

  error = write_ram_page( buffer, ptr, length, ZXSTBID_DOCK, data, 0x2000,
			  page, compress, extra_flags );
  if( error ) return error;

  return LIBSPECTRUM_ERROR_NONE;
}

static libspectrum_error
write_chunk_header( libspectrum_byte **buffer, libspectrum_byte **ptr,
		    size_t *length, const char *id,
		    libspectrum_dword block_length )
{
  libspectrum_error error;

  error = libspectrum_make_room( buffer, 8 + block_length, ptr, length );
  if( error ) return error;

  memcpy( *ptr, id, 4 ); *ptr += 4;
  libspectrum_write_dword( ptr, block_length );

  return LIBSPECTRUM_ERROR_NONE;
}
