/* Copyright 2022-2026 Kurt Nienhaus
 *
 * This file is part of VgaGames.
 * VgaGames 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.
 * VgaGames 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 VgaGames.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include "sprite.h"

void init_sprite(void);
void dest_sprite(void);

#define FRML_NLOOP      "nloop"
#define FRML_REPEAT     "repeat"
#define FRML_REPEATMAX  "repeatmax"

struct VG_Sprite * vg4_sprite_load_nolist(const char *);

static struct VG_Sprite * sprite_load(const char *);
static struct VG_Sprite * sprite_load_doit(const char *, VG_BOOL);
static void sprite_destroy(struct VG_Sprite *);
static void sprite_destroyall(void);
static const char * sprite_getname(const struct VG_Sprite *);
static int sprite_get_blocks(const struct VG_Sprite *);
static void sprite_go_block(struct VG_Sprite *, int);
static void sprite_rewind(struct VG_Sprite *);
static void sprite_imagesize(const struct VG_Sprite *, const char *, int *, int *);
static VG_BOOL sprite_has_next(struct VG_Sprite *);
static VG_BOOL sprite_next(struct VG_Sprite *, struct VG_Image **, struct VG_ImagecopyAttr *);
static VG_BOOL sprite_current(struct VG_Sprite *, struct VG_Image **, struct VG_ImagecopyAttr *);
static void sprite_dump(FILE *);

static void get_cksum(char *, const char *);
static void correct_filenames(const char *, const char *, char **, const char *, char **);
static void line_in_hash(struct SML3_hash *, const char *, VG_BOOL);
static VG_BOOL hash_in_spriteelem(struct vgi_sprite *, struct SML3_hash *);
static void sprite_addelem(struct vgi_sprite *, int, const char *, struct VG_ImagecopyAttr *, const char *, int, int);
static VG_BOOL snd_isplaying(struct vgi_sprite *);
static void snd_stop(struct vgi_sprite *, VG_BOOL);


/* set functions */
void
init_sprite(void)
{
  vg4data.lists.sprite.list = SML3_calloc(1, sizeof(*vg4data.lists.sprite.list));  /* first list-element is unused */
  vg4data.lists.sprite.list->prev = vg4data.lists.sprite.list->next = NULL;
  vg4->sprite->load = sprite_load;
  vg4->sprite->destroy = sprite_destroy;
  vg4->sprite->destroyall = sprite_destroyall;
  vg4->sprite->getname = sprite_getname;
  vg4->sprite->get_blocks = sprite_get_blocks;
  vg4->sprite->go_block = sprite_go_block;
  vg4->sprite->rewind = sprite_rewind;
  vg4->sprite->imagesize = sprite_imagesize;
  vg4->sprite->has_next = sprite_has_next;
  vg4->sprite->next = sprite_next;
  vg4->sprite->current = sprite_current;
  vg4->sprite->dump = sprite_dump;
} /* Ende init_sprite */

void
dest_sprite(void)
{
  vg4->sprite->destroyall();
  free(vg4data.lists.sprite.list);  /* first list-element is unused */
  vg4data.lists.sprite.list = NULL;
} /* Ende dest_sprite */


/* vg4_sprite_load_nolist: like sprite_load(), but without inserting into list */
struct VG_Sprite *
vg4_sprite_load_nolist(const char *filename)
{
  return sprite_load_doit(filename, VG_FALSE);
} /* Ende vg4_sprite_load_nolist */


/* return SHA1-sum of string into sha1buf */
static void
get_cksum(char *sha1buf, const char *string)
{ void *cksum;
  char bm5[64];
  if (string == NULL) { string = ""; }
  cksum = SML3_cksum_init(SML3_CKSUM_DIGEST_SHA1);
  SML3_cksum_add(cksum, (void *)string, strlen(string));
  SML3_cksum_result(cksum, bm5, sizeof(bm5));
  if (sha1buf != NULL) { memcpy(sha1buf, bm5, 40 + 1); }
} /* Ende get_cksum */


/* set corrected paths to files to load */
static void
correct_filenames(const char *dirname, const char *imgfile, char **rfileimg, const char *sndfile, char **rfilesnd)
{
  static char rfile1[VGI_PATHSIZE] = "";
  static char rfile2[VGI_PATHSIZE] = "";

  if (dirname == NULL || *dirname == '\0') { dirname = "."; }

  if (imgfile != NULL) {
    if (*imgfile != '/') {
      vg4->misc->strccat(rfile1, sizeof(rfile1), dirname, "/", imgfile, NULL);
    } else {
      vg4->misc->strcpy(rfile1, sizeof(rfile1), imgfile + 1);
    }
    if (rfileimg != NULL) { *rfileimg = rfile1; }
  } else {
    if (rfileimg != NULL) { *rfileimg = NULL; }
  }

  if (sndfile != NULL) {
    if (*sndfile != '/') {
      vg4->misc->strccat(rfile2, sizeof(rfile2), dirname, "/", sndfile, NULL);
    } else {
      vg4->misc->strcpy(rfile2, sizeof(rfile2), sndfile + 1);
    }
    if (rfilesnd != NULL) { *rfilesnd = rfile2; }
  } else {
    if (rfilesnd != NULL) { *rfilesnd = NULL; }
  }
} /* Ende correct_filenames */


