/* 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 <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include "film.h"

#define MAX_NAMESIZE  48
#define FRML_PERCENT  "percent"

void init_film(void);
void dest_film(void);

static VG_BOOL film_play(const char *, const struct VG_Rect *, VG_BOOL *, struct VG_Hash *);


struct film_checkpoint {
  struct VG_Hash *hvar;
  char dirname[128];
  int res_w, res_h;
  int anz;
  struct {
    VG_BOOL visited;
    char name[MAX_NAMESIZE];
    int floop;
    struct {
      char name[MAX_NAMESIZE];
      int floop;
    } t;
  } *e;
};

struct story_read {
  struct story_read *next;
  struct SML3_hash *hparam;
  enum { STRYRD_MUSIC, STRYRD_SOUND, STRYRD_DRAW } type;
  union {
    struct {
      ;
    } music;
    struct {
      ;
    } sound;
    struct {
      int posalign;
      int flip;
    } draw;
  } u;
};

static void line_in_hash(struct SML3_hash *, const char *, VG_BOOL, struct VG_Hash *);
static void * do_alloc(void *, size_t, size_t, size_t);
static void free_ckpoint(struct film_checkpoint *);
static void free_flm(struct vgi_film *);
static void free_stryrd(struct story_read **);
static int cmp_stories(const void *, const void *);
static int cmp_musics(const void *, const void *);
static int cmp_sounds(const void *, const void *);
static int settime_ckpoint(struct film_checkpoint *, const char *);
static VG_BOOL read_film(struct vgi_film *, struct film_checkpoint *, const char *, struct VG_Hash *);
static VG_BOOL get_stories(struct vgi_film *, const struct film_checkpoint *);
static VG_BOOL read_story(struct vgi_film *, const struct film_checkpoint *, const char *);
static void add_music(FILE *, const char *, struct story_read **, struct VG_Hash *);
static void add_sound(FILE *, const char *, struct story_read **, struct VG_Hash *);
static void add_draw(FILE *, const char *, struct story_read **, struct VG_Hash *);
static VG_BOOL add_check(const char *, int *, const char *, struct vgi_film *, const struct film_checkpoint *, struct story_read **);
static void fadeout(struct VG_Image *, struct VG_Rect *, int, int);
static VG_BOOL playit(struct vgi_film *, const struct VG_Rect *, VG_BOOL *, int, int);
static void dumpit(struct film_checkpoint *, struct vgi_film *);


/* set functions */
void
init_film(void)
{
  vg4->film->play = film_play;
} /* Ende init_film */

void
dest_film(void)
{
  ;
} /* Ende dest_film */


/* 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 VG_Hash *hvar)
{
  struct SML3_gummi gm1 = SML3_GUM_INITIALIZER, gm2 = 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", &gm1)) != NULL) {
      pt1 = strchr(tstring, '='); 
      if (pt1 != NULL && *tstring != '\0' && *pt1 != '\0') {
        *pt1++ = '\0';
        if (hvar != NULL) { vg4_intern_str_with_var(&gm2, pt1, hvar, NULL); pt1 = SML3_gumgetval(&gm2); }
        SML3_hashelem_valset(SML3_hashset(hs1, tstring, strlen(tstring) + 1), pt1, strlen(pt1) + 1);
      }
    }
  } else {
    SML3_gumcpy(&gm1, 0, zeile);
    tstring = SML3_gumgetval(&gm1);
    pt1 = strpbrk(tstring, "=:");
    if (pt1 != NULL) {
      *pt1++ = '\0';
      SML3_schnipp(tstring, " \t", 3);
      SML3_schnipp(pt1, VGI_WSPACE, 3);
      if (*tstring != '\0' && *pt1 != '\0') {
        if (hvar != NULL) { vg4_intern_str_with_var(&gm2, pt1, hvar, NULL); pt1 = SML3_gumgetval(&gm2); }
        SML3_hashelem_valset(SML3_hashset(hs1, tstring, strlen(tstring) + 1), pt1, strlen(pt1) + 1);
      }
    }
  }

  SML3_gumdest(&gm1);
  SML3_gumdest(&gm2);
} /* Ende line_in_hash */


/* allocate new element */
static void *
do_alloc(void *vptr, size_t vsize, size_t oanz, size_t nanz)
{
  if (vsize == 0 || nanz == 0) { return vptr; }
  if (oanz == 0) {
    vptr = SML3_malloc(nanz * vsize);
  } else {
    vptr = SML3_realloc(vptr, (oanz + nanz) * vsize);
  }
  memset((char *)vptr + oanz * vsize, 0, nanz * vsize);

  return vptr;
} /* Ende do_alloc */


/* free checkpoint-struct */
static void
free_ckpoint(struct film_checkpoint *ckpoint)
{
  if (ckpoint == NULL) { return; }

  if (ckpoint->e != NULL) { free(ckpoint->e); }

  memset(ckpoint, 0, sizeof(*ckpoint));
} /* Ende free_ckpoint */


/* free film-struct */
static void
free_flm(struct vgi_film *flm)
{
  int ipos;

  if (flm == NULL) { return; }

  if (flm->wimg != NULL) { vg4->image->destroy(flm->wimg); }

  for (ipos = 0; ipos < flm->loaded.anz_img; ipos++) {
    vg4_intern_free_imgcnt(&flm->loaded.imgcntp[ipos]);
  }
  if (flm->loaded.imgcntp != NULL) { free(flm->loaded.imgcntp); }

  for (ipos = 0; ipos < flm->loaded.anz_audio; ipos++) {
    vg4->audio->unload(flm->loaded.audcp[ipos]);
  }
  if (flm->loaded.audcp != NULL) { free(flm->loaded.audcp); }

  if (flm->music.mus != NULL) { free(flm->music.mus); }

  for (ipos = 0; ipos < flm->story.anz; ipos++) {
    if (flm->story.e[ipos].name != NULL) { free(flm->story.e[ipos].name); }
    if (flm->story.e[ipos].pic != NULL) {
      int i1;
      for (i1 = 0; i1 < flm->story.e[ipos].anz_pic; i1++) {
        if (flm->story.e[ipos].pic[i1] != NULL) { free(flm->story.e[ipos].pic[i1]); }
      }
      free(flm->story.e[ipos].anz_picp);
      free(flm->story.e[ipos].pic);
    }
    if (flm->story.e[ipos].snd != NULL) { free(flm->story.e[ipos].snd); }
  }
  if (flm->story.e != NULL) { free(flm->story.e); }

  memset(flm, 0, sizeof(*flm));
} /* Ende free_flm */


/* free story-elements */
static void
free_stryrd(struct story_read **stryrdp)
{
  struct story_read *stryrd, *enext;

  if (stryrdp == NULL) { return; }

  for (stryrd = *stryrdp; stryrd != NULL; stryrd = enext) {
    if (stryrd->hparam != NULL) { SML3_hashfree(&stryrd->hparam); }
    enext = stryrd->next;
    free(stryrd);
  }
  *stryrdp = NULL;
} /* Ende free_stryrd */


/* compare function for qsort() for stories */
static int
cmp_stories(const void *v1, const void *v2)
{   
  struct story_elem *e1 = (struct story_elem *)v1;
  struct story_elem *e2 = (struct story_elem *)v2;

  if (e1->drawlevel < e2->drawlevel) { return -1; }  
  if (e2->drawlevel < e1->drawlevel) { return 1; }  
  return 0;
} /* Ende cmp_stories */


/* compare function for qsort() for musics */
static int
cmp_musics(const void *v1, const void *v2)
{   
  struct vgi_film_music *m1 = (struct vgi_film_music *)v1;
  struct vgi_film_music *m2 = (struct vgi_film_music *)v2;

  if (m1->pic_no < m2->pic_no) { return -1; }  
  if (m2->pic_no < m1->pic_no) { return 1; }  
  return 0;
} /* Ende cmp_musics */


/* compare function for qsort() for sounds */
static int
cmp_sounds(const void *v1, const void *v2)
{   
  struct vgi_film_story_sound *s1 = (struct vgi_film_story_sound *)v1;
  struct vgi_film_story_sound *s2 = (struct vgi_film_story_sound *)v2;

  if (s1->pic_no < s2->pic_no) { return -1; }  
  if (s2->pic_no < s1->pic_no) { return 1; }  
  return 0;
} /* Ende cmp_sounds */


