#|

possible to disable dragging but still allow double-clicking?
possible to remap single click (instead of double click)?

|#

#lang mzscheme

(require games/cards mred mzlib/class mzlib/list mzlib/unit string-constants
         "../show-scribbling.ss")

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

(define table (make-table "Aces" 6 5))

(make-object button% (string-constant help-menu-label) table
  (let ([show-help (show-scribbling '(lib "games/scribblings/games.scrbl")
                                    "aces")])
    (lambda x (show-help))))

(define draw-pile null)

(define card-height (send (car (make-deck)) card-height))
(define card-width (send (car (make-deck)) card-width))
(define region-height (send table table-height))

;; space between cards in the 4 stacks
(define card-space 30)

(define-struct stack (x y cards))

(define (get-x-offset n)
  (let* ([table-width (send table table-width)]
         [stack-spacing 7]
         [num-stacks 5]
         [all-stacks-width (+ (* num-stacks card-width)
                              (* (- num-stacks 1) stack-spacing))])
    (+ (- (/ table-width 2) (/ all-stacks-width 2))
       (* n (+ card-width stack-spacing)))))

(define draw-pile-region
  (make-button-region
   (get-x-offset 0)
   0
   card-width
   region-height ; card-height
   #f
   #f))

(define stacks
  (list (make-stack (get-x-offset 1) 0 null)
        (make-stack (get-x-offset 2) 0 null)
        (make-stack (get-x-offset 3) 0 null)
        (make-stack (get-x-offset 4) 0 null)))

;; type state = (make-state (listof cards) (listof[4] (listof cards)))
(define-struct state (draw-pile stacks))

;; extract-current-state : -> state
(define (extract-current-state)
  (make-state (copy-list draw-pile)
              (map (lambda (x) (copy-list (stack-cards x))) stacks)))

(define (copy-list l) (map (lambda (x) x) l))

;; install-state : -> void
(define (install-state state)
  (send table begin-card-sequence)

  ;; erase all old snips
  (send table remove-cards draw-pile)
  (for-each (lambda (stack)
              (send table remove-cards (stack-cards stack)))
            stacks)

  ;; restore old state
  (set! draw-pile (state-draw-pile state))
  (for-each (lambda (stack cards) (set-stack-cards! stack cards))
            stacks
            (state-stacks state))

  ;; restore GUI
  (for-each (lambda (draw-pile-card)
              (send table add-card draw-pile-card 0 0))
            draw-pile)
  (send table move-cards-to-region draw-pile draw-pile-region)
  (for-each (lambda (draw-pile-card)
              (send table card-face-down draw-pile-card)
              (send table card-to-front draw-pile-card))
            (reverse draw-pile))

  (for-each (lambda (stack)
              (let ([num-cards (length (stack-cards stack))])
                (send table add-cards (stack-cards stack) 0 0)
                (send table move-cards (stack-cards stack)
                      (stack-x stack)
                      (stack-y stack)
                      (lambda (i)
                        (values 0 (* (- num-cards i 1) card-space)))))
              (send table cards-face-up (stack-cards stack)))
            stacks)
  (send table end-card-sequence))

;; undo-stack : (listof state)
(define undo-stack null)

;; redo-stack : (listof state)
(define redo-stack null)

;; save-undo : -> void
;; saves the current state in the undo stack
(define (save-undo)
  (set! undo-stack (cons (extract-current-state) undo-stack))
  (set! redo-stack null))

;; do-undo : -> void
;; pre: (not (null? undo-stack))
(define (do-undo)
  (let ([to-install (car undo-stack)])
    (set! redo-stack (cons (extract-current-state) redo-stack))
    (set! undo-stack (cdr undo-stack))
    (install-state to-install)))

;; do-redo : -> void
;; pre: (not (null? redo-stack))
(define (do-redo)
  (let ([to-install (car redo-stack)])
    (set! undo-stack (cons (extract-current-state) undo-stack))
    (set! redo-stack (cdr redo-stack))
    (install-state to-install)))

(define (position-cards stack)
  (let ([m (length (stack-cards stack))])
    (lambda (i)
      (values 0 (if (= m 0) 0 (* (- m i 1) card-space))))))

(define (reset-game)
  (send table remove-cards draw-pile)
  (for-each (lambda (stack) (send table remove-cards (stack-cards stack)))
            stacks)

  (set! undo-stack null)
  (set! redo-stack null)

  (let* ([deck (shuffle-list (make-deck) 7)]
         [set-stack
          (lambda (which)
            (set-stack-cards! (which stacks) (list (which deck))))])
    (for-each (lambda (card)
                (send card user-can-move #f)
                (send card user-can-flip #f))
              deck)
    (set! draw-pile (cddddr deck))
    (set-stack car)
    (set-stack cadr)
    (set-stack caddr)
    (set-stack cadddr))

  (for-each (lambda (stack)
              (send table add-cards
                    (stack-cards stack)
                    (stack-x stack)
                    (stack-y stack)
                    (position-cards stack))
              (for-each (lambda (card) (send card flip))
                        (stack-cards stack)))
            stacks)

  (send table add-cards-to-region draw-pile draw-pile-region))

(define (move-from-deck)
  (save-undo)
  (unless (null? draw-pile)
    (let ([move-one
           (lambda (select)
             (let ([stack (select stacks)]
                   [card (select draw-pile)])
               (set-stack-cards! stack (cons card (stack-cards stack)))
               (send table card-to-front card)
               (send table flip-card card)))])

      (send table begin-card-sequence)
      (move-one car)
      (move-one cadr)
      (move-one caddr)
      (move-one cadddr)
      (send table end-card-sequence)

      (let ([cards-to-move (list (car draw-pile)
                                 (cadr draw-pile)
                                 (caddr draw-pile)
                                 (cadddr draw-pile))])
        (send table move-cards cards-to-move
              0 0
              (lambda (i)
                (let ([stack (list-ref stacks i)])
                  (let-values ([(dx dy) ((position-cards stack) 0)])
                    (values (+ dx (stack-x stack))
                            (+ dy (stack-y stack))))))))

      (set! draw-pile (cddddr draw-pile))

      (send table move-cards-to-region draw-pile draw-pile-region))))

(define (move-to-empty-spot card stack)
  (save-undo)
  (send table move-cards
        (list card)
        (stack-x stack)
        (stack-y stack)
        (position-cards stack))
  (remove-card-from-stacks card)
  (set-stack-cards! stack (cons card (stack-cards stack))))

(define (remove-card card)
  (save-undo)
  (send table remove-card card)
  (remove-card-from-stacks card))

(define (remove-card-from-stacks card)
  (let ([old-cards (map stack-cards stacks)])
    (for-each (lambda (stack)
                (set-stack-cards! stack (remq card (stack-cards stack))))
              stacks)
    (for-each (lambda (stack old-cards)
                (unless (equal? (stack-cards stack) old-cards)
                  (send table move-cards
                        (stack-cards stack)
                        (stack-x stack)
                        (stack-y stack)
                        (position-cards stack))))
              stacks
              old-cards)))

(send table set-single-click-action
      (lambda (card)
        (if (send card face-down?)
          (move-from-deck)
          (let ([bottom-four
                 (let loop ([l stacks])
                   (if (null? l)
                     null
                     (let ([stack (car l)])
                       (if (null? (stack-cards stack))
                         (loop (cdr l))
                         (cons (car (stack-cards stack)) (loop (cdr l)))))))])
            (when (memq card bottom-four)
              (if (ormap (lambda (bottom-card)
                           (and (eq? (send card get-suit)
                                     (send bottom-card get-suit))
                                (or (and (not (= 1 (send card get-value)))
                                         (= 1 (send bottom-card get-value)))
                                    (and (not (= 1 (send card get-value)))
                                         (< (send card get-value)
                                            (send bottom-card get-value))))))
                         bottom-four)
                (remove-card card)
                (let loop ([stacks stacks])
                  (if (null? stacks)
                    (void)
                    (let ([stack (car stacks)])
                      (if (null? (stack-cards stack))
                        (move-to-empty-spot card stack)
                        (loop (cdr stacks))))))))))
              (check-game-over)))

(define (game-over?)
  (and (null? draw-pile)
       (let ([suits/false
              (map (lambda (x)
                     (let ([stack-cards (stack-cards x)])
                       (if (null? stack-cards)
                         #f
                         (send (car stack-cards) get-suit))))
                   stacks)])

         (if (member #f suits/false)
           #f
           (and (memq 'clubs suits/false)
                (memq 'diamonds suits/false)
                (memq 'hearts suits/false)
                (memq 'spades suits/false))))))

(define (won?)
  (and (game-over?)
       (andmap (lambda (x)
                 (let ([cards (stack-cards x)])
                   (and (not (null? cards))
                        (null? (cdr cards))
                        (= 1 (send (car cards) get-value)))))
               stacks)))

(define (check-game-over)
  (when (game-over?)
    (case (message-box "Aces"
                       (if (won?)
                         "Congratulations! You win! Play again?"
                         "Game Over. Play again?")
                       table
                       '(yes-no))
      [(yes) (reset-game)]
      [(no) (send table show #f)])))

(send table add-region draw-pile-region)
(reset-game)

(define mb (or (send table get-menu-bar)
               (make-object menu-bar% table)))
(define edit-menu (new menu% [parent mb] [label (string-constant edit-menu)]))
(new menu-item%
     [label (string-constant undo-menu-item)]
     [parent edit-menu]
     [callback (lambda (x y) (do-undo))]
     [shortcut #\z]
     [demand-callback
      (lambda (item) (send item enable (not (null? undo-stack))))])
(new menu-item%
     [label (string-constant redo-menu-item)]
     [parent edit-menu]
     [callback (lambda (x y) (do-redo))]
     [shortcut #\y]
     [demand-callback
      (lambda (item) (send item enable (not (null? redo-stack))))])

(send table show #t)

))