/* split zeile at spaces/tab and insert into hs1 */
static void
line_in_hash(struct SML3_hash *hs1, const char *zeile, VG_BOOL use_space)
{
  struct SML3_gummi gm = SML3_GUM_INITIALIZER;
  const char *saveptr;
  char *tstring, *pt1;

  if (hs1 == NULL || zeile == NULL || *zeile == '\0') { return; }

  if (use_space) {
    saveptr = zeile;
    while ((tstring = SML3_string_toks(&saveptr, " \t", &gm)) != NULL) {
      pt1 = strchr(tstring, '=');
      if (pt1 != NULL && *tstring != '\0' && *pt1 != '\0') {
        *pt1++ = '\0';
        SML3_hashelem_valset(SML3_hashset(hs1, tstring, strlen(tstring) + 1), pt1, strlen(pt1) + 1);
      }
    }
  } else {
    SML3_gumcpy(&gm, 0, zeile);
    tstring = SML3_gumgetval(&gm);
    pt1 = strpbrk(tstring, "=:");
    if (pt1 != NULL) {
      *pt1++ = '\0';
      SML3_schnipp(tstring, " \t", 3);
      SML3_schnipp(pt1, VGI_WSPACE, 3);
      if (*tstring != '\0' && *pt1 != '\0') {
        SML3_hashelem_valset(SML3_hashset(hs1, tstring, strlen(tstring) + 1), pt1, strlen(pt1) + 1);
      }
    }
  }

  SML3_gumdest(&gm);
} /* Ende line_in_hash */