/* recursive: make time for each checkpoint absolute */
static int
settime_ckpoint(struct film_checkpoint *ckpoint, const char *name)
{
  int i1, floop, dret;

  if (ckpoint == NULL || name == NULL) { return -1; }
  if (*name == '\0') { return 0; }

  floop = 0;
  for (i1 = 0; i1 < ckpoint->anz; i1++) {
    if (strcmp(ckpoint->e[i1].name, name) == 0) {
      if (ckpoint->e[i1].visited) { 
        outerr("traversing checkpoints: endless loop (%s)", name);
        return -1;
      }
      floop = ckpoint->e[i1].t.floop;
      ckpoint->e[i1].visited = VG_TRUE;
      dret = settime_ckpoint(ckpoint, ckpoint->e[i1].t.name);
      if (dret < 0) { return -1; }
      floop += dret;
      if (floop < 0) {
        outerr("traversing checkpoints: film-loops=%d negative (%s)", floop, name);
        return -1;
      }
      break;
    }
  }
  if (i1 == ckpoint->anz) {
    outerr("traversing checkpoints: checkpoint not found (%s)", name);
    return -1;
  }

  return floop;
} /* Ende settime_ckpoint */


/* read in film-file "film" and default-variables-file "vars"
 * Format:
 * - vars:
 *   (for each variable a line:) <variable-name>: <variable-default-value>
 * - film:
 *   1.line:  "[FILM" [<parameters>] "]"
 *            parameters:
 *              RESOLUTION=<x>x<y>  # e.g. 320x200, missing: no adjustment to window-size
 *              FLOOP=<number>      # time for a film-loop in milli-seconds, missing: 100
 *              FADEOUT=<1 or 0>    # whether at end fade out window, missing: 0
 *              BGCOLOR=<color>     # background color (0xRRGGBB or "transparent"), missing: transparent
 *   following lines: for each checkpoint:
 *     <checkpoint-name> ":" [<reference-checkpoint> {+|-}] <film-loops>
 *   A checkpoint-name must not begin with a digit and may have letters, digits and underscore
 *   Parameter-values may contain variables: $(<variable>)
 * Comments begin with '#'
 */
static VG_BOOL
read_film(struct vgi_film *flm, struct film_checkpoint *ckpoint, const char *dirname, struct VG_Hash *hvar)
{
  const char *filmname = "film";
  const char *varsname = "vars";
  FILE *ffp;
  char fbuf[512], *pt1, *pt2;
  size_t flen;
  struct SML3_hash *hs1;
  struct SML3_hashelem *he1;
  int i1, i2;

  if (flm == NULL) { return VG_FALSE; }
  memset(flm, 0, sizeof(*flm));

  if (ckpoint == NULL) { return VG_FALSE; }
  memset(ckpoint, 0, sizeof(*ckpoint));

  ckpoint->hvar = hvar;

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

  /* read "vars" */
  snprintf(fbuf, sizeof(fbuf), "%s/%s", dirname, varsname);
  if ((ffp = fopen(fbuf, "r")) != NULL) {
    while (fgets(fbuf, sizeof(fbuf), ffp) != NULL) {
      if ((pt1 = strchr(fbuf, '#')) != NULL) { *pt1 = '\0'; }
      SML3_schnipp(fbuf, VGI_WSPACE, 3);
      if (*fbuf == '\0') { continue; }
      pt1 = strpbrk(fbuf, "=:");
      if (pt1 == NULL) { continue; }
      *pt1++ = '\0';
      SML3_schnipp(fbuf, VGI_SPACE, 3);
      SML3_schnipp(pt1, VGI_SPACE, 3);
      if (*fbuf == '\0') { continue; }
      if (!vg4->hash->isset(ckpoint->hvar, fbuf)) { vg4->hash->setstr(ckpoint->hvar, fbuf, pt1); }
    } 
    fclose(ffp);
  }

  /* open "film" */
  snprintf(fbuf, sizeof(fbuf), "%s/%s", dirname, filmname);
  if ((ffp = fopen(fbuf, "r")) == NULL) {
    outerr("loading film: fopen(%s): %s", fbuf, strerror(errno));
    return VG_FALSE;
  }

  vg4->misc->strcpy(ckpoint->dirname, sizeof(ckpoint->dirname), dirname);

  /* read header (1.line) */

  hs1 = SML3_hashnew(NULL, 0);

  for (*fbuf = '\0';;) {
    if (fgets(fbuf, sizeof(fbuf), ffp) == NULL) { *fbuf = '\0'; break; }
    if ((pt1 = strchr(fbuf, '#')) != NULL) { *pt1 = '\0'; }
    SML3_schnipp(fbuf, VGI_WSPACE, 3);
    if (*fbuf != '\0') { break; }
  }
  if (strncmp(fbuf, "[FILM", 5) != 0 || fbuf[strlen(fbuf) - 1] != ']') {
    outerr("reading film \"%s/%s\": no Film-Header found", dirname, filmname);
    fclose(ffp);
    if (ckpoint->e != NULL) { free(ckpoint->e); ckpoint->e = NULL; }
    SML3_hashfree(&hs1);
    return VG_FALSE;
  }
  fbuf[strlen(fbuf) - 1] = '\0';
  line_in_hash(hs1, fbuf + 5, VG_TRUE, ckpoint->hvar);

  if ((he1 = SML3_hashget(hs1, "RESOLUTION", sizeof("RESOLUTION"))) != NULL) {
    if ((pt1 = (char *)SML3_hashelem_valget(he1, NULL)) != NULL) {
      ckpoint->res_w = atoi(pt1);
      if ((pt1 = strchr(pt1, 'x')) != NULL) {
        ckpoint->res_h = atoi(pt1 + 1);
      } else {
        ckpoint->res_w = ckpoint->res_h = 0;
      }
    }
  }     

  flm->floop = 100;
  if ((he1 = SML3_hashget(hs1, "FLOOP", sizeof("FLOOP"))) != NULL) {
    pt1 = (char *)SML3_hashelem_valget(he1, NULL);
    if (pt1 != NULL && (i1 = atoi(pt1)) > 0) { flm->floop = i1; }
  }

  flm->fadeout = VG_FALSE;
  if ((he1 = SML3_hashget(hs1, "FADEOUT", sizeof("FADEOUT"))) != NULL) {
    pt1 = (char *)SML3_hashelem_valget(he1, NULL);
    if (pt1 != NULL && atoi(pt1) > 0) { flm->fadeout = VG_TRUE; }
  } 

  flm->bgcolor = VG_COLOR_TRANSPARENT;
  if ((he1 = SML3_hashget(hs1, "BGCOLOR", sizeof("BGCOLOR"))) != NULL) {
    pt1 = (char *)SML3_hashelem_valget(he1, NULL);
    if (pt1 != NULL && strcmp(pt1, "transparent") != 0) {
      flm->bgcolor = (int)strtol(pt1, NULL, 16);
    }
  }

  SML3_hashfree(&hs1);

  /* read checkpoints */

  while (fgets(fbuf, sizeof(fbuf), ffp) != NULL) {
    flen = strlen(fbuf);
    if (flen == 0 || fbuf[flen - 1] != '\n') {
      outerr("reading film \"%s/%s\": line too long", dirname, filmname);
      fclose(ffp);
      if (ckpoint->e != NULL) { free(ckpoint->e); ckpoint->e = NULL; }
      return VG_FALSE;
    }
    if ((pt1 = strchr(fbuf, '#')) != NULL) { *pt1 = '\0'; }
    flen = SML3_schnipp(fbuf, VGI_WSPACE, 3);
    if (flen == 0) { continue; }

    if ((pt1 = strchr(fbuf, ':')) == NULL) { continue; }
    *pt1++ = '\0';
    SML3_schnipp(fbuf, VGI_WSPACE, 2);
    SML3_schnipp(pt1, VGI_SPACE, 1);
    if (*fbuf == '\0' || *pt1 == '\0') { continue; }

    ckpoint->e = do_alloc(ckpoint->e, sizeof(*ckpoint->e), ckpoint->anz++, 1);
    vg4->misc->strcpy(ckpoint->e[ckpoint->anz - 1].name, sizeof(ckpoint->e[0].name), fbuf);

    if (*pt1 == '\0' || strchr("-+0123456789", (int)*pt1) == NULL) {  /* reference checkpoint */
      pt2 = pt1 + strcspn(pt1, VGI_SPACE "+-");
      if (pt2 > pt1) {
        int idir = 1;
        vg4->misc->strscpy(ckpoint->e[ckpoint->anz - 1].t.name, sizeof(ckpoint->e[0].t.name), pt1, (size_t)(pt2 - pt1));
        pt1 = pt2 + strspn(pt2, VGI_SPACE);
        if (*pt1 == '-') { idir = -1; pt1++; } else if (*pt1 == '+') { pt1++; }
        pt1 += strspn(pt1, VGI_SPACE);
        if (*pt1 != '\0' && strchr("0123456789", (int)*pt1) != NULL) {
          ckpoint->e[ckpoint->anz - 1].t.floop = idir * atoi(pt1);
        }
      }
    } else {  /* floop */
      ckpoint->e[ckpoint->anz - 1].t.floop = atoi(pt1);
    }
  }

  fclose(ffp);

  /* make time for each checkpoint absolute */
  for (i1 = 0; i1 < ckpoint->anz; i1++) {
    for (i2 = 0; i2 < ckpoint->anz; i2++) { ckpoint->e[i2].visited = VG_FALSE; }
    ckpoint->e[i1].visited = VG_TRUE;
    i2 = settime_ckpoint(ckpoint, ckpoint->e[i1].t.name);
    if (i2 < 0) {
      if (ckpoint->e != NULL) { free(ckpoint->e); ckpoint->e = NULL; }
      return VG_FALSE;
    }
    ckpoint->e[i1].floop = ckpoint->e[i1].t.floop + i2;
    if (ckpoint->e[i1].floop < 0) {
      outerr("traversing checkpoints: film-loops=%d negative (%s)", ckpoint->e[i1].floop, ckpoint->e[i1].name);
      if (ckpoint->e != NULL) { free(ckpoint->e); ckpoint->e = NULL; }
      return VG_FALSE;
    }
  }

  return VG_TRUE;
} /* Ende read_film */


