#lang racket

(require games/gl-board-game/gl-board
         racket/gui
         racket/unit
         sgl/gl-vectors
         sgl
         sgl/gl
         srfi/25/array
         "honu-bitmaps.rkt")
(provide game@)

(define-struct image (width height rgba))

(define (argb->rgba argb)
  (let* ([length (bytes-length argb)]
         [rgba (make-gl-ubyte-vector length)])
    (let loop ((i 0))
      (when (< i length)
        (gl-vector-set! rgba (+ i 0) (bytes-ref argb (+ i 1)))
        (gl-vector-set! rgba (+ i 1) (bytes-ref argb (+ i 2)))
        (gl-vector-set! rgba (+ i 2) (bytes-ref argb (+ i 3)))
        (gl-vector-set! rgba (+ i 3) (bytes-ref argb (+ i 0)))
        (loop (+ i 4))))
    rgba))

(define (bitmap->argb bmp)
  (let* ([width (send bmp get-width)]
         [height (send bmp get-height)]
         [argb (make-bytes (* 4 width height) 255)]
         [dc (make-object bitmap-dc% bmp)])
    (send dc get-argb-pixels 0 0 width height argb #f)
    (when (send bmp get-loaded-mask)
      (send dc set-bitmap (send bmp get-loaded-mask))
      (send dc get-argb-pixels 0 0 width height argb #t))
    (send dc set-bitmap #f)
    argb))

(define (bitmap->image bmp)
  (make-image (send bmp get-width) (send bmp get-height)
              (argb->rgba (bitmap->argb bmp))))

#;(define light-square-img (bitmap->image (include-bitmap "light.jpg")))
(define light-square-img (bitmap->image (make-object bitmap% (build-path (collection-file-path "light.jpg" "games" "checkers")))))
(define light-square-color (gl-float-vector .7216 .6471 .5176 1))
(define dark-square-img (bitmap->image (make-object bitmap% (build-path (collection-file-path "dark.jpg" "games" "checkers")))))
(define dark-square-color (gl-float-vector .4745 .3569 .2627 1))

(define (color-name->vector name darken?)
  (let ([color (send the-color-database find-color name)]
        [adj (if darken? sqr values)])
    (unless color
      (error 'color-name->vector "could not find ~e" name))
    (gl-float-vector (adj (/ (send color red) 255))
                     (adj (/ (send color green) 255))
                     (adj (/ (send color blue) 255))
                     1.0)))

(define light-checker-img (bitmap->image honu-down-bitmap))
(define dark-checker-img (bitmap->image honu-bitmap))

(define-struct space-info (x y light?))
(define-struct piece-info (x y color king?) #:inspector (make-inspector))
(define-struct moves (list forced-jump?))

(define-signature model^
  (move))
(define-signature view^
  (add-space add-piece remove-piece move-piece set-turn show))

(define-unit view@
  (import model^)
  (export view^)

  (define (get-space-draw-fn space)
    (let* ([list-id (get-square-dl (space-info-light? space)
                                   (send texture-box get-value))]
           [sx (space-info-x space)]
           [sy (space-info-y space)])
      (lambda ()
        (gl-push-matrix)
        (gl-translate sx sy 0)
        (gl-call-list list-id)
        (gl-pop-matrix))))

  (define (add-space space)
    (send board add-space (get-space-draw-fn space) space))

  (define (get-piece-draw-fn piece glow?)
    (let ([list-id (get-checker-dl (eq? 'red (piece-info-color piece))
                                   (piece-info-king? piece)
                                   (send texture-box get-value))])
      (if glow?
        (lambda (for-shadow?)
          (gl-material-v 'front 'emission (gl-float-vector 0.15 0.15 0.15 1.0))
          (gl-call-list ((if for-shadow? cdr car) list-id))
          (gl-material-v 'front 'emission (gl-float-vector 0.0 0.0 0.0 1.0)))
        (lambda (for-shadow?)
          (gl-call-list ((if for-shadow? cdr car) list-id))))))

  (define add-piece
    (case-lambda
      [(piece) (add-piece piece #f)]
      [(piece glow?)
       (send board add-piece
             (+ .5 (piece-info-x piece)) (+ .5 (piece-info-y piece)) 0.0
             (get-piece-draw-fn piece glow?)
             piece)]))

  (define (move-piece from to-x to-y)
    (remove-piece from)
    (add-piece (make-piece-info to-x to-y
                                (piece-info-color from)
                                (piece-info-king? from))))

  (define (remove-piece p)
    (send board remove-piece p))

  (define (internal-move old move-to)
    (when (piece-info? old) (move old move-to)))

  (define (set-turn turn moves)
    (let ([pieces (send board get-pieces)])
      (for-each (lambda (p)
                  (send board set-piece-draw p (get-piece-draw-fn p #f))
                  (send board enable-piece p #f))
                pieces)
      (for-each (lambda (p)
                  (send board set-piece-draw p (get-piece-draw-fn p #t))
                  (send board enable-piece p #t))
                (moves-list moves)))
    (send msg set-label
          (if (null? (moves-list moves))
            (format "~a wins!" (if (eq? turn 'red) "Black" "Red"))
            (format "~a's turn~a"
                    (if (eq? turn 'red) "Red" "Black")
                    (if (moves-forced-jump? moves) " - must take jump" "")))))

  (define f (new frame% (label "Checkers") (width 800) (height 600)))
  (define board
    (new gl-board% (parent f) (who "Checkers")
         (min-x 0.0) (max-x 8.0) (min-y 0.0) (max-y 8.0)
         (lift .35)
         (move internal-move)))
  (define hp (new horizontal-pane% (parent f) (stretchable-height #f)))
  (define msg
    (new message% (label "") (parent hp) (stretchable-width #t)))
  (define texture-box
    (new check-box% (label "Textured") (parent hp)
         (callback
          (lambda (box _)
            (for-each
             (lambda (s)
               (send board set-space-draw s (get-space-draw-fn s)))
             (send board get-spaces))
            (for-each
             (lambda (p)
               (send board set-piece-draw p
                     (get-piece-draw-fn p (send board enabled? p))))
             (send board get-pieces))
            (send board refresh)))))
  (new grow-box-spacer-pane% [parent hp])
  (send texture-box set-value #t)

  (define q
    (send board with-gl-context (lambda () (gl-new-quadric))))

  (define-values (dark-tex light-tex dark-checker-tex light-checker-tex)
    (send board with-gl-context
          (lambda ()
            (let ((x (glGenTextures 4)))
              (values (gl-vector-ref x 0)
                      (gl-vector-ref x 1)
                      (gl-vector-ref x 2)
                      (gl-vector-ref x 3))))))

  (define (init-tex tex img)
    (send board with-gl-context
          (lambda ()
            (glBindTexture GL_TEXTURE_2D tex)
            (glTexParameteri GL_TEXTURE_2D GL_TEXTURE_MIN_FILTER GL_LINEAR)
            (glTexParameteri GL_TEXTURE_2D GL_TEXTURE_MAG_FILTER GL_LINEAR)
            (glTexParameteri GL_TEXTURE_2D GL_TEXTURE_WRAP_S GL_CLAMP)
            (glTexParameteri GL_TEXTURE_2D GL_TEXTURE_WRAP_T GL_CLAMP)
            (glTexImage2D GL_TEXTURE_2D 0 GL_RGBA
                          (image-width img) (image-height img) 0
                          GL_RGBA GL_UNSIGNED_BYTE (image-rgba img)))))

  (init-tex light-tex light-square-img)
  (init-tex dark-tex dark-square-img)
  (init-tex dark-checker-tex dark-checker-img)
  (init-tex light-checker-tex light-checker-img)

  (define (make-piece-dl color height tex shadow?)
    (send board with-gl-context
          (lambda ()
            (let ([list-id (gl-gen-lists 1)])
              (gl-quadric-draw-style q 'fill)
              (gl-quadric-normals q 'smooth)
              (gl-new-list list-id 'compile)

              (when shadow? (gl-disable 'lighting))
              (gl-material-v 'front 'specular (gl-float-vector 1.0 1.0 1.0 1.0))
              (gl-material 'front 'shininess 120.0)  

              (gl-material-v 'front 'ambient-and-diffuse color)
              (gl-cylinder q .35 .35 height 25 1)
              (gl-push-matrix)
              (gl-translate 0.0 0.0 height)

              (when (and tex (not shadow?))
                (gl-enable 'texture-2d)
                (glBindTexture GL_TEXTURE_2D tex)
                (glTexEnvf GL_TEXTURE_ENV GL_TEXTURE_ENV_MODE GL_DECAL)
                (gl-quadric-texture q #t))

              (gl-disk q 0.0 .35 25 1)

              (when (and tex (not shadow?))
                (gl-quadric-texture q #f)
                (glTexEnvf GL_TEXTURE_ENV GL_TEXTURE_ENV_MODE GL_MODULATE)
                (gl-disable 'texture-2d))

              (gl-pop-matrix)

              (when shadow? (gl-enable 'lighting))
              (gl-end-list)
              list-id))))

  (define (make-tex-square-dl tex)
    (send board with-gl-context
          (lambda ()
            (let ([list-id (gl-gen-lists 1)])
              (gl-new-list list-id 'compile)
              (gl-enable 'texture-2d)
              (glBindTexture GL_TEXTURE_2D tex)
              (gl-material-v 'front 'ambient-and-diffuse
                             (gl-float-vector 1 1 1 1))
              (gl-begin 'polygon)
              (gl-tex-coord 0.0 0.0)
              (gl-vertex 0.0 0.0 0.0)
              (gl-tex-coord 1.0 0.0)
              (gl-vertex 1.0 0.0 0.0)
              (gl-tex-coord 1.0 1.0)
              (gl-vertex 1.0 1.0 0.0)
              (gl-tex-coord 0.0 1.0)
              (gl-vertex 0.0 1.0 0.0)
              (gl-end)
              (gl-disable 'texture-2d)
              (gl-end-list)
              list-id))))

  (define (make-square-dl color)
    (send board with-gl-context
          (lambda ()
            (let ([list-id (gl-gen-lists 1)])
              (gl-new-list list-id 'compile)
              (gl-material-v 'front 'ambient-and-diffuse color)
              (gl-begin 'polygon)
              (gl-vertex 0.0 0.0 0.0)
              (gl-vertex 1.0 0.0 0.0)
              (gl-vertex 1.0 1.0 0.0)
              (gl-vertex 0.0 1.0 0.0)
              (gl-end)
              (gl-end-list)
              list-id))))

  (define checkers
    (map (lambda (x)
           (let ([color (if (car x)
                            (color-name->vector "firebrick" #t)
                            (gl-float-vector 0.15 0.15 0.15 1.0))]
                 [height (if (cadr x) .4 .2)]
                 [tex (if (caddr x)
                        (if (car x) light-checker-tex dark-checker-tex)
                        #f)])
             (cons x (cons (make-piece-dl color height tex #f)
                           (make-piece-dl color height tex #t)))))
         '((#f #f #f)
           (#f #f #t)
           (#f #t #f)
           (#f #t #t)
           (#t #f #f)
           (#t #f #t)
           (#t #t #f)
           (#t #t #t))))
  (define (get-checker-dl light? king? tex?)
    (cdr (assoc (list light? king? tex?) checkers)))

  (define dark-square (cons (make-tex-square-dl dark-tex)
                            (make-square-dl dark-square-color)))
  (define light-square (cons (make-tex-square-dl light-tex)
                             (make-square-dl light-square-color)))
  (define (get-square-dl light? tex?)
    (let ((getter (if tex? car cdr)))
      (getter (if light? light-square dark-square))))

  (define (show) (send f show #t)))

(define-unit model@
  (import view^)
  (export model^)

  (define turn 'red)
  (define board (make-array (shape 0 8 0 8) #f))

  (let loop ([i 0] [j 0])
    (cond
      [(and (< j 8) (< i 8))
       (cond
         [(even? (+ i j))
          (add-space (make-space-info j i #f))
          (cond [(< i 3)
                 (array-set! board j i (cons 'red #f))
                 (add-piece (make-piece-info j i 'red #f))]
                [(> i 4)
                 (array-set! board j i (cons 'black #f))
                 (add-piece (make-piece-info j i 'black #f))])]
         [else (add-space (make-space-info j i #t))])
       (loop i (add1 j))]
      [(< i 8) (loop (add1 i) 0)]))

  (define (other-color c)
    (if (eq? c 'red) 'black 'red))

  (define (single-move-ok? direction from-x from-y to-x to-y)
    (and (= to-y (+ direction from-y))
         (= 1 (abs (- from-x to-x)))))

  (define (can-move? direction from-x from-y)
    (and (<= 0 (+ from-y direction) 7)
         (or (and (<= 0 (+ from-x 1) 7)
                  (not (array-ref board (+ from-x 1) (+ from-y direction))))
             (and (<= 0 (+ from-x -1) 7)
                  (not (array-ref board (+ from-x -1) (+ from-y direction)))))))

  (define (get-jumped-piece color direction from-x from-y to-x to-y)
    (and (= to-y (+ direction direction from-y))
         (= 2 (abs (- from-x to-x)))
         (let* ([jumped-x (+ from-x (/ (- to-x from-x) 2))]
                [jumped-y (+ from-y direction)]
                [jumped-piece (array-ref board jumped-x jumped-y)])
           (and jumped-piece
                (eq? (other-color color) (car jumped-piece))
                (make-piece-info jumped-x jumped-y
                                 (car jumped-piece) (cdr jumped-piece))))))

  (define (can-jump? direction from-color from-x from-y)
    (let ([to-y (+ direction direction from-y)]
          [to-x1 (+ from-x 2)]
          [to-x2 (- from-x 2)])
      (and (<= 0 to-y 7)
           (or (and (<= 0 to-x1 7)
                    (not (array-ref board to-x1 to-y))
                    (get-jumped-piece from-color direction
                                      from-x from-y
                                      to-x1 to-y))
               (and (<= 0 to-x2)
                    (not (array-ref board to-x2 to-y))
                    (get-jumped-piece from-color direction
                                      from-x from-y
                                      to-x2 to-y))))))

  (define (fold-board f v)
    (let iloop ([i 0] [v v])
      (if (= i 8)
        v
        (let jloop ([j 0] [v v])
          (if (= j 8)
            (iloop (add1 i) v)
            (jloop (add1 j) (if (even? (+ i j)) (f i j v) v)))))))

  (define (get-jump-moves)
    (let ([direction (if (eq? turn 'red) 1 -1)])
      (fold-board
       (lambda (i j l)
         (let ([p (array-ref board i j)])
           (if (and p
                    (eq? (car p) turn)
                    (or (can-jump? direction turn i j)
                        (and (cdr p)
                             (can-jump? (- direction) turn i j))))
             (cons (make-piece-info i j turn (cdr p)) l)
             l)))
       null)))

  (define (get-moves)
    (let ([jumps (get-jump-moves)])
      (if (pair? jumps)
        (make-moves jumps #t)
        (make-moves
         (let ([direction (if (eq? turn 'red) 1 -1)])
           (fold-board
            (lambda (i j l)
              (let ([p (array-ref board i j)])
                (if (and p
                         (eq? (car p) turn)
                         (or (can-move? direction i j)
                             (and (cdr p) (can-move? (- direction) i j))))
                  (cons (make-piece-info i j turn (cdr p)) l)
                  l)))
            null))
         #f))))

  (define (move from to)
    (let* ([to-x (inexact->exact (floor (gl-vector-ref to 0)))]
           [to-y (inexact->exact (floor (gl-vector-ref to 1)))]
           [from-x (piece-info-x from)]
           [from-y (piece-info-y from)]
           [from-color (piece-info-color from)]
           [from-king? (piece-info-king? from)]
           [to-king? (or from-king? (= to-y (if (eq? 'red from-color) 7 0)))]
           [direction (if (eq? turn 'red) 1 -1)])
      (when (and (eq? turn from-color)
                 (<= 0 to-x 7)
                 (<= 0 to-y 7)
                 (not (array-ref board to-x to-y)))
        (cond [(and (null? (get-jump-moves))
		    (or (single-move-ok? direction from-x from-y to-x to-y)
			(and from-king?
			     (single-move-ok? (- direction) from-x from-y
                                              to-x to-y))))
	       (move-piece from to-x to-y)
               (set! turn (other-color from-color))
               (array-set! board to-x to-y (cons from-color to-king?))
               (array-set! board from-x from-y #f)
               (when (and to-king? (not from-king?))
                 (remove-piece (make-piece-info to-x to-y from-color from-king?))
                 (add-piece (make-piece-info to-x to-y from-color to-king?)))
	       (set-turn turn (get-moves))]
              [(or (get-jumped-piece from-color direction from-x from-y
                                     to-x to-y)
		   (and from-king?
			(get-jumped-piece from-color (- direction) from-x from-y to-x to-y)))
               =>
               (lambda (j)
                 (remove-piece j)
                 (move-piece from to-x to-y)
                 (array-set! board (piece-info-x j) (piece-info-y j) #f)
                 (array-set! board from-x from-y #f)
                 (array-set! board to-x to-y (cons from-color to-king?))
                 (when (and to-king? (not from-king?))
                   (remove-piece (make-piece-info to-x to-y from-color from-king?))
                   (add-piece (make-piece-info to-x to-y from-color to-king?)))
                 (cond
                   [(or (can-jump? direction from-color to-x to-y)
                        (and from-king?
                             (can-jump? (- direction) from-color to-x to-y)))
                    (set-turn turn
                              (make-moves (list (make-piece-info
                                                 to-x to-y from-color to-king?))
                                          #t))]
                   [else
                    (set! turn (other-color from-color))
                    (set-turn turn (get-moves))]))]))))

  (set-turn turn (get-moves))
  )

(define-unit show@
  (import view^)
  (export)
  (show))

(define game@
  (compound-unit/infer (import) (export) (link view@ model@ show@)))
