#lang mzscheme
(require games/cards mred mzlib/class mzlib/unit mzlib/list)

(provide game@)

(define game@ (unit (import) (export)

;; Layout width and height:
(define WIDTH 5)
(define HEIGHT 4)
(define MAX-MATCHES (/ (* WIDTH HEIGHT) 2))

;; Randomize
(random-seed (modulo (current-milliseconds) 10000))

;; Set up the table
(define t (make-table "Memory" (+ 2 WIDTH) (+ 1 HEIGHT)))
(send t show #t)
(send t set-double-click-action #f)

;; Get table width & height
(define w (send t table-width))
(define h (send t table-height))

;; Set up the cards
(define deck 
  (let ([cards (map (lambda (name value)
                      (let ([bm (make-object
                                 bitmap%
                                 (build-path
                                  (collection-path "games" "memory" "images")
                                  (format "~a.png" name)))])
                        (make-card bm #f 0 value)))
                    '("club" "heart" "spade" "diamond"
                      "happy" "unhappy"
                      "fish" "two-fish"
                      "jack" "star")
                    '(1 2 3 4 5 6 7 8 9 10))])
    (append cards (map (lambda (c) (send c copy)) cards))))
(for-each (lambda (card)
            (send card user-can-move #f)
            (send card user-can-flip #t))
          deck)

;; Card width & height
(define cw (send (car deck) card-width))
(define ch (send (car deck) card-height))

(define dx (/ cw (+ 2 WIDTH)))
(define dy (/ ch (+ 1 HEIGHT)))

(define match-x (- w cw dx))
(define match-y dy)

(define time-h (+ 12 5 5))
(define time-x match-x)
(define time-y (+ ch dy dy))

;; Put the cards on the table
(send t add-cards deck match-x match-y)

;; Setup
(define (setup)
  (reset-timer)
  (set! deck (shuffle-list deck 7))
  (send t stack-cards deck)
  (send t move-cards deck 0 0
        (lambda (pos)
          (let ([i (modulo pos WIDTH)]
                [j (quotient pos WIDTH)])
            (values (+ dx (* i (+ cw dx)))
                    (+ dy (* j (+ ch dy))))))))

;; Number of matches found so far:
(define matches 0)

;; First card flipped, or #f if non flipped, yet
(define card-1 #f)

(define (flip-and-match c)
  (cond [(eq? c card-1)
         ;; Cancel first card
         (send t flip-card c)
         (set! card-1 #f)]
        [(not (send c face-down?))
         ;; Can't click a matched card, unless the game is over, 
         ;; in which case we reset the game
         (when (= matches MAX-MATCHES)
           (send t flip-cards deck)
           (set! matches 0)
           (setup))]
        [else
         ;; Flip over a card...
         (send t flip-card c)
         (send t card-to-front c)
         (run-timer)
         (cond [(not card-1)
                ;; That was the first card
                (set! card-1 c)]
               [(and (equal? (send card-1 get-value) (send c get-value))
                     (equal? (send card-1 get-suit) (send c get-suit)))
                ;; Match
                (send t pause 0.5)
                (send t move-cards (list card-1 c) match-x match-y)
                (set! card-1 #f)
                (set! matches (add1 matches))]
               [else
                ;; Not a match
                (send t pause 0.5)
                (send t flip-cards (list card-1 c))
                (set! card-1 #f)])]))
(send t set-single-click-action flip-and-match)

;; The timer turns out to be the most difficult part:
(define (make-time-region secs)
  (make-region time-x time-y cw time-h
               (if (>= secs 6000)
                 "XX:XX"
                 (format
                  "~a:~a"
                  (substring (number->string (+ 100 (quotient secs 60))) 1)
                  (substring (number->string (+ 100 (modulo secs 60))) 1)))
               #f))
(define start-time #f)   ; in inexact milliseconds; #f means not started
(define shown-seconds 0) ; used to compute the delay until the next update
(define time-region (make-time-region 0)) ; old region, so we wan remove it
(send t add-region time-region) ; start with the initial region added
(define (show-time n)
  ;; Compute new time to show:
  (set! shown-seconds n)
  ;; Update the time by removing the old region and adding a new one:
  (send t begin-card-sequence)
  (send t remove-region time-region)
  (set! time-region (make-time-region shown-seconds))
  (send t add-region time-region)
  (send t end-card-sequence))
(define (get-update-delta)
  ;; Figure out how many milliseconds to sleep before the next update
  (max 0 (inexact->exact (floor (- (+ start-time (* 1000 shown-seconds) 1000)
                                   (current-inexact-milliseconds))))))
(define time-timer
  (make-object timer%
    (lambda ()
      (unless (= matches MAX-MATCHES)
        (show-time
         (inexact->exact
          (floor (/ (- (current-inexact-milliseconds) start-time) 1000))))
        (send time-timer start (get-update-delta) #t)))))
(define (reset-timer)
  (send time-timer stop)
  (set! start-time #f)
  (show-time 0))
(define (run-timer)
  (unless start-time
    (set! start-time (current-inexact-milliseconds))
    (send time-timer start 1000 #t)))

;; Start the game:
(send t pause 0.25)
(setup)))