/* insert new element from hs1 into spre */
static VG_BOOL
hash_in_spriteelem(struct vgi_sprite *spre, struct SML3_hash *hs1)
{
  int anz_nloop, voladj, audioflag, prez, repeat, irep;
  struct VG_ImagecopyAttr iattr;
  char *imgfile, *sndfile, *pval, buf[32], *rfileimg, *rfilesnd;
  struct SML3_hashelem *he1, *he_nloop, *he_repeat, *he_repeatmax;
  struct SML3_formel_vhash *vhash;
  SML3_int64 iwert;

  if (spre == NULL || hs1 == NULL) { return VG_TRUE; }

  vhash = SML3_formel_ebene_new(1);

  /* add variables */

  if (SML3_formel_expandvar(vhash, &he_nloop, FRML_NLOOP, NULL, 1) == NULL) {
    outerr("%s", SML3_fehlermsg());
    return VG_FALSE;
  }
  if (!SML3_hashelem_typ_is_normal(he_nloop)) {
    outerr("variable %s is invalid", FRML_NLOOP);
    return VG_FALSE;
  }
  snprintf(buf, sizeof(buf), "%d", spre->dfl_nloop);
  SML3_hashelem_valset(he_nloop, buf, strlen(buf) + 1);

  if (SML3_formel_expandvar(vhash, &he_repeat, FRML_REPEAT, NULL, 1) == NULL) {
    outerr("%s", SML3_fehlermsg());
    return VG_FALSE;
  }
  if (!SML3_hashelem_typ_is_normal(he_repeat)) {
    outerr("variable %s is invalid", FRML_REPEAT);
    return VG_FALSE;
  }

  if (SML3_formel_expandvar(vhash, &he_repeatmax, FRML_REPEATMAX, NULL, 1) == NULL) {
    outerr("%s", SML3_fehlermsg());
    return VG_FALSE;
  }
  if (!SML3_hashelem_typ_is_normal(he_repeatmax)) {
    outerr("variable %s is invalid", FRML_REPEATMAX);
    return VG_FALSE;
  }

  /* read parameters */

  repeat = 1;
  if ((he1 = SML3_hashget(hs1, "REPEAT", sizeof("REPEAT"))) != NULL) {
    pval = (char *)SML3_hashelem_valget(he1, NULL);
    prez = -1;
    if (SML3_formel_rechne(vhash, &iwert, NULL, &prez, pval, NULL) == NULL) {
      outerr("%s", SML3_fehlermsg());
      return VG_FALSE;
    }
    repeat = (int)iwert;
    if (repeat < 1) { repeat = 1; }
  }
  snprintf(buf, sizeof(buf), "%d", repeat);
  SML3_hashelem_valset(he_repeatmax, buf, strlen(buf) + 1);

  imgfile = NULL;
  if ((he1 = SML3_hashget(hs1, "IMAGE", sizeof("IMAGE"))) != NULL) {
    imgfile = (char *)SML3_hashelem_valget(he1, NULL);
  }

  /* for each repetition */
  for (irep = 1; irep <= repeat; irep++) {
    snprintf(buf, sizeof(buf), "%d", irep);
    SML3_hashelem_valset(he_repeat, buf, strlen(buf) + 1);

    anz_nloop = 0;
    if ((he1 = SML3_hashget(hs1, "NLOOP", sizeof("NLOOP"))) != NULL) {
      pval = (char *)SML3_hashelem_valget(he1, NULL);
      prez = -1;
      if (SML3_formel_rechne(vhash, &iwert, NULL, &prez, pval, NULL) == NULL) {
        outerr("%s", SML3_fehlermsg());
        return VG_FALSE;
      }
      anz_nloop = (int)iwert;
    }

    VG_IMAGECOPY_ATTR_DEFAULT(&iattr);

    if ((he1 = SML3_hashget(hs1, "ZOOM", sizeof("ZOOM"))) != NULL) {
      pval = (char *)SML3_hashelem_valget(he1, NULL);
      prez = -1;
      if (SML3_formel_rechne(vhash, &iwert, NULL, &prez, pval, NULL) == NULL) {
        outerr("%s", SML3_fehlermsg());
        return VG_FALSE;
      }
      iattr.image.zoom_width = iattr.image.zoom_height = (int)iwert;
      iattr.image.zoom_ispercent = VG_TRUE;
    }

    if ((he1 = SML3_hashget(hs1, "ROTATE", sizeof("ROTATE"))) != NULL) {
      pval = (char *)SML3_hashelem_valget(he1, NULL);
      prez = -1;
      if (SML3_formel_rechne(vhash, &iwert, NULL, &prez, pval, NULL) == NULL) {
        outerr("%s", SML3_fehlermsg());
        return VG_FALSE;
      }
      iattr.image.rotate = (int)iwert;
    }

    if ((he1 = SML3_hashget(hs1, "FLIP", sizeof("FLIP"))) != NULL) {
      pval = (char *)SML3_hashelem_valget(he1, NULL);
      if (*pval == 'H') {
        iattr.image.flip = VG_AXE_HORIZONTAL;
      } else if (*pval == 'V') {
        iattr.image.flip = VG_AXE_VERTICAL;
      }
    }

    if ((he1 = SML3_hashget(hs1, "BRIGHT", sizeof("BRIGHT"))) != NULL) {
      pval = (char *)SML3_hashelem_valget(he1, NULL);
      prez = -1;
      if (SML3_formel_rechne(vhash, &iwert, NULL, &prez, pval, NULL) == NULL) {
        outerr("%s", SML3_fehlermsg());
        return VG_FALSE;
      }
      iattr.pixel.brightness = (int)iwert;
    }

    if ((he1 = SML3_hashget(hs1, "OPAQUE", sizeof("OPAQUE"))) != NULL) {
      pval = (char *)SML3_hashelem_valget(he1, NULL);
      prez = -1;
      if (SML3_formel_rechne(vhash, &iwert, NULL, &prez, pval, NULL) == NULL) {
        outerr("%s", SML3_fehlermsg());
        return VG_FALSE;
      }
      iattr.pixel.opaqueness = (int)iwert;
    }

    sndfile = NULL;
    voladj = 100;
    audioflag = SPRITE_AUDIOFLAG_STOP;
    if (irep == 1) {  /* first repetition */
      if ((he1 = SML3_hashget(hs1, "SOUND", sizeof("SOUND"))) != NULL) {
        sndfile = (char *)SML3_hashelem_valget(he1, NULL);
      }

      if ((he1 = SML3_hashget(hs1, "SNDVOL", sizeof("SNDVOL"))) != NULL) {
        pval = (char *)SML3_hashelem_valget(he1, NULL);
        prez = -1;
        if (SML3_formel_rechne(vhash, &iwert, NULL, &prez, pval, NULL) == NULL) {
          outerr("%s", SML3_fehlermsg());
          return VG_FALSE;
        }
        voladj = (int)iwert;
      }

      if ((he1 = SML3_hashget(hs1, "SNDFLAG", sizeof("SNDFLAG"))) != NULL) {
        pval = (char *)SML3_hashelem_valget(he1, NULL);
        if (strcmp(pval, "WAIT") == 0) {
          audioflag = SPRITE_AUDIOFLAG_WAIT;
        } else if (strcmp(pval, "LOOPING") == 0) {
          audioflag = SPRITE_AUDIOFLAG_LOOPING;
        }
      }
    }

    correct_filenames(spre->dirname, imgfile, &rfileimg, sndfile, &rfilesnd);
    sprite_addelem(spre, anz_nloop, rfileimg, &iattr, rfilesnd, voladj, audioflag);
  }

  SML3_formel_ebene_free(&vhash);
  SML3_hashclear(hs1);
  return VG_TRUE;
} /* Ende hash_in_spriteelem */