/* find film-stories (<name>.story) and read in them */
static VG_BOOL
get_stories(struct vgi_film *flm, const struct film_checkpoint *ckpoint)
{
  DIR *dirp;
  struct dirent *direntp;
  char storyfile[512];
  size_t slen;

  if (flm == NULL || ckpoint == NULL) { return VG_FALSE; }

  dirp = opendir(ckpoint->dirname);
  if (dirp == NULL) {
    outerr("open film-directory \"%s\": %s", ckpoint->dirname, strerror(errno));
    return VG_FALSE;
  }

  while ((direntp = readdir(dirp)) != NULL) {
    if (strcmp(direntp->d_name, ".") == 0 || strcmp(direntp->d_name, "..") == 0) { continue; }
    slen = strlen(direntp->d_name);
    if (slen > 6 && strncmp(direntp->d_name + slen - 6, ".story", 6) == 0) {
      vg4->misc->strccat(storyfile, sizeof(storyfile), ckpoint->dirname, "/", direntp->d_name, NULL);
      if (!read_story(flm, ckpoint, storyfile)) { closedir(dirp); return VG_FALSE; }
    }
  }

  closedir(dirp);

  /* sort musics according to picture-positions */
  if (flm->music.mus != NULL) {
    qsort(flm->music.mus, flm->music.anz, sizeof(*flm->music.mus), cmp_musics);
  }

  /* sort stories according to drawlevel */
  if (flm->story.e != NULL) { qsort(flm->story.e, flm->story.anz, sizeof(*flm->story.e), cmp_stories); }

  return VG_TRUE;
} /* Ende get_stories */


/* read in film-story
 * Format:
 *   1.line:  "[STORY" [<parameters>] "]"
 *            parameters:
 *              DRAWLEVEL=<value>  # level-number for drawing order (begins with 1 = lowest level), missing: 1
 *            Parameter-values may contain variables: $(<variable>)
 *   following lines may contain:
 *    - CHECK: <name of checkpoint>
 *    - SOUND:
 *        VALID: <boolean test, when to start sound>
 *        FILE: <filename>
 *    - MUSIC:
 *        START: <time-percent>
 *        FILE: <filename>
 *        CRESC: <1|0>
 *        PLAY: <LOOPING|END>
 *    - DRAW:
 *        VALID: <boolean test, whether relevant>
 *        IMAGE|SPRITE|TEXT: <filename>
 *        POSX: <(formula-)x-position>
 *        POSY: <(formula-)y-position>
 *        POSALIGN: <"upper-left"|"lower-left"|"centered"|"upper-right"|"lower-right">  # default: centered
 *        ZOOM: <(formula-)percent-value>
 *        ROTATE: <(formula-)value>
 *        BRIGHT: <(formula-)value>
 *        OPAQUE: <(formula-)value>
 *        FLIP: <"horizontal"|"vertical">
 *   A formula or a boolean test may use the variable "percent",
 *   which is set to the time-percent (1 to 100) between the actual two checkpoints.
 *   Values may contain variables: $(<variable>)
 */
static VG_BOOL
read_story(struct vgi_film *flm, const struct film_checkpoint *ckpoint, const char *filename)
{
  FILE *ffp;
  char fbuf[512], *pt1;
  size_t flen;
  int ckp_idx, i1;
  struct story_read *stryrd;
  struct SML3_hash *hs1;
  struct SML3_hashelem *he1;

  if (flm == NULL || ckpoint == NULL || filename == NULL || *filename == '\0') { return VG_FALSE; }

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

  ckp_idx = -1;
  stryrd = NULL;

  flm->story.e = do_alloc(flm->story.e, sizeof(*flm->story.e), flm->story.anz++, 1);

  flm->story.e[flm->story.anz - 1].name = SML3_strdup(filename);

  /* read header (1.line) */

  hs1 = SML3_hashnew(NULL, 0);

  for (*fbuf = '\0';;) {
    if (fgets(fbuf, sizeof(fbuf), ffp) == NULL) { *fbuf = '\0'; break; }
    if ((pt1 = strchr(fbuf, '#')) != NULL) { *pt1 = '\0'; }
    SML3_schnipp(fbuf, VGI_WSPACE, 3);
    if (*fbuf != '\0') { break; }
  }
  if (strncmp(fbuf, "[STORY", 6) != 0 || fbuf[strlen(fbuf) - 1] != ']') {
    outerr("reading film-story \"%s\": no Film-Story-Header found", filename);
    fclose(ffp);
    SML3_hashfree(&hs1);
    return VG_FALSE;
  }
  fbuf[strlen(fbuf) - 1] = '\0';
  line_in_hash(hs1, fbuf + 6, VG_TRUE, ckpoint->hvar);

  flm->story.e[flm->story.anz - 1].drawlevel = 1;
  if ((he1 = SML3_hashget(hs1, "DRAWLEVEL", sizeof("DRAWLEVEL"))) != NULL) {
    pt1 = (char *)SML3_hashelem_valget(he1, NULL);
    if (pt1 != NULL && (i1 = atoi(pt1)) > 0) { flm->story.e[flm->story.anz - 1].drawlevel = i1; }
  }

  SML3_hashfree(&hs1);

  /* read body */

  while (fgets(fbuf, sizeof(fbuf), ffp) != NULL) {
    flen = strlen(fbuf);
    if (flen == 0 || fbuf[flen - 1] != '\n') {
      outerr("reading film-story \"%s\": line too long", filename);
      fclose(ffp);
      return VG_FALSE;
    }
    if ((pt1 = strchr(fbuf, '#')) != NULL) { *pt1 = '\0'; }
    flen = SML3_schnipp(fbuf, VGI_WSPACE, 3);
    if (flen == 0) { continue; }

    if (strncmp(fbuf, "MUSIC:", 6) == 0) {
      add_music(ffp, fbuf + 6, &stryrd, ckpoint->hvar);
    } else if (strncmp(fbuf, "SOUND:", 6) == 0) {
      add_sound(ffp, fbuf + 6, &stryrd, ckpoint->hvar);
    } else if (strncmp(fbuf, "DRAW:", 6) == 0) {
      add_draw(ffp, fbuf + 6, &stryrd, ckpoint->hvar);
    } else if (strncmp(fbuf, "CHECK:", 6) == 0) {
      if (!add_check(filename, &ckp_idx, fbuf + 6, flm, ckpoint, &stryrd)) { fclose(ffp); return VG_FALSE; }
    }
  }

  fclose(ffp);
  free_stryrd(&stryrd);

  /* sort sounds according to picture-positions */
  if (flm->story.e[flm->story.anz - 1].snd != NULL) {
    int istory = flm->story.anz - 1;
    qsort(flm->story.e[istory].snd, flm->story.e[istory].anz_snd, sizeof(*flm->story.e[istory].snd), cmp_sounds);
  }

  return VG_TRUE;
} /* Ende read_story */


