;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; main gameplay engine.
;; uses "game" module for playsim.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(begin-module 'play-engine)
(module-export '(
  init-resources
  init-console
  ;;
  reinit
  ;;
  run-game-loop
))
(module-export-r/w '(
  explosion-shake
  exit-opened-shake
  death-explosion-shake
  hatch-shake
  ;;
  scroll-speed
))
(import '(
  ::sdl
  ::flexgui
  ::flexgui:lgfx => lgfx
  ::bjprng
  ::concmd
  ::game
  ::bdcff-loader
  ::etype
))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define-macro ginc!(nspace env _ var ... rest)
  ;printf "var=%o; rest=%o\n" var rest
  cond
    {null? rest}
      `(,gset! ,var {,var + 1})
    {{pair? rest} and {null? cdr(rest)}}
      `(,gset! ,var {,var + ,car(rest)})
    else
      error "ivalid ginc! usage!"

define-macro gdec!(nspace env _ var ... rest)
  cond
    {null? rest}
      `(,gset! ,var {,var - 1})
    {{pair? rest} and {null? cdr(rest)}}
      `(,gset! ,var {,var - ,car(rest)})
    else
      error "ivalid ginc! usage!"


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; print chars and strings
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define sdl-print-char-internal(x y ch color)
  if between?(ch 0 127)
    then
      define ix bit-shl(bit-and(ch 15) 3)
      define iy bit-shl(bit-shr(ch 4) 3)
      sdl:set-texture-color-mod game-font-tex color
      sdl:blit-texture-rect game-font-tex x y 8 8 ix iy 8 8

define sdl-print-char(x y ch color)
  sdl:set-texture-blend-mode game-font-tex 'NONE
  sdl-print-char-internal x y ch color

define sdl-print-trans-char(x y ch color)
  sdl:set-texture-blend-mode game-font-tex 'BLEND
  sdl-print-char-internal x y ch color


define sdl-print-str-with-internal(x y str)
  define ch
  define ix
  define iy
  define idx 0
  while {idx <> string-length(str)}
    if between?({ch := {str[idx]}} 0 127)
      then
        {ix := bit-shl(bit-and(ch 15) 3)}
        {iy := bit-shl(bit-shr(ch 4) 3)}
        sdl:blit-texture-rect game-font-tex x y 8 8 ix iy 8 8
    inc! x 8
    inc! idx

define sdl-print-str(x y str color)
  sdl:set-texture-blend-mode game-font-tex 'NONE
  sdl:set-texture-color-mod game-font-tex color
  sdl-print-str-with-internal x y str