/* add sprite-element */
static void
sprite_addelem(struct vgi_sprite *spre, int anz_nloop, const char *imgfile, struct VG_ImagecopyAttr *iattr, const char *sndfile, int voladj, int audioflag)
{
  int ielm;

  if (spre == NULL) { return; }

  if (spre->e == NULL) {
    spre->anz_elem = 1;
    spre->e = SML3_malloc(sizeof(*spre->e));
  } else {
    spre->anz_elem++;
    spre->e = SML3_realloc(spre->e, spre->anz_elem * sizeof(*spre->e));
  }
  ielm = spre->anz_elem - 1;

  spre->e[ielm].anz_nloop = anz_nloop;
  if (spre->e[ielm].anz_nloop < 0) { spre->e[ielm].anz_nloop = 0; }

  spre->e[ielm].audc = 0;

  /* insert image */
  spre->e[ielm].img = NULL;
  if (imgfile != NULL && *imgfile != '\0') {
    char sha1buf[40 + 1];
    struct SML3_hashelem *he1;
    get_cksum(sha1buf, imgfile);
    he1 = SML3_hashget(spre->himg, sha1buf, strlen(sha1buf) + 1);
    if (he1 == NULL) {  /* load image */
      spre->e[ielm].img = vg4_image_load_nolist(imgfile);
      if (spre->e[ielm].img == NULL) {
        pwarn("skipping sprite-element: image %s not loaded\n", imgfile);
        spre->anz_elem--;
        return;
      }
      he1 = SML3_hashset(spre->himg, sha1buf, strlen(sha1buf) + 1);
      SML3_hashelem_valset(he1, spre->e[ielm].img, 0);
    }
    spre->e[ielm].img = (struct VG_Image *)SML3_hashelem_valget(he1, NULL);
  }

  /* insert image-attributes */
  VG_IMAGECOPY_ATTR_DEFAULT(&spre->e[ielm].iattr);
  if (iattr != NULL) { spre->e[ielm].iattr = *iattr; }

  /* insert audio */
  if (sndfile != NULL && *sndfile != '\0') {
    char *endg = strrchr(sndfile, '.');
    if (endg == NULL) { endg = ""; }
    if (strcasecmp(endg, ".wav") == 0 || strcasecmp(endg, ".wave") == 0
        || strcasecmp(endg, ".fla") == 0 || strcasecmp(endg, ".flac") == 0
       ) {
      char sha1buf[40 + 1];
      struct SML3_hashelem *he1;
      get_cksum(sha1buf, sndfile);
      he1 = SML3_hashget(spre->hsnd, sha1buf, strlen(sha1buf) + 1);
      if (he1 == NULL) {
        int audc = vg4_audio_load_nolist(sndfile, voladj, VG_AUDIO_VOLUME_SOUND);
        if (audc == 0) {
          pwarn("skipping sprite-element\n");
          pwarn("skipping sprite-element: sound %s not loaded\n", sndfile);
          spre->anz_elem--;
          return;
        }
        he1 = SML3_hashset(spre->hsnd, sha1buf, strlen(sha1buf) + 1);
        SML3_hashelem_valset(he1, (const void *)(size_t)audc, 0);
      }
      spre->e[ielm].audc = (int)(size_t)SML3_hashelem_valget(he1, NULL);
      spre->e[ielm].audioflag = audioflag;
    }
  }
} /* Ende sprite_addelem */


/* check for sounds with SPRITE_AUDIOFLAG_WAIT */
static VG_BOOL
snd_isplaying(struct vgi_sprite *spre)
{
  int ielm;

  if (spre == NULL) { return VG_FALSE; }

  /* if networking return false */
  if (vg4data.lists.nw.nw_seed > 0) { return VG_FALSE; }

  /* check for playing SPRITE_AUDIOFLAG_WAIT */
  for (ielm = 0; ielm < spre->anz_elem; ielm++) {
    if (spre->e[ielm].audc > 0) {
      if (vg4->audio->is_playing(spre->e[ielm].audc, NULL)) {
        if (spre->e[ielm].audioflag == SPRITE_AUDIOFLAG_WAIT) { return VG_TRUE; }
      }
    }
  }

  return VG_FALSE;
} /* Ende snd_isplaying */


/* stop all sounds, evtl. except SPRITE_AUDIOFLAG_LOOPING */
static void
snd_stop(struct vgi_sprite *spre, VG_BOOL stoplooping)
{
  int ielm;

  if (spre == NULL) { return; }

  for (ielm = 0; ielm < spre->anz_elem; ielm++) {
    if (spre->e[ielm].audc > 0) {
      if (stoplooping || spre->e[ielm].audioflag != SPRITE_AUDIOFLAG_LOOPING) { vg4->audio->stop(spre->e[ielm].audc, VG_FALSE); }
    }
  }
} /* Ende snd_stop */


/* sprite_load:
 * load sprite from file
 * @param filename  filename
 * @return  sprite or NULL = error
 *
 * sprite-file:
 *   comment: after '#'
 *   1.line:  "[SPRITE" [<parameters>] "]"
 *            parameters:
 *              NLOOP=<number: default number of game-loops for each element>
 *              LIFETIME=<number: cycles, how often repeat the sprite before ending, or 0 = infinite>
 *   following blocks of lines (for each element: each key at one line, following keys indented)
 *     selection from:
 *     - REPEAT: <number or formula: number of repetitions, sets FRML_REPEAT (from 1 to REPEAT) and FRML_REPEATMAX (to REPEAT)>
 *     within a REPEAT (if present) following keys:
 *       - NLOOP: <number or formula: number of game-loops for this element, or 0 = use default NLOOP number>
 *       - IMAGE: <filename (with path), relative to the path of the sprite file or from the current directory when starting with '/': image>
 *       - ZOOM: <number of formula: zoom factor of the image in percent (100 = default)>
 *       - ROTATE: <number or formula: rotating degrees of the image (0 to 359)>
 *       - FLIP: <string: flipping axe of the image, "V"=vertical, "H"=horizontal>
 *       - BRIGHT: <number or formula: brightness of the image (0=dark to VG_MAX_BRIGHTNESS, 100=default)>
 *       - OPAQUE: <number or formula: transparency of the image (0=transparent to 100=default)>
 *       - SOUND: <filename (with path), relative to the path of the sprite file or from the current directory when starting with '/': audio file to play (only wave or flac)>
 *               (will be played only once within a REPEAT)
 *       - SNDVOL: <number or formula: volume of the sound (0 to 255, 100 = default)>
 *       - SNDFLAG: <string: "STOP" = play once, but at most until sprite cycle ends (default),
 *                          "WAIT" = play once and wait for end before sprite cycle ends,
 *                          "LOOPING" = play looping>
 */