/* add music to story-elements */
static void
add_music(FILE *ffp, const char *rline, struct story_read **stryrdp, struct VG_Hash *hvar)
{
  struct story_read *stryrd;
  char fbuf[512], *pt1;
  long lpos;

  if (ffp == NULL || rline == NULL || stryrdp == NULL) { return; }

  if (*stryrdp == NULL) {
    stryrd = *stryrdp = SML3_calloc(1, sizeof(*stryrd));
  } else {
    struct story_read **pptr;
    for (pptr = &(*stryrdp)->next; *pptr != NULL; pptr = &(*pptr)->next) {;}
    stryrd = *pptr = SML3_calloc(1, sizeof(*stryrd));
  }
  stryrd->type = STRYRD_MUSIC;

  stryrd->hparam = SML3_hashnew(NULL, 0);

  lpos = ftell(ffp);
  while (fgets(fbuf, sizeof(fbuf), ffp) != NULL) {
    SML3_schnipp(fbuf, VGI_WSPACE, 2);
    if ((pt1 = strchr(fbuf, '#')) != NULL) { *pt1 = '\0'; }
    if (*fbuf == '\0' || (*fbuf != ' ' && *fbuf != '\t')) {  /* block ended */
      fseek(ffp, lpos, SEEK_SET);
      break;
    }
    SML3_schnipp(fbuf, VGI_SPACE, 1);

    line_in_hash(stryrd->hparam, fbuf, VG_FALSE, hvar);
  }
} /* Ende add_music */


/* add sound to story-elements */
static void
add_sound(FILE *ffp, const char *rline, struct story_read **stryrdp, struct VG_Hash *hvar)
{
  struct story_read *stryrd;
  char fbuf[512], *pt1;
  long lpos;

  if (ffp == NULL || rline == NULL || stryrdp == NULL) { return; }

  if (*stryrdp == NULL) {
    stryrd = *stryrdp = SML3_calloc(1, sizeof(*stryrd));
  } else {
    struct story_read **pptr;
    for (pptr = &(*stryrdp)->next; *pptr != NULL; pptr = &(*pptr)->next) {;}
    stryrd = *pptr = SML3_calloc(1, sizeof(*stryrd));
  }
  stryrd->type = STRYRD_SOUND;

  stryrd->hparam = SML3_hashnew(NULL, 0);

  lpos = ftell(ffp);
  while (fgets(fbuf, sizeof(fbuf), ffp) != NULL) {
    SML3_schnipp(fbuf, VGI_WSPACE, 2);
    if ((pt1 = strchr(fbuf, '#')) != NULL) { *pt1 = '\0'; }
    if (*fbuf == '\0' || (*fbuf != ' ' && *fbuf != '\t')) {  /* block ended */
      fseek(ffp, lpos, SEEK_SET);
      break;
    }
    SML3_schnipp(fbuf, VGI_SPACE, 1);

    line_in_hash(stryrd->hparam, fbuf, VG_FALSE, hvar);
  }
} /* Ende add_sound */


/* add draw to story-elements */
static void
add_draw(FILE *ffp, const char *rline, struct story_read **stryrdp, struct VG_Hash *hvar)
{
  struct story_read *stryrd;
  char fbuf[512], *pt1;
  long lpos;

  if (ffp == NULL || rline == NULL || stryrdp == NULL) { return; }

  if (*stryrdp == NULL) {
    stryrd = *stryrdp = SML3_calloc(1, sizeof(*stryrd));
  } else {
    struct story_read **pptr;
    for (pptr = &(*stryrdp)->next; *pptr != NULL; pptr = &(*pptr)->next) {;}
    stryrd = *pptr = SML3_calloc(1, sizeof(*stryrd));
  }
  stryrd->type = STRYRD_DRAW;

  stryrd->hparam = SML3_hashnew(NULL, 0);
  stryrd->u.draw.posalign = VG_POS_CENTERED;
  stryrd->u.draw.flip = VG_AXE_NONE;

  lpos = ftell(ffp);
  while (fgets(fbuf, sizeof(fbuf), ffp) != NULL) {
    SML3_schnipp(fbuf, VGI_WSPACE, 2);
    if ((pt1 = strchr(fbuf, '#')) != NULL) { *pt1 = '\0'; }
    if (*fbuf == '\0' || (*fbuf != ' ' && *fbuf != '\t')) {  /* block ended */
      fseek(ffp, lpos, SEEK_SET);
      break;
    }
    SML3_schnipp(fbuf, VGI_SPACE, 1);

    if (strncmp(fbuf, "POSALIGN:", 9) == 0) {
      pt1 = fbuf + 9 + strspn(fbuf + 9, VGI_SPACE);
      SML3_schnipp(pt1, VGI_WSPACE, 2);
      if (*pt1 != '\0') {
        struct SML3_gummi gmv = SML3_GUM_INITIALIZER;
        if (hvar != NULL) { vg4_intern_str_with_var(&gmv, pt1, hvar, NULL); pt1 = SML3_gumgetval(&gmv); }
        if (strcmp(pt1, "upper-left") == 0) {
          stryrd->u.draw.posalign = VG_POS_UPPER_LEFT;
        } else if (strcmp(pt1, "lower-left") == 0) {
          stryrd->u.draw.posalign = VG_POS_LOWER_LEFT;
        } else if (strcmp(pt1, "centered") == 0) {
          stryrd->u.draw.posalign = VG_POS_CENTERED;
        } else if (strcmp(pt1, "upper-right") == 0) {
          stryrd->u.draw.posalign = VG_POS_UPPER_RIGHT;
        } else if (strcmp(pt1, "lower-right") == 0) {
          stryrd->u.draw.posalign = VG_POS_LOWER_RIGHT;
        }
        SML3_gumdest(&gmv);
      }
    } else if (strncmp(fbuf, "FLIP:", 5) == 0) {
      pt1 = fbuf + 5 + strspn(fbuf + 5, VGI_SPACE);
      SML3_schnipp(pt1, VGI_WSPACE, 2);
      if (*pt1 != '\0') {
        struct SML3_gummi gmv = SML3_GUM_INITIALIZER;
        if (hvar != NULL) { vg4_intern_str_with_var(&gmv, pt1, hvar, NULL); pt1 = SML3_gumgetval(&gmv); }
        if (strcmp(pt1, "horizontal") == 0) {
          stryrd->u.draw.flip = VG_AXE_HORIZONTAL;
        } else if (strcmp(pt1, "vertical") == 0) {
          stryrd->u.draw.flip = VG_AXE_VERTICAL;
        }
        SML3_gumdest(&gmv);
      }
    } else {
      line_in_hash(stryrd->hparam, fbuf, VG_FALSE, hvar);
    }
  }
} /* Ende add_draw */