define sdl-print-trans-str(x y str color)
  sdl:set-texture-blend-mode game-font-tex 'BLEND
  sdl:set-texture-color-mod game-font-tex color
  sdl-print-str-with-internal x y str


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; for console
(define in-game #f)
(define cheated #f)
(define single-step-mode #f)


(define explosion-shake #t)
(define death-explosion-shake #f)
(define exit-opened-shake #t)
(define hatch-shake #t)


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(constant window-width round({1024 / window-scale-x}))
(constant window-height round({722 / window-scale-y}))
(define game-gfx-surface #nil)
(define game-gfx-tex #nil)
(define game-font-tex #nil)


define init-resources()
  (gset! lgfx:use-vsync #t)
  (sdl:set-app-name "Miho Dash SDL")
  (lgfx:open-window window-width window-height "Miho Dash" window-scale-x window-scale-y)
  (sdl:stop-text-input)
  ;;
  ;; it will be recolored and uploaded to `game-gfx-tex`
  (gset! game-gfx-surface sdl:new-surface-from-image(make-data-path("gfx/boulder_rush.png")))
  ;;
  (gset! game-font-tex sdl:new-texture-from-image(lgfx:window make-data-path("gfx/c64_font_transparent.png")))
  (sdl:set-texture-blend-mode game-font-tex 'NONE)
  ;(sdl:set-texture-scale-mode game-font-tex 'LINEAR)


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; globals
(define game-return-to-gui #f)
(define game-time #nil)
(define rendered-frame-ticks 0)
(define last-timing-report 0)
(define scr-x-ofs 0)
(define scr-y-ofs 9)
(define scr-w #nil)
(define scr-h #nil)
(define ntt #nil)
(define next-frame-time #nil)
(define next-game-time #nil)
(define anim-wait anim-delay)
(define gate-flash-time 0)
(define gate-flash-type 0)
(define evt #nil)
(define need-show #t)
(define reload-cave #t)
(define need-recolor #t)
(define sounds-loaded #f)
;; game states
(define game-state-handler #nil)
(define game-state-counter 0)
(define in-covering #nil)
;; allow "fire" after expration of this counter in covering states
(define game-state-fire-counter 0)
;;
(define scroll-speed 1)
(define scroll-delay 0)
;;
(define camera-x 0)
(define camera-y 0)
(define camera-scroll-cooldown 0)
;;
(define con-height 20)
(define con-visible #f)
(define con-line-skip 0)
;;
(define game-paused #f)
(define anim-paused #f)
(define anim-frame 0)
;(define con-cave-skipped #f)
;;


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; call before starting a new game
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define reinit()
  gset! game-return-to-gui #f
  gset! game-time game:cave-step-time
  gset! rendered-frame-ticks 0
  gset! last-timing-report 0
  gset! scr-x-ofs 0
  gset! scr-y-ofs 9
  gset! scr-w window-width
  gset! scr-h {window-height - scr-y-ofs}
  gset! ntt ticks-msec()
  gset! next-frame-time {ntt + frame-time}
  gset! next-game-time {ntt + game-time}
  gset! anim-wait anim-delay
  gset! gate-flash-time 0
  gset! gate-flash-type 0
  ;gset! evt #nil
  gset! need-show #t
  gset! reload-cave #t
  gset! need-recolor #t
  ;gset! sounds-loaded #f
  ;; game states
  gset! game-state-handler #nil
  gset! game-state-counter 0
  gset! in-covering #nil
  ;; allow "fire" after expration of this counter in covering states
  gset! game-state-fire-counter 0
  ;;
  gset! camera-x {game:viewport-x * 16}
  gset! camera-y {game:viewport-y * 16}
  gset! camera-scroll-cooldown 0
  ;;
  gset! single-step-mode #f
  ;gset! con-height 20
  ;gset! con-visible #f
  ;gset! con-line-skip 0
  ;gset! con-cave-skipped #f
  ;;
  gset! game-paused #f
  gset! anim-paused #f
  gset! anim-frame 0
  ;;
  gset! explosion-shake player-prop-ref-bool("camera-explosion-shake" #t)
  gset! exit-opened-shake player-prop-ref-bool("camera-exit-opened-shake" #t)
  gset! death-explosion-shake player-prop-ref-bool("camera-death-explosion-shake" #f)
  gset! hatch-shake player-prop-ref-bool("camera-hatch-shake" #t)
  ;;
  gset! scroll-speed clamp(player-prop-ref("camera-scroll-speed" 1) 1 256)
  gset! scroll-delay clamp(player-prop-ref("camera-scroll-delay" 0) 0 4)


(load "bd-70-play-engine-10-concmds.lsp")
(load "bd-70-play-engine-20-conmore.lsp")


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; gfx recoloring
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define recolor-prng-ctx bjprng:new())

define recolor-gfx-texture()
  if sdl:texture?(game-gfx-tex)
    sdl:free-texture game-gfx-tex
  define rri(min max)
    {min + bjprng:random(recolor-prng-ctx {max - min - -1})}
  define delta-h {rri(-255 255) / 255}
  define delta-s {rri(-32 255) / 255}
  define delta-l {rri(-64 64) / 255}
  define new-surf recolor-surface(game-gfx-surface delta-h delta-s delta-l)
  gset! game-gfx-tex sdl:new-texture-from-surface(lgfx:window new-surf)
  sdl:free-surface new-surf


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; color pattern for dirt/wall/etc. shading
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define dirt-pattern #nil)

define make-dirt-pattern(w h)
  define idx
  define rr
  define vv make-vector({w * h} sdl:rgbf(1 1 1))
  iterate
    init {idx := 0}
    repeat {idx <> vector-length(vv)}
      ;{rr := {1 - {game:random(32) / 96}}}
      {rr := {1 - {{game:random(8) * 4} / 128}}}
      {vv[idx] := sdl:rgbf(rr rr rr)}
      inc! idx
    else #void
  gset! dirt-pattern vv


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; cover/uncover code
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; decremented first
(constant uncover-anim-frames 152)  ;;120; 2.53*60
(define covered-array #nil)


define init-covering(xval)
  gset! covered-array make-vector({game:field-width * game:field-height} xval)

define deinit-covering()
  gset! covered-array #nil

;; xval: value to set
define do-covering(xval)
  ; original game uncovered one cell per line each frame.
  ; we have different cave sizes, so uncover width*height/40 random cells each frame. (original was width=40).
  ; this way the uncovering is the same speed also for intermissions.
  define left max(1 {{game:viewport-w * game:viewport-h} div 80})
  define x
  define y
  while {positive? left}
    {x := {game:random(game:viewport-w) + game:viewport-x}}
    {y := {game:random(game:viewport-h) + game:viewport-y}}
    {covered-array[{{y * game:field-width} + x}] := xval}
    dec! left


(load "bd-70-play-engine-40-cave-render.lsp")


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; main game loop helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; determine which amoeba/magic wall sound should be played
define amoeba-sound()
  define amb game:seen-active-amoeba?()
  define mag game:seen-active-magic-walls?()
  cond
    {amb and mag} play-sound-with-id('GD_S_AMOEBA_MAGIC)
    amb           play-sound-with-id('GD_S_AMOEBA)
    mag           play-sound-with-id('GD_S_MAGIC_WALL)
    else          stop-amoeba-magic-sounds()


;; is fire key pressed?
define fire-pressed?()
  cond
    {{gdec! game-state-fire-counter} <= 0}
      cond
        game:key-pressed?(game:KEY_SNAP)
          game:key-reset(game:KEY_SNAP)
          #t
        else
          #f
    ;; hack
    game:key-pressed?(game:KEY_DROP)
      game:key-reset(game:KEY_DROP)
      #t
    else
      #f


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; playsim state handlers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define cave-idx 0)
(define difficulty-level 0)

(define explosion-shake-time 0)
(define shake-delta 0)
(define was-hatch-shake #f)
(define perform-step #f)  ;; for single-stepping

(define saved-state)
(define saved-camera-x)
(define saved-camera-y)
(define saved-explosion-shake-time)
(define saved-shake-delta)
(define saved-was-hatch-shake)
(define saved-camera-catch-up)
(define saved-camera-scroll-cooldown)


define clear-saved-game-state()
  game:wipe-state saved-state
  gset! saved-state #nil

define save-current-game-state()
  if {in-game and {not game:in-replay?()} and {game-state-handler eq? game-state-normal}}
    then
      game:wipe-state saved-state
      gset! saved-state game:save-state()
      gset! saved-camera-x camera-x
      gset! saved-camera-y camera-y
      gset! saved-explosion-shake-time explosion-shake-time
      gset! saved-shake-delta shake-delta
      gset! saved-was-hatch-shake was-hatch-shake
      gset! saved-camera-catch-up camera-catch-up
      gset! saved-camera-scroll-cooldown camera-scroll-cooldown
      #t
    else
      #f

define load-current-game-state()
  if {in-game and {not game:in-replay?()} and {not-null? saved-state}}
    then
      game:load-state saved-state
      gset! camera-x saved-camera-x
      gset! camera-y saved-camera-y
      gset! explosion-shake-time saved-explosion-shake-time
      gset! shake-delta saved-shake-delta
      gset! was-hatch-shake saved-was-hatch-shake
      gset! camera-catch-up saved-camera-catch-up
      gset! camera-scroll-cooldown saved-camera-scroll-cooldown
      deinit-covering()
      gset! in-covering #nil
      gset! game-state-handler game-state-normal
      #t
    else
      #f


;; uncovering the cave
define game-state-uncover()
  cond
    {{not fire-pressed?()} and {not {null? in-covering}}}
      ;do-covering #f
      play-sound('GD_S_COVER)
    else
      ;reset-sounds()
      deinit-covering()
      gset! in-covering #nil
      gset! game-state-handler game-state-normal


;; covering the cave (player exited)
define game-state-exit-cover()
  cond
    {{not fire-pressed?()} and {not {null? in-covering}}}
      gset! ignore-any-sounds #t
      game:step()
      gset! ignore-any-sounds #f
      ;do-covering #t
      play-sound('GD_S_COVER)
    else
      reset-sounds()
      ;deinit-covering()
      ginc! cave-idx
      gset! reload-cave #t
      gset! need-recolor #t
      gset! in-covering #nil


;; covering the cave (player died)
define game-state-death-cover()
  cond
    {{not fire-pressed?()} and {not {null? in-covering}}}
      gset! ignore-any-sounds #t
      game:step()
      gset! ignore-any-sounds #f
      ;do-covering #t
      play-sound('GD_S_COVER)
    else
      reset-sounds()
      ;deinit-covering()
      gset! reload-cave #t
      gset! need-recolor #f
      gset! in-covering #nil


;; wait a little before covering (player exited)
define game-state-exit-wait()
  cond
    {{gdec! game-state-counter} <= 0}
      gset! game-state-counter uncover-anim-frames
      gset! in-covering #t
      init-covering #f
      gset! game-state-handler game-state-exit-cover
      gset! game-state-fire-counter 16
    else #void
  gset! ignore-any-sounds #t
  game:step()
  gset! ignore-any-sounds #f


;; wait a little before covering (player died)
define game-state-death-wait()
  cond
    {{gdec! game-state-counter} <= 0}
      if #t ;game:key-pressed?(game:KEY_SNAP)
        then
          gset! game-state-counter uncover-anim-frames
          gset! in-covering #t
          init-covering #f
          gset! game-state-handler game-state-death-cover
          gset! game-state-fire-counter 16
        else
          gset! game-state-counter 0
    else #void
  game:step()


;; main playsim state
define game-state-normal()
  game:step()
  if game:gate-flash
    then
      play-sound 'GD_S_ENOUGH_DIAMONDS
      gset! gate-flash-time -1
      gset! gate-flash-type 0
      gset! game:gate-flash #f
      ;printf "flash!\n"
      if exit-opened-shake
        then
          gset! explosion-shake-time game:random-range-incl(6 8)
          gset! shake-delta 1
  cond
    game:cave-finished
      play-sound 'GD_S_FINISHED
      gset! gate-flash-time -1
      gset! gate-flash-type 1
      gset! gate-flash-time 0  ;; no flash
      gset! game-state-handler game-state-exit-wait
      gset! game-state-counter 16
      record-cave-finished()
    game:player-dead
      gset! gate-flash-time -1
      gset! gate-flash-type 2
      if game:option-ref('intermission)
        gset! game-state-handler game-state-exit-wait
        gset! game-state-handler game-state-death-wait
      gset! game-state-counter 16
      record-cave-dead()
      if death-explosion-shake
        then
          gset! explosion-shake-time game:random-range-incl(8 12)
          gset! shake-delta 2
    else
      if {explosion-shake and game:was-explosion}
        then
          gset! explosion-shake-time game:random-range-incl(8 12)
          gset! shake-delta 2
      if {game:player-hatched and {not was-hatch-shake} and hatch-shake}
        then
          gset! was-hatch-shake #t
          gset! explosion-shake-time game:random-range-incl(6 8)
          gset! shake-delta 1


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; camera management (scrolling)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; camera offset from (0, 0)
;; (1 2) means that we should start rendering at (-1 -2)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define camera-catch-up #f)

define player-rel-x()
  {{{game:player-x * 16} - camera-x} - {scr-w div 2}}

define player-rel-y()
  {{{game:player-y * 16} - camera-y} - {scr-h div 2}}


define shift-camera-x(dx)
  assert {not-zero? dx}
  define ofs {min(abs(dx) scroll-speed) * sign(dx)}
  define new-cx {camera-x + ofs}
  {new-cx := min(new-cx {{{game:viewport-x + game:viewport-w} * 16} - scr-w})}
  {new-cx := max(new-cx {game:viewport-x * 16})}
  if {new-cx <> camera-x}
    then
      gset! camera-x new-cx
      gset! need-show #t
      gset! camera-catch-up #t

define shift-camera-y(dy)
  assert {not-zero? dy}
  define ofs {min(abs(dy) scroll-speed) * sign(dy)}
  define new-cy {camera-y + ofs}
  {new-cy := min(new-cy {{{game:viewport-y + game:viewport-h} * 16} - scr-h})}
  {new-cy := max(new-cy {game:viewport-y * 16})}
  if {new-cy <> camera-y}
    then
      gset! camera-y new-cy
      gset! need-show #t
      gset! camera-catch-up #t


define center-small-cave()
  if {{game:viewport-w * 16} <= scr-w}
    gset! camera-x {- {{{scr-w - {game:viewport-w * 16}} div 2} + {game:viewport-x * 16}}}
  if {{game:viewport-h * 16} <= scr-h}
    gset! camera-y {- {{{scr-h - {game:viewport-h * 16}} div 2} + {game:viewport-y * 16}}}


define perform-scroll()
  center-small-cave()
  ;
  define sdelta-x 0
  define sdelta-y 0
  if camera-catch-up
    then
      gset! camera-catch-up #f
    else
      {sdelta-x := scroll-offset-x}
      {sdelta-y := scroll-offset-y}
  ;
  if {zero? camera-scroll-cooldown}
    then
      gset! camera-scroll-cooldown scroll-delay
      define dx player-rel-x()
      define dy player-rel-y()
      if {{game:viewport-w * 16} > scr-w}
        if {abs(dx) > sdelta-x}
          shift-camera-x dx
      ;
      if {{game:viewport-h * 16} > scr-h}
        if {abs(dy) > sdelta-y}
          shift-camera-y dy
    else
      gset! camera-scroll-cooldown {camera-scroll-cooldown - 1}


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; timings
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; calc maximum wait timeout for SDL event fetcher
define calc-timeout(ntt)
  define fto max(0 {next-frame-time - ntt})
  define gto max(0 {next-game-time - ntt})
  define res min(fto gto)
  cond
    {res >= 1}
      trunc(res)
    {res > 0}
      1
    else
      #f

;; caculate next frame time
define calc-next-time(ntt curr step)
  define diff {trunc(ntt) - trunc(curr)}
  cond
    {diff > {step * 16}}  {ntt + step}
    {diff >= 0}           calc-next-time(ntt {curr + step} step)
    else                  curr


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; process game frame if the time has come
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define do-game-frame(ntt)
  if {trunc(ntt) >= trunc(next-game-time)}
    then
      ;printf "gst=%o\n" game-time
      gset! next-game-time calc-next-time(ntt next-game-time game-time)
      if {{not game-paused} and
          (or {not single-step-mode}
              perform-step
              game:cave-finished
              game:player-dead
              {not game:player-hatched})}
        then
          game-state-handler()
          gset! need-show #t
          if single-step-mode
            gset! perform-step #f
          if aggressive-gc do-gc()


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; process animation frame if the time has come
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define do-anim-frame(ntt)
  if {trunc(ntt) >= trunc(next-frame-time)}
    then
      gset! next-frame-time calc-next-time(ntt next-frame-time frame-time)
      if {{not game-paused} and {not {null? in-covering}}}
        then
          ;printf "!!! %o\n" in-covering
          if {{gdec! game-state-counter} <= 0}
            gset! in-covering #nil
            else
              do-covering in-covering
              gset! need-show #t
      if (and {not game-paused}
              game:player-hatched
              {not single-step-mode}
              {not game:player-dead}
              {not game:cave-finished})
          then
            ginc! rendered-frame-ticks frame-time
            while {rendered-frame-ticks >= 1000}
              gset! game:cave-total-time {game:cave-total-time + 1}
              gset! game:cave-time max(0 {game:cave-time - 1})
              gdec! rendered-frame-ticks 1000
      ;;
      perform-scroll()
      cond
        {negative? gate-flash-time}
          gset! gate-flash-time flash-duration-frames
          gset! need-show #t
        {positive? gate-flash-time}
          gdec! gate-flash-time
          gset! need-show #t
        else #f
      if {{not anim-paused} and {zero? gdec!(anim-wait)}}
        then
          gset! anim-wait anim-delay
          ;; advance animation frame
          gset! anim-frame {{anim-frame + 1} mod 8}
          gset! need-show #t


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; rendering
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define render-flash()
  define flash-alpha trunc({gate-flash-time * {64 / flash-duration-frames}})
  cond
    {gate-flash-type = 0}
      lgfx:set-draw-color sdl:rgba(255 255 255 flash-alpha)
    {gate-flash-type = 1}
      lgfx:set-draw-color sdl:rgba(255 255 0 flash-alpha)
    {gate-flash-type = 2}
      lgfx:set-draw-color sdl:rgba(255 0 0 {flash-alpha * 2})
    else
      lgfx:set-draw-color sdl:rgba(0 255 0 flash-alpha)
  lgfx:set-clip-rect scr-x-ofs scr-y-ofs scr-w scr-h
  lgfx:set-draw-blend-mode 'ADD
  lgfx:fill-rect()
  lgfx:set-draw-blend-mode 'NONE
  lgfx:set-draw-color sdl:rgb(0 0 0)
  lgfx:reset-clip-rect()

define do-render-frame()
  if need-show
    then
      define stt ticks-msec()
      lgfx:set-draw-color sdl:rgb(0 0 0)
      lgfx:clear()
      gset! explosion-shake-time max(0 {explosion-shake-time - 1})
      if sdl:texture?(game-gfx-tex)
        sdl-render-cave {- camera-x} {- camera-y}
          game:viewport-x
          game:viewport-y
          game:viewport-w
          game:viewport-h
          scr-x-ofs
          scr-w
          scr-y-ofs
          scr-h
          {positive? explosion-shake-time}
          shake-delta
      sdl-render-statusbar 0 0
      if {positive? gate-flash-time} render-flash()
      if con-visible
        sdl-render-console con-line-skip con-height window-width window-height
      lgfx:present()
      gset! need-show con-visible
      ;; load sounds here, so we'll have a game screen rendered first
      if {not {sounds-loaded}}
        then
          load-sounds()
          set-master-volume master-volume
          gset! sounds-loaded #t
          {stt := ticks-msec()}
          gset! next-frame-time {stt + frame-time}
          gset! next-game-time {stt + game-time}
      amoeba-sound()
      update-sounds()
      frame-time-report({ticks-msec() - stt})
      ;if {frame-time-report() > 6}
      ;  printf "***WARNING! cave render time is %o msecs!\n" frame-time-report()
      if {{ticks-msec() - last-timing-report} >= 1000}
        then
          gset! last-timing-report ticks-msec()
          sdl:set-window-title lgfx:window
            string-append("Miho Dash: frame: " number->string(frame-time-report())
                          "; step: " number->string(game:step-time-report()))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; load new cave, or reload the current one
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define load-cave()
  define fix-camera()
    define x0 {game:viewport-x * 16}
    define y0 {game:viewport-y * 16}
    define cend-x {x0 + max(0 {{game:viewport-w * 16} - scr-w})}
    define cend-y {y0 + max(0 {{game:viewport-h * 16} - scr-h})}
    gset! camera-x clamp(camera-x x0 cend-x)
    gset! camera-y clamp(camera-y y0 cend-y)
    gset! camera-scroll-cooldown 0
  ;;
  reset-sounds()
  game:flatten-bdcff cave-idx difficulty-level
  update-cave-credits(selected-game-id cave-idx difficulty-level)
  if {bytevector? action-replay-data}
    do-remove-reset-cave-stats()
    record-cave-start(selected-game-id cave-idx difficulty-level)
  gset! game-state-counter uncover-anim-frames
  gset! game-state-fire-counter 16
  gset! in-covering #f
  init-covering #t
  gset! game-state-handler game-state-uncover
  gset! reload-cave #f
  gset! need-show #t
  gset! rendered-frame-ticks 0
  gset! game-time game:cave-step-time
  if need-recolor
    then
      bjprng:seed-set! recolor-prng-ctx game:map-cave-hash-u32
      recolor-gfx-texture()
      make-dirt-pattern(game:max-field-width game:max-field-height)
      gset! need-recolor #f
  ;;
  gset! camera-catch-up #f
  fix-camera()
  ;;
  gset! explosion-shake-time 0
  gset! was-hatch-shake #f
  gset! ntt ticks-msec()
  gset! next-frame-time {ntt + frame-time}
  gset! next-game-time {ntt + game-time}
  gset! in-game #t
  gset! cheated #f
  center-small-cave()
  clear-saved-game-state()
  if {bytevector? action-replay-data}
    game:set-replay-data action-replay-data action-replay-otypes
    (game:start-recording)


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; process SDL events
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define got-quit-event #f)

define process-event(evt)
  define iistr
  define left 128
  ;;
  define do-console-autocompletion()
    define cpl concmd:collect-completions(con-cmd-line)
    cond
      {string? cpl}
        gset! con-cmd-line cpl
      else
        assert {string? car(cpl)} "oops!"
        assert {vector? cdr(cpl)} "oops!"
        gset! con-cmd-line car(cpl)
        let vloop <* idx 0 \\ vec cdr(cpl) *>
          if {idx <> vector-length(vec)}
            then
              if {zero? idx}
                printf "=== COMPLETIONS ===\n"
              printf " %s\n" vec[idx]
              vloop {idx + 1} vec
  ;;
  define console-keydown(evt)
    case (sdl:event-scan-name evt)
      (UP KP-8)
        if {{evt sdl:event-scan? "C-up"} or {evt sdl:event-scan? "C-kp-8"}}
          gset! con-height max(2 {con-height - 1})
          con-cmd-prev()
      (DOWN KP-2)
        if {{evt sdl:event-scan? "C-down"} or {evt sdl:event-scan? "C-kp-2"}}
          gset! con-height min(32 {con-height + 1})
          con-cmd-next()
      (PAGE-UP KP-9)
        gset! con-line-skip min(512 {con-line-skip + 3})
      (PAGE-DOWN KP-3)
        gset! con-line-skip max(0 {con-line-skip - 3})
      (ESCAPE)
        gset! con-visible #f
        gset! need-show #t
        (sdl:start-text-input)
      (BACKSPACE)
        con-cmd-put-char 8
      (RETURN)
        if {not {empty-string? con-cmd-line}}
          concmd:execute-string con-cmd-push(con-cmd-line)
      (TAB)
        con-cmd-put-char 9
      (Y)
        if {evt sdl:event-scan? "C-y"}
          con-cmd-put-char 1
      (GRAVE)
        if {empty-string? con-cmd-line}
          then
            gset! con-visible #f
            gset! con-line-skip 0
            gset! need-show #t
            (sdl:start-text-input)
      else #void
  ;;
  define game-keydown(evt)
    case (sdl:event-scan-name evt)
      (LEFT KP-4) game:key-down(game:KEY_LEFT)
      (RIGHT KP-6) game:key-down(game:KEY_RIGHT)
      (UP KP-8) game:key-down(game:KEY_UP)
      (DOWN KP-2) game:key-down(game:KEY_DOWN)
      (LEFT-CTRL RIGHT-CTRL) game:key-down(game:KEY_SNAP)
      (LEFT-ALT) game:key-down(game:KEY_DROP)
      (ESCAPE)
        ;; do not save aborted caves, because fuck why.
        ;; note: if the game is finished, do not remove!
        ;; otherwise we will lost the successfull replay.
        if {not game:cave-finished}
          do-remove-reset-cave-stats()
        gset! game-return-to-gui #t
      (SPACE)
        if in-game
          if single-step-mode
            then
              gset! perform-step #t
            else
              concmd:execute-string "pause toggle all"
          gset! game-return-to-gui #t
        gset! need-show #t
      (RETURN ENTER)
        if {not in-game}
          gset! game-return-to-gui #t
      (F2)
        if in-game
          concmd:execute-string "pause toggle game"
          gset! game-return-to-gui #t
        gset! need-show #t
      (F3)
        if in-game
          concmd:execute-string "pause toggle animation"
          gset! game-return-to-gui #t
        gset! need-show #t
      (F6)
        if save-current-game-state()
          printf "game state saved!\n"
      (F9)
        if load-current-game-state()
          printf "game state loaded!\n"
      (S)
        if {{evt sdl:event-scan? "M-s"} or {evt sdl:event-scan? "C-s"}}
          if save-current-game-state()
            printf "game state saved!\n"
      (V)
        if {{evt sdl:event-scan? "M-v"} or {evt sdl:event-scan? "C-v"}}
          if save-current-game-state()
            printf "game state saved!\n"
      (R)
        if {{evt sdl:event-scan? "M-r"} or {evt sdl:event-scan? "C-r"}}
          if load-current-game-state()
            printf "game state loaded!\n"
      (F12)
        if in-game
          if {evt sdl:event-scan? "M-f12"}
            concmd:execute-string "single-step toggle"
            concmd:execute-string "game-speed toggle"
      (H)
        if {evt sdl:event-scan? "M-h"}
          then
            record-cave-cheated()
            concmd:execute-string "mobiles toggle-harmless"
      (F)
        if {game:player-hatched and {evt sdl:event-scan? "M-f"}}
          then
            record-cave-skipped()
            concmd:execute-string "gate-open"
      (T)
        if (and in-game
                game:player-hatched
                {not game:cave-finished}
                {evt sdl:event-scan? "M-t"})
          then
            play-sound 'GD_S_FINISHED
            gset! game:cave-finished #t
            ;gset! con-cave-skipped #t
            record-cave-skipped()
            explode-no-center game:player-x game:player-y etype:O_EXPLODE_1
      (E)
        if {in-game and game:player-hatched and {evt sdl:event-scan? "M-e"}}
          concmd:execute-string "explode"
      (C)
        cond
          {evt sdl:event-scan? "M-c"}
            concmd:execute-string "recolor"
          else #void
      (X)
        cond
          {evt sdl:event-scan? "M-x"}
            record-cave-cancelled()
            concmd:execute-string "quit"
          else #void
      (Q)
        if {in-game and game:player-hatched and {evt sdl:event-scan? "M-q"}}
          concmd:execute-string "suicide"
      (GRAVE)
        if in-game
          then
            gset! con-visible #t
            gset! con-line-skip 0
            gset! need-show #t
            (sdl:start-text-input)
            ;; release playsim keys
            game:key-up(game:KEY_LEFT)
            game:key-up(game:KEY_RIGHT)
            game:key-up(game:KEY_UP)
            game:key-up(game:KEY_DOWN)
            game:key-up(game:KEY_SNAP)
            game:key-up(game:KEY_DROP)
      else #void
  ;;
  ;; main "process-event()" code
  if {false? (sdl:event-type evt)}
    {evt := (sdl:wait-event evt #f)}
  while (and {not got-quit-event}
             {positive? left}
             {not {false? (sdl:event-type evt)}})
    dec! left
    if {not flexgui:lgfx:window?()}
      gset! got-quit-event #t
    if {not got-quit-event}
      then
        case (sdl:event-type evt)
          (QUIT)
            gset! got-quit-event #t
          (KEY-DOWN)
            ;printf "DOWN: gtick=%o frt=%o grt=%o\n" game:tick {trunc(next-frame-time) - ticks-msec()} {trunc(next-game-time) - ticks-msec()}
            if con-visible
              console-keydown(evt)
              game-keydown(evt)
          (KEY-UP)
            ;printf "UP: gtick=%o frt=%o grt=%o\n" game:tick {trunc(next-frame-time) - ticks-msec()} {trunc(next-game-time) - ticks-msec()}
            if {not con-visible}
              case (sdl:event-scan-name evt)
                (LEFT KP-4) game:key-up(game:KEY_LEFT)
                (RIGHT KP-6) game:key-up(game:KEY_RIGHT)
                (UP KP-8) game:key-up(game:KEY_UP)
                (DOWN KP-2) game:key-up(game:KEY_DOWN)
                (LEFT-CTRL RIGHT-CTRL) game:key-up(game:KEY_SNAP)
                (LEFT-ALT) game:key-up(game:KEY_DROP)
                else #void
          (WINDOW)
            if {(sdl:event-window-action evt) eq? 'EXPOSED}
              gset! need-show #t
          (TEXT-INPUT)
            if con-visible
              then
                {iistr := (sdl:event-text evt)}
                while {not {empty-string? iistr}}
                  cond
                    {iistr[0] = #\`}
                      if {not {empty-string? con-cmd-line}}
                        con-cmd-put-char iistr[0]
                    between?(iistr[0] 32 127)
                      con-cmd-put-char iistr[0]
                    else
                      #void
                  {iistr := substring(iistr 1)}
                  gset! need-show #t
          else
            #void
        {evt := (sdl:wait-event evt #f)}
  #t


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; real main game loop
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define run-game-loop(a-cave-idx a-difficulty-level)
  ;;
  define game-finished?()
    {reload-cave and {cave-idx >= bdcff-loader:get-game-cave-count()}}
  ;;
  define replay-done?()
    if game:replay-finished?()
      {not camera-catch-up}
      #f
  ;;
  gset! cave-idx a-cave-idx
  gset! difficulty-level a-difficulty-level
  ;; main game loop code
  gset! explosion-shake-time 0
  gset! shake-delta 0
  gset! was-hatch-shake #f
  gset! perform-step #f  ;; for single-stepping
  gset! camera-catch-up #f
  gset! last-timing-report ticks-msec()
  ;;
  clear-saved-game-state()
  ;;
  gset! got-quit-event #f
  ;;
  while (and flexgui:lgfx:window?()
             {not got-quit-event}
             {not game-finished?()}
             ;{not replay-done?()}
             {not game-return-to-gui}
             process-event(evt))
    ;; game/animation advance
    cond
      reload-cave
        gset! gate-flash-time 0
        gset! gate-flash-type 0
        gset! need-show #t
        gset! reload-cave #f
        if {not game:replay-finished?()}
          load-cave()
      else #void
    ;; if replay finished, block the game logic
    if replay-done?()
      gset! in-game #f
    ;; timings
    gset! ntt ticks-msec()
    do-anim-frame ntt
    if in-game
      do-game-frame ntt
    do-render-frame()
    ;printf "*** to=%o\n" calc-timeout(ntt)
    gset! evt (sdl:wait-event evt calc-timeout(ntt))
  ;;
  ;; TODO: finish panning on replay!
  gset! in-game #f
  reset-sounds()
  clear-saved-game-state()
  ;;
  if cheated
    then
      do-remove-reset-cave-stats()
  ;;
  if {not flexgui:lgfx:window?()}
    gset! got-quit-event #t
  if got-quit-event
    lgfx:close-window()
    ;if {not game-return-to-gui}
    ;  wait-space-enter-esc()


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; not used
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
#|
define wait-space-enter-esc()
  define evt
  define done #f
  iterate
    init {evt := (sdl:wait-event evt #t)}
    exit done #void
    exit {{not flexgui:lgfx:window?()} or {false? (sdl:event-type evt)}} #void
    repeat #t
      case (sdl:event-type evt)
        (QUIT)
          {done := #t}
          lgfx:close-window()
        (KEY-DOWN)
          case (sdl:event-scan-name evt)
            (RETURN ENTER SPACE ESCAPE)
              {done := #t}
            else #void
        else #void
      {evt := (sdl:wait-event evt #t)}
|#

(end-module 'play-engine)