static struct VG_Sprite *
sprite_load(const char *filename)
{
  return sprite_load_doit(filename, VG_TRUE);
} /* Ende sprite_load */


/* sprite_load_doit: do action */
static struct VG_Sprite *
sprite_load_doit(const char *filename, VG_BOOL intolist)
{
  struct VG_Sprite *sprt, **sprtp;
  FILE *ffp;
  const char *kptr;
  char buf[1024], *pt1;
  struct SML3_hash *hs1;
  struct SML3_hashelem *he1;

  if (filename == NULL || *filename == '\0') { outerr("loading sprite: no filename"); return NULL; }

  if ((ffp = fopen(filename, "r")) == NULL) { outerr("loading sprite: fopen(%s): %s", filename, strerror(errno)); return NULL; }

  /* create sprite */
  sprt = SML3_calloc(1, sizeof(*sprt));
  sprt->prev = sprt->next = NULL;
  sprt->spre.fname = SML3_strdup(filename);
  kptr = strrchr(filename, '/');
  if (kptr != NULL) {
    snprintf(sprt->spre.dirname, sizeof(sprt->spre.dirname), "%.*s", (int)(kptr - filename), filename);
  } else {
    snprintf(sprt->spre.dirname, sizeof(sprt->spre.dirname), ".");
  }
  sprt->spre.himg = SML3_hashnew(NULL, 0);
  sprt->spre.hsnd = SML3_hashnew(NULL, 0);
  sprt->spre.lifetime = 0;
  sprt->spre.dfl_nloop = 1;

  hs1 = SML3_hashnew(NULL, 0);

  /* read header (1.line) */

  for (*buf = '\0';;) {
    if (fgets(buf, sizeof(buf), ffp) == NULL) { *buf = '\0'; break; }
    if ((pt1 = strchr(buf, '#')) != NULL) { *pt1 = '\0'; }
    SML3_schnipp(buf, VGI_WSPACE, 3);
    if (*buf != '\0') { break; }
  }
  if (strncmp(buf, "[SPRITE", 7) != 0 || buf[strlen(buf) - 1] != ']') {
    outerr("reading sprite \"%s\": no Sprite-Header found", filename);
    fclose(ffp);
    vg4->sprite->destroy(sprt);
    SML3_hashfree(&hs1);
    return NULL;
  }
  buf[strlen(buf) - 1] = '\0';
  line_in_hash(hs1, buf + 7, VG_TRUE);

  if ((he1 = SML3_hashget(hs1, "NLOOP", sizeof("NLOOP"))) != NULL) {
    sprt->spre.dfl_nloop = atoi((char *)SML3_hashelem_valget(he1, NULL));
  }
  if (sprt->spre.dfl_nloop < 1) { sprt->spre.dfl_nloop = 1; }

  if ((he1 = SML3_hashget(hs1, "LIFETIME", sizeof("LIFETIME"))) != NULL) {
    sprt->spre.lifetime = atoi((char *)SML3_hashelem_valget(he1, NULL));
  }
  if (sprt->spre.lifetime < 1) { sprt->spre.lifetime = 0; }

  SML3_hashclear(hs1);

  /* read element line-blocks */

  for (;;) {
    if (fgets(buf, sizeof(buf), ffp) == NULL) { break; }
    if ((pt1 = strchr(buf, '#')) != NULL) { *pt1 = '\0'; }
    SML3_schnipp(buf, VGI_WSPACE, 2);
    if (*buf == '\0') { continue; }

    if (*buf != ' ' && *buf != '\t') {  /* new block */
      /* insert actual block */
      if (SML3_hashlist(hs1, NULL, 0) != NULL) {
        if (!hash_in_spriteelem(&sprt->spre, hs1)) {
          fclose(ffp);
          vg4->sprite->destroy(sprt);
          SML3_hashfree(&hs1);
          return NULL;
        }
      }
    }

    /* insert block-line */
    line_in_hash(hs1, buf, VG_FALSE);
  }
  fclose(ffp);

  /* insert remaining block */
  if (SML3_hashlist(hs1, NULL, 0) != NULL) {
    if (!hash_in_spriteelem(&sprt->spre, hs1)) {
      vg4->sprite->destroy(sprt);
      SML3_hashfree(&hs1);
      return NULL;
    }
  }

  SML3_hashfree(&hs1);

  /* insert sprite into list */
  if (intolist) {
    for (sprtp = &vg4data.lists.sprite.list; *sprtp != NULL; sprtp = &(*sprtp)->next) { sprt->prev = *sprtp; }
    *sprtp = sprt;
  }

  return sprt;
} /* Ende sprite_load_doit */


/* sprite_destroy:
 * destroy sprite
 * @param sprt  sprite
 */