/* next checkpoint: set story-elements into film-story */
static VG_BOOL
add_check(const char *filename, int *ckp_idx, const char *rline, struct vgi_film *flm, const struct film_checkpoint *ckpoint, struct story_read **stryrdp)
{
  const char *kpt1;
  struct story_read *stryrd;
  int ckp_nidx, anz_pic, istory, pos_pic, prez;
  int start_pic;
  char fname[256], *pval, bval[32];
  struct SML3_formel_vhash *vhash;
  struct SML3_hashelem *he_percent, *he1;
  SML3_int64 iwert;

  if (ckp_idx == NULL || rline == NULL || flm == NULL || ckpoint == NULL || stryrdp == NULL) { return VG_TRUE; }

  kpt1 = rline + strspn(rline, VGI_SPACE);
  if (*kpt1 == '\0') { return VG_TRUE; }

  for (ckp_nidx = 0; ckp_nidx < ckpoint->anz; ckp_nidx++) {
    if (strcmp(kpt1, ckpoint->e[ckp_nidx].name) == 0) { break; }
  }
  if (ckp_nidx == ckpoint->anz) {
    outerr("reading film-story \"%s\": checkpoint %s not found", filename, kpt1);
    return VG_FALSE;
  }
  if (*ckp_idx >= 0 && ckpoint->e[ckp_nidx].floop < ckpoint->e[*ckp_idx].floop) {
    outerr("reading film-story \"%s\": film-loop-position of checkpoint %s is less than film-loop-position of previous checkpoint %s (%d < %d)",
           filename, ckpoint->e[ckp_nidx].name, ckpoint->e[*ckp_idx].name,
           ckpoint->e[ckp_nidx].floop, ckpoint->e[*ckp_idx].floop);
    return VG_FALSE;
  }

  istory = flm->story.anz - 1;

  /* number of pictures */
  if (*ckp_idx >= 0) {
    anz_pic = ckpoint->e[ckp_nidx].floop - ckpoint->e[*ckp_idx].floop;
  } else {
    anz_pic = ckpoint->e[ckp_nidx].floop;
  }

  /* update to new checkpoint */
  *ckp_idx = ckp_nidx;

  if (flm->story.e[istory].anz_pic == 0 && *stryrdp == NULL) {  /* first real checkpoint */
    flm->story.e[istory].pic_start = anz_pic;
    return VG_TRUE;
  }

  vhash = SML3_formel_ebene_new(1);
  if (SML3_formel_expandvar(vhash, &he_percent, FRML_PERCENT, NULL, 1) == NULL) {
    outerr("reading film-story \"%s\": %s", filename, SML3_fehlermsg());
    return VG_FALSE;
  }
  if (!SML3_hashelem_typ_is_normal(he_percent)) {
    outerr("reading film-story \"%s\": variable %s is invalid", filename, FRML_PERCENT);
    return VG_FALSE;
  }

  start_pic = flm->story.e[istory].anz_pic;

  /* allocate pictures */
  flm->story.e[istory].pic = do_alloc(flm->story.e[istory].pic, sizeof(*flm->story.e[0].pic), flm->story.e[istory].anz_pic, anz_pic);
  flm->story.e[istory].anz_picp = do_alloc(flm->story.e[istory].anz_picp, sizeof(*flm->story.e[0].anz_picp), flm->story.e[istory].anz_pic, anz_pic);

  /* set pictures */
  for (stryrd = *stryrdp; stryrd != NULL; stryrd = stryrd->next) {
    if (stryrd->type == STRYRD_MUSIC) {
      struct vgi_film_music fmus;

      /* load audio */
      fmus.audc = 0;
      if ((he1 = SML3_hashget(stryrd->hparam, "FILE", sizeof("FILE"))) != NULL) {
        pval = (char *)SML3_hashelem_valget(he1, NULL);
        if (pval != NULL) {
          snprintf(fname, sizeof(fname), "%s/%s", ckpoint->dirname, pval);
          fmus.audc = vg4->audio->load(fname, 100, VG_AUDIO_VOLUME_MUSIC);
        }
      }

      if (fmus.audc > 0) {
        fmus.looping = fmus.cresc = fmus.toend = VG_FALSE;
        flm->loaded.audcp = do_alloc(flm->loaded.audcp, sizeof(*flm->loaded.audcp), flm->loaded.anz_audio++, 1);
        flm->loaded.audcp[flm->loaded.anz_audio - 1] = fmus.audc;

        if ((he1 = SML3_hashget(stryrd->hparam, "PLAY", sizeof("PLAY"))) != NULL) {
          pval = (char *)SML3_hashelem_valget(he1, NULL);
          if (pval != NULL) {
            if (strcmp(pval, "LOOPING") == 0) {
              fmus.looping = VG_TRUE;
            } else if (strcmp(pval, "END") == 0) {
              fmus.toend = VG_TRUE;
            }
          }
        }

        if ((he1 = SML3_hashget(stryrd->hparam, "CRESC", sizeof("CRESC"))) != NULL) {
          pval = (char *)SML3_hashelem_valget(he1, NULL);
          if (pval != NULL) {
            if (strcmp(pval, "YES") == 0 || strcmp(pval, "Y") == 0 || atoi(pval) > 0) {
              fmus.cresc = VG_TRUE;
            }
          }
        }

        pos_pic = start_pic;
        if ((he1 = SML3_hashget(stryrd->hparam, "START", sizeof("START"))) != NULL) {
          pval = (char *)SML3_hashelem_valget(he1, NULL);
          if (pval != NULL) {
            pos_pic = atoi(pval);
            if (pos_pic < 0) { pos_pic = 0; }
            if (pos_pic > 99) { pos_pic = 99; }
            pos_pic = start_pic + pos_pic * anz_pic / 100;
          }
        }

        flm->music.mus = do_alloc(flm->music.mus, sizeof(*flm->music.mus), flm->music.anz++, 1);
        fmus.pic_no = flm->story.e[istory].pic_start + pos_pic;
        flm->music.mus[flm->music.anz - 1] = fmus;
      }

    } else if (stryrd->type == STRYRD_SOUND) {
      struct vgi_film_story_sound fsnd;

      /* load audio */
      fsnd.audc = 0;
      if ((he1 = SML3_hashget(stryrd->hparam, "FILE", sizeof("FILE"))) != NULL) {
        pval = (char *)SML3_hashelem_valget(he1, NULL);
        if (pval != NULL) {
          snprintf(fname, sizeof(fname), "%s/%s", ckpoint->dirname, pval);
          fsnd.audc = vg4->audio->load(fname, 100, VG_AUDIO_VOLUME_SOUND);
        }
      }

      if (fsnd.audc > 0) {
        int pclast, pcnow;

        flm->loaded.audcp = do_alloc(flm->loaded.audcp, sizeof(*flm->loaded.audcp), flm->loaded.anz_audio++, 1);
        flm->loaded.audcp[flm->loaded.anz_audio - 1] = fsnd.audc;

        pclast = -1;
        for (pos_pic = start_pic; pos_pic < start_pic + anz_pic; pos_pic++) {
          /* set percent variable */
          pcnow = (pos_pic - start_pic) * 100 / anz_pic;
          if (pcnow == pclast) { continue; }  /* just one sound per percent-value */
          snprintf(bval, sizeof(bval), "%d", pcnow + 1);
          SML3_hashelem_valset(he_percent, bval, strlen(bval) + 1);

          /* valid? */
          if ((he1 = SML3_hashget(stryrd->hparam, "VALID", sizeof("VALID"))) != NULL) {
            pval = (char *)SML3_hashelem_valget(he1, NULL);
            if (pval != NULL) {
              int boolerg;
              for (pclast++; pclast <= pcnow; pclast++) {
                /* update percent variable */
                snprintf(bval, sizeof(bval), "%d", pclast + 1);
                SML3_hashelem_valset(he_percent, bval, strlen(bval) + 1);
                /* test validness */
                if (SML3_formel_booltest(vhash, -1, 0, pval, NULL, &boolerg) == NULL) {
                  outerr("reading film-story \"%s\": %s", filename, SML3_fehlermsg());
                  goto ch_end;
                }
                if (boolerg) { break; }  /* valid */
              }
              if (pclast > pcnow) { pclast = pcnow; continue; }  /* not valid */
            }
          } else {
            if (pos_pic > start_pic) { break; }
          }
          pclast = pcnow;

          flm->story.e[istory].snd = do_alloc(flm->story.e[istory].snd, sizeof(*flm->story.e[0].snd), flm->story.e[istory].anz_snd++, 1);
          fsnd.pic_no = flm->story.e[istory].pic_start + pos_pic;
          flm->story.e[istory].snd[flm->story.e[istory].anz_snd - 1] = fsnd;
        }
      }

    } else if (stryrd->type == STRYRD_DRAW) {
      int imgcnt_idx;
      struct vgi_film_story_picture *spic;
      struct vgi_imgcnt imgcnt;

      /* load image|sprite|text */
      imgcnt_idx = -1;
      imgcnt.type = VGI_IMGCNT_NONE;
      if ((he1 = SML3_hashget(stryrd->hparam, "IMAGE", sizeof("IMAGE"))) != NULL) {
        pval = (char *)SML3_hashelem_valget(he1, NULL);
        if (pval != NULL) {
          snprintf(fname, sizeof(fname), "%s/%s", ckpoint->dirname, pval);
          imgcnt.u.img = vg4->image->load(fname);
          if (imgcnt.u.img != NULL) { imgcnt.type = VGI_IMGCNT_IMAGE; }
        }
      } else if ((he1 = SML3_hashget(stryrd->hparam, "SPRITE", sizeof("SPRITE"))) != NULL) {
        pval = (char *)SML3_hashelem_valget(he1, NULL);
        if (pval != NULL) {
          snprintf(fname, sizeof(fname), "%s/%s", ckpoint->dirname, pval);
          imgcnt.u.sprt = vg4->sprite->load(fname);
          if (imgcnt.u.sprt != NULL) { imgcnt.type = VGI_IMGCNT_SPRITE; }
        }
      } else if ((he1 = SML3_hashget(stryrd->hparam, "TEXT", sizeof("TEXT"))) != NULL) {
        pval = (char *)SML3_hashelem_valget(he1, NULL);
        if (pval != NULL) {
          snprintf(fname, sizeof(fname), "%s/%s", ckpoint->dirname, pval);
          imgcnt.u.img = vg4->font->loadtext(fname, NULL, ckpoint->hvar, NULL);
          if (imgcnt.u.img != NULL) { imgcnt.type = VGI_IMGCNT_IMAGE; }
        }
      }
      if (imgcnt.type != VGI_IMGCNT_NONE) {
        flm->loaded.imgcntp = do_alloc(flm->loaded.imgcntp, sizeof(*flm->loaded.imgcntp), flm->loaded.anz_img++, 1);
        flm->loaded.imgcntp[flm->loaded.anz_img - 1] = imgcnt;
        imgcnt_idx = flm->loaded.anz_img - 1;
      }

      if (imgcnt_idx >= 0) {
        for (pos_pic = start_pic; pos_pic < start_pic + anz_pic; pos_pic++) {
          /* set percent variable */
          snprintf(bval, sizeof(bval), "%d", (pos_pic - start_pic) * 100 / anz_pic + 1);
          SML3_hashelem_valset(he_percent, bval, strlen(bval) + 1);

          /* valid? */
          if ((he1 = SML3_hashget(stryrd->hparam, "VALID", sizeof("VALID"))) != NULL) {
            pval = (char *)SML3_hashelem_valget(he1, NULL);
            if (pval != NULL) {
              int boolerg;
              if (SML3_formel_booltest(vhash, -1, 0, pval, NULL, &boolerg) == NULL) {
                outerr("reading film-story \"%s\": %s", filename, SML3_fehlermsg());
                goto ch_end;
              }
              if (!boolerg) { continue; }
            }
          }

          flm->story.e[istory].pic[pos_pic] = do_alloc(
                                                flm->story.e[istory].pic[pos_pic],
                                                sizeof(*flm->story.e[istory].pic[0]),
                                                flm->story.e[istory].anz_picp[pos_pic]++,
                                                1);
          spic = &flm->story.e[istory].pic[pos_pic][flm->story.e[istory].anz_picp[pos_pic] - 1];

          spic->imgcnt_idx = imgcnt_idx;
          spic->posi.pos = stryrd->u.draw.posalign;
          spic->zoom = 100;
          spic->flip = stryrd->u.draw.flip;

          /* set x-position */
          if ((he1 = SML3_hashget(stryrd->hparam, "POSX", sizeof("POSX"))) != NULL) {
            pval = (char *)SML3_hashelem_valget(he1, NULL);
            prez = -1;
            if (SML3_formel_rechne(vhash, &iwert, NULL, &prez, pval, NULL) == NULL) {
              outerr("reading film-story \"%s\": %s", filename, SML3_fehlermsg());
              goto ch_end;
            }
            spic->posi.x = (int)iwert;
          }

          /* set y-position */
          if ((he1 = SML3_hashget(stryrd->hparam, "POSY", sizeof("POSY"))) != NULL) {
            pval = (char *)SML3_hashelem_valget(he1, NULL);
            prez = -1;
            if (SML3_formel_rechne(vhash, &iwert, NULL, &prez, pval, NULL) == NULL) {
              outerr("reading film-story \"%s\": %s", filename, SML3_fehlermsg());
              goto ch_end;
            }
            spic->posi.y = (int)iwert;
          }

          /* set zoom */
          if ((he1 = SML3_hashget(stryrd->hparam, "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("reading film-story \"%s\": %s", filename, SML3_fehlermsg());
              goto ch_end;
            }
            spic->zoom = (int)iwert;
          }

          /* set rotate */
          if ((he1 = SML3_hashget(stryrd->hparam, "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("reading film-story \"%s\": %s", filename, SML3_fehlermsg());
              goto ch_end;
            }
            spic->rotate = (int)iwert;
          }

          /* set brightness */
          spic->bright = 100;
          if ((he1 = SML3_hashget(stryrd->hparam, "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("reading film-story \"%s\": %s", filename, SML3_fehlermsg());
              goto ch_end;
            }
            spic->bright = (int)iwert;
          }

          /* set opaqueness */
          spic->opaque = 100;
          if ((he1 = SML3_hashget(stryrd->hparam, "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("reading film-story \"%s\": %s", filename, SML3_fehlermsg());
              goto ch_end;
            }
            spic->opaque = (int)iwert;
          }
        }
      }
    }
  }

ch_end:
  SML3_formel_ebene_free(&vhash);

  flm->story.e[istory].anz_pic += anz_pic;
  free_stryrd(stryrdp);

  return VG_TRUE;
} /* Ende add_check */


