#!/usr/bin/env bash

# -*- mode: scheme -*-

export GUILE_LOAD_PATH=$HOME/.local/share/guile/site/2.2
export GUILE_LOAD_COMPILED_PATH=$HOME/.local/lib/guile/2.2/site-ccache

exec guile -q -e main -s $0
!#

;; (add-to-load-path "/usr/local/share/guile/site/2.2/")
(add-to-load-path (dirname (current-filename)))

(define (media str)
  (string-append (dirname (current-filename)) str))

(use-modules (srfi srfi-1)
             (srfi srfi-27)

             (sdl2)
             (sdl2 ttf)
             (sdl2 video)
             (sdl2 image)
             (sdl2 rect)
             (sdl2 render)
             (sdl2 surface)
             (sdl2 events)

             (rnrs base)

             (ice-9 threads)
             (ice-9 atomic)

             (util)
             (map)
             (class)
             (draw)
             )

(define-values (wait wake)
  (let ((mutex (make-mutex))
        (condvar (make-condition-variable)))
    (values
     (lambda () (with-mutex mutex (wait-condition-variable condvar mutex)))
     (lambda () (broadcast-condition-variable condvar)))))

(define texture-map (make-hash-table))

(define ray-count 64)

(define rays (make-atomic-box '()))

(define game-map (call-with-input-file "b-huset.map" (compose parse-map read-map)))

(define player (make-player (+ 1/2 (car (board-spawn game-map)))
                            (+ 1/2 (cadr (board-spawn game-map)))
                            0.1))

(define update
  (let ((keys-down '()))
    (lambda (dt)                             ; dt in ms

      (let ((ev (poll-event)))
        (cond #; [(keyboard-event-repeat? ev) #f] ;; TODO implement in bindings

         [(keyboard-down-event? ev)
          (set! keys-down (lset-adjoin eq? keys-down (keyboard-event-scancode ev)))]

         [(keyboard-up-event? ev)
          (set! keys-down (delv (keyboard-event-scancode ev) keys-down))]

         [(quit-event? ev) (raise SIGINT)]))

      (for-each
       (lambda (key)
         (case key
           ((x q) (raise SIGINT))
           ((w s) (set! (p player)
                        = (+ (* dt 6e-3 (if (eq? key 'w) 1 -1)
                                (v3 (cos (a player))
                                    (sin (a player))))))
            (wake))
           ((j) (set! (a player) = (- (* dt 4e-3)))
            (when (> 0 (a player))
              (set! (a player) tau))
            (wake))

           ((p) (set! (a player) = (+ (* dt 4e-3)))
            (when (< tau (a player))
              (set! (a player) 0))
            (wake))

           ((a d) (set! (p player)
                      = (+ (* dt 3e-3 (if (eq? key 'a) 1 -1)
                            (v3 (sin (a player))
                                (- (cos (a player)))))))
            (wake))
          ))
       keys-down))))

(define (find-next-edge x y a callback)
  (let ((dx-ampl (mod (* -1 (sgn (cos a)) (decimals x)) 1))
        (dy-ampl (mod (* -1 (sgn (sin a)) (decimals y)) 1)))
    (let* ((dx (* 1.01 dx-ampl (sgn (cos a))))
           (dy (* dx (tan a))))
      (if (<= (abs dy) dy-ampl)
          (callback (+ x dx) (+ y dy) 'x)
          (let* ((dy (* 1.01 dy-ampl (sgn (sin a))))
                 (dx (* dy (cot a))))
            (callback (+ x dx) (+ y dy) 'y))))))

(define (ray-trace player ray-count)
  (let ((p (p player)))
    (let ((px (x p))
          (py (y p)))
      (map (lambda (a)
             (let loop ((x px) (y py) (dir 'x))
               (cond [(or (not (and (<= 0 x) (< x (board-width game-map))
                                    (<= 0 y) (< y (board-height game-map))))
                          (< 15 (sqrt (+ (expt (- x px) 2)
                                         (expt (- y py) 2)))))
                      ;; outside board
                      (list (make-ray #:a a #:type #f
                                      #:v (v3 (- x px) (- y py))
                                      #:hitf (v3 x y))) ]

                     ;; hit wall
                     [(array-ref (board-data game-map) (int y) (int x))
                      (lambda (tile) (memv tile '(wall)))
                      => (lambda (tile)
                           (format #t "start!!!~%")
                           (list (make-ray #:a a #:type tile
                                           #:v (v3 (- x px) (- y py))
                                           #:hitf (v3 x y)
                                           #:htw
                                           ;; (random-integer 5)
                                           ;; för a > 0: 
                                           (let loop ((htl 1)
                                                      (x (- x (* 0.1 (sgn (cos a)))))
                                                      (y (- y (* 0.1 (sgn (sin a)))))
                                                      (a (case dir
                                                          ((x) (mod (- a) tau))
                                                          ((y) (mod (- pi a) tau)))))
                                             (find-next-edge
                                              x y a
                                              (lambda (x y dir)
                                                ;; (format #t "x = ~a, y = ~a, a = ~a~%"
                                                ;;         x y a)
                                                (cond
                                                 [(or (not (and (<= 0 x) (< x (board-width game-map))
                                                                (<= 0 y) (< y (board-height game-map))))
                                                      #; 
                                                      (< 15 (sqrt (+ (expt (- x px) 2) ;
                                                      (expt (- y py) 2)))))
                                                  -1
                                                  ]



                                                 [(eq? 'lamp
                                                       (array-ref (board-data game-map)
                                                                  (int y) (int x)))
                                                  htl]

                                                 [(> htl 10)
                                                  -1]

                                                 [(array-ref (board-data game-map) (int y) (int x))
                                                  (lambda (tile) (memv tile '(wall)))
                                                  => (lambda _
                                                       (loop (1+ htl)
                                                             (- x (* 0.1 (sgn (cos a))))
                                                             (- y (* 0.1 (sgn (sin a))))
                                                             (case dir
                                                               ((x) (mod (- a) tau))
                                                               ((y) (mod (- pi a) tau)))))]
                                                 [else
                                                  (loop htl
                                                        x y a
                                                        #; (mod (+ pi a) tau)
                                                        )]))))))
                           )]

                     [(array-ref (board-data game-map) (int y) (int x))
                      (lambda (tile) (memv tile '(lamp)))
                      => (lambda (tile)
                           (list (make-ray #:a a #:type tile
                                           #:v (v3 (- x px) (- y py))
                                           #:htw 0
                                           #:hitf (v3 x y))))]

                     ;; hit window
                     [(array-ref (board-data game-map) (int y) (int x))
                      (lambda (tile) (memv tile '(window)))
                      => (lambda (tile)
                           (cons (make-ray #:a a #:type tile
                                           #:v (v3 (- x px) (- y py))
                                           #:hitf (v3 x y))
                                 (find-next-edge x y a loop)))]

                     ;; follow ray
                     [else (find-next-edge x y a loop)])))

           (iota ray-count (- (a player) (/ (fov player) 2))
                 (/ (fov player) ray-count)))))
  )

(define (draw-map)
  (set-draw-color #xFF #xFF #xFF)
  (clear)

  ;; draw level
  (for-each
   (lambda (pt)
     (apply set-draw-color
      (case (array-ref (board-data game-map) (y pt) (x pt))
        ((wall)   '(#xBB #xBB #xBB))
        ((window) '(#x10 #x10 #xFF))
        ((lamp)   '(#xFF #xFF #x0))
        ((grass)  '(0 #xCC 0))
        (else     '(#xFF #xFF #xFF))))
     (fill-rect (x pt) (y pt) 1 1))
   (map (lambda (p) (apply v3 p))
        (cross-product (iota (board-width game-map)) (iota (board-height game-map)))))

  ;; draw raycast
  (set-draw-color #xFF 0 0)

  (let ((pp (p player)))
    (for-each
     (lambda (rays)
       (for-each
        (lambda (ray)
          (let ((v (+ pp (vec ray))))
            (draw-line (x pp) (y pp)
                       (x v)  (y v))))
        rays))
     (atomic-box-ref rays)))
  )

(define (draw-first-person-perspective)
  (set-draw-color #xBB #xBB #xFF)       ; light blue
  (clear)

  ;; floor
  (set-draw-color #x33 #x33 #x33)
  (render-fill-rect (current-renderer)
                    (make-rect 0 240 640 240))

  (let ((rays (atomic-box-ref rays)))
    (unless (null? rays)
      (for-each
       (lambda (i rays)
         (for-each
          (lambda (r)
            (cond ((hashq-ref texture-map (type r))
                   => (lambda (texture)
                        (let* ((l (length r))
                               (segment-height (int (- 480 (* 30 l) 40))))

                          ;; 0, 5
                            ;; (format #t "HTL = ~a~%" (hits-to-wall r))

                          (when (= -1 (hits-to-wall r))
                            (let ((c 0))
                              ;; add light effect
                              (set-texture-color-mod! texture c c c)))

                          (when (< 0 (hits-to-wall r))
                            (let ((c (max 0 (int (- #xFF (* (/ (hits-to-wall r)) l 10))))))
                              ;; add light effect
                              (set-texture-color-mod! texture c c c)))

                          (render-copy
                           (current-renderer)
                           texture
                           #:dstrect (list (floor/ (* i 640) ray-count)
                                           (floor/ (- 480 segment-height) 2)
                                           (floor/ 640 ray-count)
                                           segment-height)


                           #:srcrect
                           (list (int (* 16 (pos-on-wall r))) 0
                                 1 16)))))))
          rays))
       (iota ray-count)
       rays
       ))))




(define color (make-parameter (make-color 0 0 0 #xFF)))
(define* (render-text str #:key (line 0))
  (let ((surf (render-font-blended (current-font) str (color))))
    (render-copy (current-renderer)
                 (surface->texture (current-renderer) surf)
                 #:dstrect (list 5 (+ 1 (* line (+ 1 (font-height (current-font)))))
                                 (surface-width surf)
                                 (surface-height surf)))
    (delete-surface! surf)))



(define (with-render-target target thunk)
  (dynamic-wind (lambda () (set-render-target! (current-renderer) target))
                thunk
                (lambda () (set-render-target! (current-renderer) #f))))

(define (make-fps-counter)
  (let ((last-time (sdl-ticks)))
    (lambda ()
      ;; TODO this only ticks when sdl is active, which is NOT when
      ;; the window is unmapped
      (let ((new-time (sdl-ticks)))
        (let ((return (- new-time last-time)))
          (set! last-time new-time)
          return)))))

(define draw
  (let ((fps-counter (make-fps-counter)))
    (lambda ( window rend)
      (parameterize ((current-renderer rend)
                     (current-tile-size (/ 480 (board-height game-map))))

        ;; minimap
        (let ((texture (make-texture rend 'rgba8888 'target
                                     (int (* (board-width game-map)  (current-tile-size)))
                                     (* (board-height game-map) (current-tile-size)))))
          (with-render-target texture draw-map)

          ;; Camera
          (draw-first-person-perspective)

          (render-copy rend texture
                       #:dstrect
                       (let ((map-size 100))
                         (list 0 (- 480 map-size)
                               map-size map-size)))

          (delete-texture! texture))

        ;; Text overlay
        (parameterize ((color (make-color 0 0 0 #xFF)))

          ;; FPS counter
          (render-text (format #f "FPS: ~,2f" (/ 1000 (1+ (fps-counter)))) #:line 0)

          (render-text
           (format #f "x = ~,4f   y = ~,4f   a = ~,4f"
                   (x (p player)) (y (p player)) (a player))
           #:line 1))

        (present)))))

(define (main-loop window)
  (define rend (make-renderer window '(accelerated #; vsync texture)))
  (define fps-counter (make-fps-counter))

  (begin-thread
   (while
       #t (let ((rays-next (ray-trace player ray-count)))
            (atomic-box-set! rays rays-next)
            (wait))))

  ;; load textures
  (for-each
   (lambda (type)
     (hashq-set! texture-map type
                 (surface->texture rend (load-image (media (format #f "/textures/~a.png" type))))))
   '(wall window))

  (parameterize ((current-renderer rend))
    (let ((lamp-text (make-texture rend 'rgba8888 'target 16 16)))
      (format #t "lamp = ~a~%" lamp-text)
      (with-render-target
       lamp-text
       (lambda ()
         (set-draw-color #xFF #xFF #x0)
         (clear)))
      (hashq-set! texture-map 'lamp lamp-text)))

  (let ((counter 0))
    (while #t
           (sigaction SIGINT (lambda (sig) (break)))

           (if (= counter 1)
               (begin (draw window rend) (set! counter 0))
               (let ((dt (fps-counter)))
                 (update dt)
                 (usleep (max 0 (floor/ (- 1000 dt) 360)))
                 (set! counter = (+ 1)))))))

(define current-font (make-parameter #f))

(define (main args)
  (sdl-init)
  (ttf-init)

  (current-font (load-font (media "/font.otf") 20))

  ;; (let ((sock-path "/tmp/guile-socket"))
  ;;   (use-modules (system repl server))
  ;;   (delete-file sock-path)
  ;;   (spawn-server (make-unix-domain-server-socket #:path sock-path)))

  (call-with-window
   (make-window
    #:title "Wolf 3D"
    #:size '(640 480))
   main-loop)

  (ttf-quit)
  (sdl-quit))