static void
sprite_destroy(struct VG_Sprite *sprt)
{
  int audc;
  struct VG_Image *imgp;
  struct SML3_hashelem *he1;

  if (sprt == NULL) { return; }

  if (sprt->spre.fname != NULL) { free(sprt->spre.fname); }

  for (he1 = SML3_hashlist(sprt->spre.himg, NULL, 0); he1 != NULL; he1 = SML3_hashlist(sprt->spre.himg, he1, 0)) {
    imgp = (struct VG_Image *)SML3_hashelem_valget(he1, NULL);
    if (imgp != NULL) { vg4->image->destroy(imgp); }
  }
  SML3_hashfree(&sprt->spre.himg);

  for (he1 = SML3_hashlist(sprt->spre.hsnd, NULL, 0); he1 != NULL; he1 = SML3_hashlist(sprt->spre.hsnd, he1, 0)) {
    audc = (int)(size_t)SML3_hashelem_valget(he1, NULL);
    if (audc > 0) { vg4->audio->unload(audc); }
  }
  SML3_hashfree(&sprt->spre.hsnd);

  if (sprt->spre.e != NULL) { free(sprt->spre.e); }

  if (sprt->prev != NULL) { sprt->prev->next = sprt->next; }
  if (sprt->next != NULL) { sprt->next->prev = sprt->prev; }

  free(sprt);
} /* Ende sprite_destroy */


/* sprite_destroyall:
 * destroy all sprites
 */
static void
sprite_destroyall(void)
{
  struct VG_Sprite *sprt0, *sprt1;

  if (vg4data.lists.sprite.list == NULL) { return; }

  for (sprt0 = vg4data.lists.sprite.list->next; sprt0 != NULL; sprt0 = sprt1) {
    if (sprt0->prev != NULL) { sprt0->prev->next = sprt0->next; }
    if (sprt0->next != NULL) { sprt0->next->prev = sprt0->prev; }
    sprt1 = sprt0->next;
    sprt0->prev = sprt0->next = NULL;
    vg4->sprite->destroy(sprt0);
  }
  vg4data.lists.sprite.list->prev = vg4data.lists.sprite.list->next = NULL;
} /* Ende sprite_destroyall */


/* sprite_getname:
 * get filename of the sprite
 * @param sprt  sprite
 * @return  filename
 */
static const char *
sprite_getname(const struct VG_Sprite *sprt)
{
  if (sprt == NULL || sprt->spre.fname == NULL) { return ""; }

  return sprt->spre.fname;
} /* Ende sprite_getname */


/* sprite_get_blocks:
 * return number of blocks in a sprite
 * @param sprt  sprite
 * @return  number of blocks
 */
static int
sprite_get_blocks(const struct VG_Sprite *sprt)
{
  if (sprt == NULL) { return 0; }
  return sprt->spre.anz_elem;
} /* Ende sprite_get_blocks */


/* sprite_go_block:
 * go to a block in a sprite
 * @param sprt  sprite
 * @param blk   block-number (from 1 to vg4->sprite->get_blocks())
 */
static void
sprite_go_block(struct VG_Sprite *sprt, int elem)
{
  if (sprt == NULL) { return; }
  vg4->sprite->rewind(sprt);
  if (elem < 1 || elem > sprt->spre.anz_elem) { elem = 1; }
  sprt->spre.mom_elem = elem - 1;
} /* Ende sprite_go_block */


/* sprite_rewind:
 * rewind sprite
 * @param sprt  sprite
 */
static void
sprite_rewind(struct VG_Sprite *sprt)
{
  int audc;
  struct SML3_hashelem *he1;

  if (sprt == NULL) { return; }

  for (he1 = SML3_hashlist(sprt->spre.hsnd, NULL, 0); he1 != NULL; he1 = SML3_hashlist(sprt->spre.hsnd, he1, 0)) {
    audc = (int)(size_t)SML3_hashelem_valget(he1, NULL);
    if (audc > 0) { vg4->audio->stop(audc, VG_FALSE); }
  }

  sprt->spre.mom_life = 0;
  sprt->spre.mom_nloop = 0;
  sprt->spre.mom_elem = 0;
} /* Ende sprite_rewind */


/* sprite_imagesize:
 * calculate width and height of a sprite
 * @param sprt     sprite
 * @param bflag    calculate width and height from:
 *                   "min" = the smallest width and height
 *                   "max" = the greatest width and height
 *                   "avg" = the average width and height
 *                   (else: from the first image)
 * @param width    for returning width in pixels, or NULL
 * @param height   for returning height in pixels, or NULL
 */
static void
sprite_imagesize(const struct VG_Sprite *sprt, const char *bflag, int *width, int *height)
{
  int iflag, mom_elem, anzelm, w0, h0, w1, h1;

  if (width != NULL) { *width = 0; }
  if (height != NULL) { *height = 0; }

  if (sprt == NULL) { return; }

  iflag = 0;
  if (bflag != NULL) {
    if (strcmp(bflag, "min") == 0) {
      iflag = 1;
    } else if (strcmp(bflag, "max") == 0) {
      iflag = 2;
    } else if (strcmp(bflag, "avg") == 0) {
      iflag = 3;
    }
  }

  anzelm = 0;
  w0 = h0 = -1;
  for (mom_elem = 0; mom_elem < sprt->spre.anz_elem; mom_elem++) {
    if (sprt->spre.e[mom_elem].img == NULL) { continue; }

    anzelm++;
    vg4->image->getsize(sprt->spre.e[mom_elem].img, &sprt->spre.e[mom_elem].iattr.image, &w1, &h1);

    if (iflag == 0) {  /* first image */
      w0 = w1;
      h0 = h1;
      break;
    } else if (iflag == 1) {  /* smallest size */
      if (w0 < 0 || w1 < w0) { w0 = w1; }
      if (h0 < 0 || h1 < h0) { h0 = h1; }
    } else if (iflag == 2) {  /* greatest size */
      if (w0 < 0 || w1 > w0) { w0 = w1; }
      if (h0 < 0 || h1 > h0) { h0 = h1; }
    } else if (iflag == 3) {  /* average */
      if (w0 < 0) { w0 = h0 = 0; }
      w0 += w1;
      h0 += h1;
    }
  }

  if (anzelm > 0) {
    if (iflag == 3) { w0 /= anzelm; h0 /= anzelm; }
  } else {
    w0 = h0 = 0;
  }

  if (width != NULL) { *width = w0; }
  if (height != NULL) { *height = h0; }
} /* Ende sprite_imagesize */