/* fade out film */
static void
fadeout(struct VG_Image *wimg, struct VG_Rect *frect, int bgcolor, int audc)
{
  const int loop_time = 100;
  struct VG_ImagecopyAttr iattr;
  struct VG_Position posi;

  VG_IMAGECOPY_ATTR_DEFAULT(&iattr);

  for (;;) {
    if (audc > 0) {
      if (!vg4->audio->is_playing(audc, NULL)) { audc = 0; }
    } else {
      if (iattr.pixel.opaqueness == 0) { break; }
    }

    if (iattr.pixel.opaqueness < 3) { iattr.pixel.opaqueness = 0; } else { iattr.pixel.opaqueness -= 3; }

    if (wimg != NULL) {
      vg4->window->draw_rect(frect, bgcolor, VG_TRUE);

      if (frect != NULL) {
        posi.x = frect->x;
        posi.y = frect->y;
        posi.pos = VG_POS_UPPER_LEFT;
        vg4->window->copy(wimg, &posi, &iattr);
      } else {
        vg4->window->copy(wimg, NULL, &iattr);
      }
    }

    vg4->window->flush();
    vg4->misc->wait_time(loop_time);
  }
} /* Ende fadeout */


/* play loaded film */
static VG_BOOL
playit(struct vgi_film *flm, const struct VG_Rect *filmrect, VG_BOOL *filmskip, int res_w, int res_h)
{
  struct VG_Rect frect, arect;
  int istory, idx;
  int kpause_space, kquit_return, kquit_escape;
  struct VG_Position posi;
  struct VG_Image *aimg;
  VG_BOOL atend;
  VG_BOOL retw = VG_TRUE;

  if (flm == NULL) { return VG_TRUE; }

  if (filmrect != NULL && filmrect->w > 0 && filmrect->h > 0) {
    frect = *filmrect;
  } else {
    frect.x = frect.y = 0;
    vg4->window->getsize(&frect.w, &frect.h);
  }
  flm->wimg = vg4_image_create_nolist(frect.w, frect.h);

  /* set scaling */
  aimg = NULL;
  arect = frect; arect.x = arect.y = 0;
  if (res_w > 0 && res_h > 0) {
    int sc_w, sc_h;
    sc_w = frect.w * 1000 / res_w;
    sc_h = frect.h * 1000 / res_h;
    if (sc_w < sc_h) { flm->scale_promille = sc_w; } else { flm->scale_promille = sc_h; }
    if (flm->scale_promille == 1000) { flm->scale_promille = 0; }
    if (flm->scale_promille > 0) {
      arect.x = (frect.w - flm->scale_promille * res_w / 1000) / 2;
      arect.y = (frect.h - flm->scale_promille * res_h / 1000) / 2;
      arect.w -= (arect.x * 2);
      arect.h -= (arect.y * 2);
      if (arect.w > 0 && arect.h > 0) { aimg = vg4_image_create_nolist(arect.w, arect.h); }
    }
  } else {
    flm->scale_promille = 0;
  }

  /* reset */
  flm->music.mus_pos = 0;
  for (istory = 0; istory < flm->story.anz; istory++) {
    flm->story.e[istory].snd_pos = 0;
  }

  /* set skipping keys */
  kpause_space = vg4->input->key_insert("Pause-film", VG_FALSE, VG_FALSE);
  vg4->input->key_setkbd(kpause_space, VG_INPUT_KBDCODE_SPACE);
  kquit_return = vg4->input->key_insert("Quit-film", VG_FALSE, VG_FALSE);
  vg4->input->key_setkbd(kquit_return, VG_INPUT_KBDCODE_RETURN);
  kquit_escape = vg4->input->key_insert("Quit-film", VG_FALSE, VG_FALSE);
  vg4->input->key_setkbd(kquit_escape, VG_INPUT_KBDCODE_ESCAPE);

  /* film-loop */
  for (flm->pic_pos = 0;; flm->pic_pos++) {
    if (!vg4->input->update(VG_TRUE)) { retw = VG_FALSE; goto playend; }

    if (filmskip != NULL) {
      if (vg4->input->key_newpressed(kquit_return)) { *filmskip = VG_TRUE; goto playend; }
      if (vg4->input->key_newpressed(kquit_escape)) { *filmskip = VG_TRUE; goto playend; }
    }

    if (vg4->input->key_newpressed(kpause_space)) {
      VG_BOOL au_susp = vg4->audio->suspend(VG_TRUE);
      for (;;) {
        if (!vg4->input->update(VG_FALSE)) { retw = VG_FALSE; goto playend; }
        if (vg4->input->key_newpressed(kpause_space)) { break; }
        if (vg4->input->key_newpressed(kquit_return)) { break; }
        if (vg4->input->key_newpressed(kquit_escape)) { break; }
        vg4->window->flush();
        SML3_sleep_msek(VGI_WAIT_TIME);
      }
      vg4->audio->suspend(au_susp);
    }

    /* music */
    if (flm->music.anz > 0 && flm->music.mus_pos < flm->music.anz) {
      idx = flm->music.mus_pos;
      /* just play last music for musics with the same starting picture */
      while (idx < flm->music.anz) {
        if (flm->music.mus[idx].pic_no > flm->pic_pos) { break; }
        idx++;
      }
      if (idx > 0) {
        if (flm->music.mus[idx - 1].pic_no == flm->pic_pos) {
          /* stop old music and start new music */
          if (idx - 1 > 0 && vg4->audio->is_playing(flm->music.mus[idx - 2].audc, NULL)) {
            /* if new music is using crescendo, stop old music also using crescendo */
            vg4->audio->stop(flm->music.mus[idx - 2].audc, flm->music.mus[idx - 1].cresc);
          }
          vg4->audio->play(flm->music.mus[idx - 1].audc, flm->music.mus[idx - 1].looping, flm->music.mus[idx - 1].cresc);
        }
      }
      flm->music.mus_pos = idx;
    }

    /* check if all stories have been ended */
    atend = VG_FALSE;
    for (istory = 0; istory < flm->story.anz; istory++) {
      if (flm->story.e[istory].anz_pic > 0) {
        if (flm->pic_pos < flm->story.e[istory].pic_start + flm->story.e[istory].anz_pic) { break; }
        if (flm->pic_pos == flm->story.e[istory].pic_start + flm->story.e[istory].anz_pic) { atend = VG_TRUE; }
      }
    }
    if (istory == flm->story.anz) {
      if (!atend) { break; }
    } else {
      atend = VG_FALSE;
    }

    if (!atend) {
      vg4->image->clear(flm->wimg);
      if (aimg != NULL) { vg4->image->clear(aimg); }
      vg4->window->draw_rect(&frect, flm->bgcolor, VG_TRUE);
    }

    /* stories */
    for (istory = 0; istory < flm->story.anz; istory++) {
      if (flm->story.e[istory].anz_pic == 0
          || flm->pic_pos < flm->story.e[istory].pic_start
          || flm->pic_pos > flm->story.e[istory].pic_start + flm->story.e[istory].anz_pic) { continue; }

      /* picture */
      if (flm->story.e[istory].anz_pic > 0) {
        if (flm->pic_pos >= flm->story.e[istory].pic_start
            && flm->pic_pos < flm->story.e[istory].pic_start + flm->story.e[istory].anz_pic) {
          struct vgi_film_story_picture *spic;
          struct VG_ImagecopyAttr iattr1, iattr2, iattr_sum;
          struct VG_Image *imgp;
          int eidx, ipic;
          eidx = flm->pic_pos - flm->story.e[istory].pic_start;
          for (ipic = 0; ipic < flm->story.e[istory].anz_picp[eidx]; ipic++) {
            spic = &flm->story.e[istory].pic[eidx][ipic];
            if (spic->imgcnt_idx < 0) { continue; }
            imgp = vg4_intern_get_img_from_imgcnt(&flm->loaded.imgcntp[spic->imgcnt_idx], &iattr1);
            if (imgp != NULL) {  /* have image */
              /* sum image-copy attributes from image with them from film */
              VG_IMAGECOPY_ATTR_DEFAULT(&iattr2);
              iattr2.pixel.brightness = spic->bright;
              iattr2.pixel.opaqueness = spic->opaque;
              iattr2.image.zoom_width = iattr2.image.zoom_height = spic->zoom;
              iattr2.image.zoom_ispercent = VG_TRUE;
              iattr2.image.rotate = spic->rotate;
              iattr2.image.flip = spic->flip;
              vg4->image->attr_sum(&iattr_sum, &iattr1, &iattr2);
              if (flm->scale_promille > 0) {
                iattr_sum.image.zoom_width = iattr_sum.image.zoom_width * flm->scale_promille / 1000;
                iattr_sum.image.zoom_height = iattr_sum.image.zoom_width;
              }
              /* set position of image */
              posi = spic->posi;
              if (flm->scale_promille > 0) {
                posi.x = posi.x * flm->scale_promille / 1000;
                posi.y = posi.y * flm->scale_promille / 1000;
              }
              /* copy image */
              if (aimg != NULL) {
                vg4->image->copy(aimg, imgp, &posi, &iattr_sum);
              } else {
                posi.x += arect.x;
                posi.y += arect.y;
                vg4->image->copy(flm->wimg, imgp, &posi, &iattr_sum);
              }
            }
          }

        } else if (flm->pic_pos == flm->story.e[istory].pic_start + flm->story.e[istory].anz_pic) {
          /* story ended: stop playing sounds */
          for (idx = 0; idx < flm->story.e[istory].snd_pos; idx++) {
            if (vg4->audio->is_playing(flm->story.e[istory].snd[idx].audc, NULL)) {
              vg4->audio->stop(flm->story.e[istory].snd[idx].audc, VG_FALSE);
            }
          }
        }
      }

      /* sound(s) */
      if (flm->story.e[istory].anz_snd > 0 && flm->story.e[istory].snd_pos < flm->story.e[istory].anz_snd) {
        idx = flm->story.e[istory].snd_pos;
        /* play all sounds with the same starting picture */
        while (idx < flm->story.e[istory].anz_snd) {
          if (flm->story.e[istory].snd[idx].pic_no > flm->pic_pos) { break; }
          vg4->audio->play(flm->story.e[istory].snd[idx].audc, VG_FALSE, VG_FALSE);
          idx++;
        }
        flm->story.e[istory].snd_pos = idx;
      }
    }

    if (!atend) {
      if (aimg != NULL) { vg4->image->copy(flm->wimg, aimg, NULL, NULL); }
      posi.x = frect.x;
      posi.y = frect.y;
      posi.pos = VG_POS_UPPER_LEFT;
      vg4->window->copy(flm->wimg, &posi, NULL);
    }
    vg4->window->flush();
    vg4->misc->wait_time(flm->floop);
  }

  /* fade out or stop music or wait to end */
  idx = flm->music.mus_pos - 1;
  if (flm->fadeout) {  /* fade out window, evtl. stop music using decrescendo */
    int audc = 0;
    if (idx >= 0 && vg4->audio->is_playing(flm->music.mus[idx].audc, NULL)) {
      audc = flm->music.mus[idx].audc;
      if (!flm->music.mus[idx].toend) { vg4->audio->stop(audc, VG_TRUE); }
    }
    fadeout(flm->wimg, &frect, flm->bgcolor, audc);
  } else if (idx >= 0 && flm->music.mus[idx].toend) {  /* wait for end of music */
    while (vg4->audio->is_playing(flm->music.mus[idx].audc, NULL)) {
      if (!vg4->input->update(VG_TRUE)) { retw = VG_FALSE; goto playend; }
      if (filmskip != NULL) {
        if (vg4->input->key_newpressed(kquit_return)) { *filmskip = VG_TRUE; goto playend; }
        if (vg4->input->key_newpressed(kquit_escape)) { *filmskip = VG_TRUE; goto playend; }
      }
      vg4->window->flush();
      vg4->misc->wait_time(flm->floop);
    }
  }

playend:
  if (aimg != NULL) { vg4->image->destroy(aimg); }
  /* remove skipping keys */
  vg4->input->key_remove(kpause_space);
  vg4->input->key_remove(kquit_return);
  vg4->input->key_remove(kquit_escape);

  return retw;
} /* Ende playit */


/* film_play:
 * play film from directory
 * @param dirname   directory-name
 * @param rect      rectangle where to play film, or NULL = whole window
 * @param filmskip  if not NULL allow skipping film with return or escape and return whether it was skipped
 * @param hvar      hash with variable-values for text control-commands "var" and for "$(<varname>)", or NULL
 *                    hash-format:
 *                     - key:   variable name
 *                     - value: variable value
 * @return  VG_TRUE = OK or VG_FALSE = exit-request
 *
 * Pause the film mit space.
 *
 * film-directory:
 * - must contain a file named "film"
 *   format: see read_film()
 * - must contain one or more files with extension ".story"
 *   format: see read_story()
 *
 * The stories are packed together
 * and played according to their start- and end-checkpoints,
 * which are defined in the file "film"
 */
static VG_BOOL
film_play(const char *dirname, const struct VG_Rect *rect, VG_BOOL *filmskip, struct VG_Hash *hvar)
{
  struct film_checkpoint ckpoint;
  struct vgi_film flm;
  struct VG_Hash *hclon;
  VG_BOOL retw = VG_TRUE;

  if (filmskip != NULL) { *filmskip = VG_FALSE; }

  if (hvar != NULL) {
    hclon = vg4_hash_clone_nolist(hvar);
  } else {
    hclon = vg4->hash->create();
  }

  if (!read_film(&flm, &ckpoint, dirname, hclon)) { goto fp_end; }
  if (!get_stories(&flm, &ckpoint)) { goto fp_end; }

  if (vg4data.env.film_dump > 0) {
    dumpit(&ckpoint, &flm);
  }

  if (vg4data.env.film_dump < 2) {
    retw = playit(&flm, rect, filmskip, ckpoint.res_w, ckpoint.res_h);
  }

  free_ckpoint(&ckpoint);
  free_flm(&flm);

fp_end:
  vg4->hash->destroy(hclon);
  return retw;
} /* Ende film_play */