/* sprite_has_next:
 * test sprite if there is a next element
 * @param sprt  sprite
 * @return  VG_TRUE = yes or VG_FALSE = no
 */
static VG_BOOL
sprite_has_next(struct VG_Sprite *sprt)
{
  if (sprt == NULL) { return VG_FALSE; }

  /* sprite had ended? */
  if (sprt->spre.lifetime > 0 && sprt->spre.mom_life >= sprt->spre.lifetime) { return VG_FALSE; }
  if (sprt->spre.anz_elem == 0) { return VG_FALSE; }

  if (sprt->spre.mom_elem >= sprt->spre.anz_elem) {
    /* sprite cycle ended */
    if (snd_isplaying(&sprt->spre)) { return VG_TRUE; }
    if (sprt->spre.lifetime > 0 && sprt->spre.mom_life >= sprt->spre.lifetime - 1) {
      return VG_FALSE;
    }
  } else {
    int anz_nloop = sprt->spre.e[sprt->spre.mom_elem].anz_nloop;
    if (anz_nloop <= 0) { anz_nloop = sprt->spre.dfl_nloop; }
    if (sprt->spre.mom_nloop >= anz_nloop) {  /* element done */
      if (sprt->spre.mom_elem >= sprt->spre.anz_elem - 1) {  /* next sprite cycle */
        if (snd_isplaying(&sprt->spre)) { return VG_TRUE; }
        if (sprt->spre.lifetime > 0 && sprt->spre.mom_life >= sprt->spre.lifetime - 1) {
          return VG_FALSE;
        }
      }
    }
  }

  return VG_TRUE;
} /* Ende sprite_has_next */


/* sprite_next:
 * set sprite to next element,
 * return image with attributes and play sound
 * @param sprt     sprite
 * @param imgp     for returning image,
 *                 its value may be NULL if element has no image
 * @param iattr    for returning image-copy attributes
 * @return  VG_TRUE = OK or VG_FALSE = sprite ended
 */
static VG_BOOL
sprite_next(struct VG_Sprite *sprt, struct VG_Image **imgp, struct VG_ImagecopyAttr *iattr)
{
  if (imgp != NULL) { *imgp = NULL; }
  if (iattr != NULL) { VG_IMAGECOPY_ATTR_DEFAULT(iattr); }
  if (sprt == NULL) { return VG_FALSE; }

  /* sprite had ended? */
  if (sprt->spre.lifetime > 0 && sprt->spre.mom_life >= sprt->spre.lifetime) { return VG_FALSE; }
  if (sprt->spre.anz_elem == 0) { return VG_FALSE; }

  if (sprt->spre.mom_elem >= sprt->spre.anz_elem) {
    /* sprite cycle ended, but waits for sound ending */
    if (snd_isplaying(&sprt->spre)) {  /* wait still for sound ending */
      if (imgp != NULL) { *imgp = sprt->spre.e[sprt->spre.anz_elem - 1].img; }
      if (iattr != NULL) { *iattr = sprt->spre.e[sprt->spre.anz_elem - 1].iattr; }
      return VG_TRUE;
    }
    if (sprt->spre.lifetime > 0 && ++sprt->spre.mom_life >= sprt->spre.lifetime) {
      snd_stop(&sprt->spre, VG_TRUE);
      return VG_FALSE;
    } else {
      snd_stop(&sprt->spre, VG_FALSE);
    }
    sprt->spre.mom_elem = 0;
    sprt->spre.mom_nloop = 1;
  } else {
    int anz_nloop = sprt->spre.e[sprt->spre.mom_elem].anz_nloop;
    if (anz_nloop <= 0) { anz_nloop = sprt->spre.dfl_nloop; }
    if (++sprt->spre.mom_nloop > anz_nloop) {  /* element done */
      if (++sprt->spre.mom_elem >= sprt->spre.anz_elem) {  /* next sprite cycle */
        if (snd_isplaying(&sprt->spre)) {  /* wait for sound ending */
          if (imgp != NULL) { *imgp = sprt->spre.e[sprt->spre.anz_elem - 1].img; }
          if (iattr != NULL) { *iattr = sprt->spre.e[sprt->spre.anz_elem - 1].iattr; }
          return VG_TRUE;
        }
        if (sprt->spre.lifetime > 0 && ++sprt->spre.mom_life >= sprt->spre.lifetime) {
          snd_stop(&sprt->spre, VG_TRUE);
          return VG_FALSE;
        } else {
          snd_stop(&sprt->spre, VG_FALSE);
        }
        sprt->spre.mom_elem = 0;
      }
      sprt->spre.mom_nloop = 1;
    }
  }

  if (imgp != NULL) { *imgp = sprt->spre.e[sprt->spre.mom_elem].img; }
  if (iattr != NULL) { *iattr = sprt->spre.e[sprt->spre.mom_elem].iattr; }

  if (sprt->spre.mom_nloop == 1) {  /* first game-loop of element */
    if (sprt->spre.e[sprt->spre.mom_elem].audc > 0) {  /* play audio */
      vg4->audio->play(sprt->spre.e[sprt->spre.mom_elem].audc, 
                       (sprt->spre.e[sprt->spre.mom_elem].audioflag == SPRITE_AUDIOFLAG_LOOPING ? VG_TRUE : VG_FALSE),
                       VG_FALSE);
    }
  }

  return VG_TRUE;
} /* Ende sprite_next */


/* sprite_current:
 * return image with attributes of current sprite element (but don't play sound again)
 * @param sprt     sprite
 * @param imgp     for returning image,
 *                 its value may be NULL if element has no image
 * @param iattr    for returning image-copy attributes
 * @return  VG_TRUE = OK or VG_FALSE = sprite ended
 */
static VG_BOOL
sprite_current(struct VG_Sprite *sprt, struct VG_Image **imgp, struct VG_ImagecopyAttr *iattr)
{
  if (imgp != NULL) { *imgp = NULL; }
  if (iattr != NULL) { VG_IMAGECOPY_ATTR_DEFAULT(iattr); }
  if (sprt == NULL) { return VG_FALSE; }

  /* sprite had ended? */
  if (sprt->spre.lifetime > 0 && sprt->spre.mom_life >= sprt->spre.lifetime) { return VG_FALSE; }
  if (sprt->spre.anz_elem == 0) { return VG_FALSE; }

  if (sprt->spre.mom_nloop > 0) {
    int mom_elem = sprt->spre.mom_elem;
    if (mom_elem >= sprt->spre.anz_elem) { mom_elem = sprt->spre.anz_elem - 1; }
    if (imgp != NULL) { *imgp = sprt->spre.e[mom_elem].img; }
    if (iattr != NULL) { *iattr = sprt->spre.e[mom_elem].iattr; }
  }

  return VG_TRUE;
} /* Ende sprite_current */


/* sprite_dump:
 * dump sprite entries
 * @param outfp  filepointer to dump to, or NULL = stdout
 */
static void
sprite_dump(FILE *outfp)
{
  struct VG_Sprite *sprt;
  int ielm, i1;
  const char *namptr;

  if (outfp == NULL) { outfp = stdout; }

  fprintf(outfp, "\nDump of sprite entries\n");
  fprintf(outfp, "======================\n\n");

  if (vg4data.lists.sprite.list == NULL) { return; }

  for (sprt = vg4data.lists.sprite.list->next; sprt != NULL; sprt = sprt->next) {
    fprintf(outfp, "- %s\n", sprt->spre.fname);
    fprintf(outfp, "  file-path=%s\n", sprt->spre.dirname);
    fprintf(outfp, "  lifetime=%d, default-game-loop-number=%d\n", sprt->spre.lifetime, sprt->spre.dfl_nloop);
    fprintf(outfp, "  sprite-elements=%d:\n", sprt->spre.anz_elem);
    for (ielm = 0; ielm < sprt->spre.anz_elem; ielm++) {
      fprintf(outfp, "  - [%d]\n", ielm + 1);
      fprintf(outfp, "    game-loop-number=%d\n",
              (sprt->spre.e[ielm].anz_nloop <= 0 ? sprt->spre.dfl_nloop : sprt->spre.e[ielm].anz_nloop));
      namptr = vg4->image->getname(sprt->spre.e[ielm].img);
      fprintf(outfp, "    image=%s\n", namptr);
      if (namptr != NULL) {
        fprintf(outfp, "    image-attributes:\n");
        fprintf(outfp, "    - opaqueness=%d\n", sprt->spre.e[ielm].iattr.pixel.opaqueness);
        fprintf(outfp, "    - brightness=%d\n", sprt->spre.e[ielm].iattr.pixel.brightness);
        fprintf(outfp, "    - zoom=%d%%\n", sprt->spre.e[ielm].iattr.image.zoom_width);
        fprintf(outfp, "    - rotate=%d\n", sprt->spre.e[ielm].iattr.image.rotate);
        i1 = sprt->spre.e[ielm].iattr.image.flip;
        fprintf(outfp, "    - flip=%s\n", (i1 == VG_AXE_HORIZONTAL ? "horizontal" : (i1 == VG_AXE_VERTICAL ? "vertical" : "[no]")));
      }
      namptr = vg4->audio->getname(sprt->spre.e[ielm].audc);
      fprintf(outfp, "    sound=%s\n", namptr);
      if (namptr != NULL) {
        i1 = sprt->spre.e[ielm].audioflag;
        fprintf(outfp, "    soundflag=%s\n", (i1 == SPRITE_AUDIOFLAG_WAIT ? "WAIT" : (i1 == SPRITE_AUDIOFLAG_LOOPING ? "LOOPING" : "STOP")));
      }
    }
    fprintf(outfp, "\n");
  }
} /* Ende sprite_dump */