/* dump film to stderr */
static void
dumpit(struct film_checkpoint *ckpoint, struct vgi_film *flm) {
  int i1, i2, i3;
  struct vgi_imgcnt *imgcntp;
  const char *namptr;

  if (ckpoint == NULL || flm == NULL) { return; }

  fprintf(stderr, "\nDump of film entries\n");
  fprintf(stderr, "====================\n\n");

  fprintf(stderr, "- HVAR\n  ----\n");
  { void *vpos;
    const char *vkey, *vval;
    vpos = NULL;
    for (vkey = vg4->hash->list(ckpoint->hvar, &vpos); vpos != NULL; vkey = vg4->hash->list(ckpoint->hvar, &vpos)) {
      vval = (const char *)vg4->hash->get(ckpoint->hvar, vkey, NULL);
      printf("  - %s: %s\n", vkey, (vval == NULL ? "" : vval));
    }
  }
  fprintf(stderr, "\n");

  fprintf(stderr, "- CKPOINT\n  -------\n");
  fprintf(stderr, "  - dirname=%s\n", ckpoint->dirname);
  fprintf(stderr, "  - res=%dx%d\n", ckpoint->res_w, ckpoint->res_h);
  fprintf(stderr, "  - anz=%d\n", ckpoint->anz);
  for (i1 = 0; i1 < ckpoint->anz; i1++) {
    fprintf(stderr, "    - e[%d]: name=%s, floop=%d, t.name=%s, t.floop=%d\n", i1, ckpoint->e[i1].name, ckpoint->e[i1].floop, ckpoint->e[i1].t.name, ckpoint->e[i1].t.floop);
  }
  fprintf(stderr, "\n");

  fprintf(stderr, "- FLM\n  ---\n");
  fprintf(stderr, "  - floop = %d\n", flm->floop);
  fprintf(stderr, "  - fadeout = %d\n", flm->fadeout);
  fprintf(stderr, "  - bgcolor = %d\n", flm->bgcolor);
  fprintf(stderr, "  - scale_promille = %d\n", flm->scale_promille);
  fprintf(stderr, "  - loaded:\n");
  fprintf(stderr, "    - anz_img = %d\n", flm->loaded.anz_img);
  for (i1 = 0; i1 < flm->loaded.anz_img; i1++) {
    namptr = vg4_intern_get_name_from_imgcnt(&flm->loaded.imgcntp[i1]);
    fprintf(stderr, "      - imgcntp[%d] = %s\n", i1, namptr);
  }
  fprintf(stderr, "    - anz_audio = %d\n", flm->loaded.anz_audio);
  for (i1 = 0; i1 < flm->loaded.anz_audio; i1++) {
    fprintf(stderr, "      - audcp[%d] = %s\n", i1, vg4->audio->getname(flm->loaded.audcp[i1]));
  }
  fprintf(stderr, "  - music:\n");
  fprintf(stderr, "    - anz = %d\n", flm->music.anz);
  for (i1 = 0; i1 < flm->music.anz; i1++) {
    fprintf(stderr, "      - mus[%d]\n", i1);
    fprintf(stderr, "        - file    = %s\n", vg4->audio->getname(flm->music.mus[i1].audc));
    fprintf(stderr, "        - pic_no  = %d\n", flm->music.mus[i1].pic_no);
    fprintf(stderr, "        - looping = %d\n", flm->music.mus[i1].looping);
    fprintf(stderr, "        - cresc   = %d\n", flm->music.mus[i1].cresc);
    fprintf(stderr, "        - toend   = %d\n", flm->music.mus[i1].toend);
  }
  fprintf(stderr, "  - story:\n");
  fprintf(stderr, "    - anz = %d\n", flm->story.anz);
  for (i1 = 0; i1 < flm->story.anz; i1++) {
    fprintf(stderr, "      - e[%d]: %s\n", i1, flm->story.e[i1].name);
    fprintf(stderr, "        - pic_start = %d\n", flm->story.e[i1].pic_start);
    fprintf(stderr, "        - anz_pic   = %d\n", flm->story.e[i1].anz_pic);
    for (i2 = 0; i2 < flm->story.e[i1].anz_pic; i2++) {
      struct vgi_film_story_picture *spic;
      for (i3 = 0; i3 < flm->story.e[i1].anz_picp[i2]; i3++) {
        spic = &flm->story.e[i1].pic[i2][i3];
        if (i3 == 0) {
          fprintf(stderr, "          - pic[%d](pic_pos=%d):", i2, flm->story.e[i1].pic_start + i2);
        } else {
          fprintf(stderr, "            add-pic:");
        }
        if (spic->imgcnt_idx >= 0) {
          imgcntp = &flm->loaded.imgcntp[spic->imgcnt_idx];
        } else {
          imgcntp = NULL;
        }
        namptr = vg4_intern_get_name_from_imgcnt(imgcntp);
        fprintf(stderr, " imgcnt=\"%s\"", namptr);
        fprintf(stderr, " posi=%d,%d,(%d)", spic->posi.x, spic->posi.y, spic->posi.pos);
        fprintf(stderr, " zoom=%d", spic->zoom);
        fprintf(stderr, " rotate=%d", spic->rotate);
        fprintf(stderr, " bright=%d", spic->bright);
        fprintf(stderr, " opaque=%d", spic->opaque);
        fprintf(stderr, " flip=%d\n", spic->flip);
      }
    }
    fprintf(stderr, "        - anz_snd   = %d\n", flm->story.e[i1].anz_snd);
    for (i2 = 0; i2 < flm->story.e[i1].anz_snd; i2++) {
      fprintf(stderr, "          - snd[%d]:", i2);
      fprintf(stderr, " audc=%d", flm->story.e[i1].snd[i2].audc);
      fprintf(stderr, " file=%s", vg4->audio->getname(flm->story.e[i1].snd[i2].audc));
      fprintf(stderr, " pic_no=%d\n", flm->story.e[i1].snd[i2].pic_no);
    }
  }
} /* Ende dumpit */
