#lang debug racket
(require racket/generator sugar graph)
(provide (all-defined-out))

(struct $csp (variables domains neighbors constraints initial curr_domains nassigns nchecks current graph) #:transparent #:mutable)
;; `current` = current assignment
(define assignment? hash?)
(define variable? symbol?)
(define removal? (cons/c variable? any/c))

(define (update-assignment assignment var val)
  (define h (hash-copy assignment))
  (hash-set! h var val)
  h)

(struct $constraint (names proc) #:transparent)
(struct $vd (name vals) #:transparent)

(define (constraint-graph variables constraints)
  (for*/fold ([g (unweighted-graph/undirected variables)])
             ([constraint (in-list constraints)]
              [edge (in-combinations ($constraint-names constraint) 2)])
    (apply add-edge! g edge)
    g))

(define/contract (make-csp vds [constraints null])
  (((listof $vd?)) ((listof $constraint?)) . ->* . $csp?)
  (define variables (map $vd-name vds))
  (define domains (for/hasheq ([vd (in-list vds)])
                    (match vd
                      [($vd name vals) (values name vals)])))
  (define g (constraint-graph variables constraints))
  (define neighbors (for/hasheq ([v (in-list variables)])
                      (values v (get-neighbors g v))))
  ($csp variables domains neighbors constraints null #f 0 0 #f g))

(define/contract (domain csp var)
  ($csp? variable? . -> . (listof any/c))
  (hash-ref ($csp-domains csp) var))

(define/contract (curr_domain csp var)
  ($csp? variable? . -> . (listof any/c))
  (hash-ref ($csp-curr_domains csp) var))

(define/contract (neighbors csp var)
  ($csp? variable? . -> . (listof variable?))
  (hash-ref ($csp-neighbors csp) var))

(define/contract (assigns? assignment var)
  (assignment? variable? . -> . boolean?)
  (hash-has-key? assignment var))

(define/contract (assignment-ref assignment name-or-names)
  (assignment? (or/c (listof variable?) variable?) . -> . (or/c any/c (listof any/c)))
  (let loop ([name-or-names name-or-names])
    (match name-or-names
      [(? variable? name) (hash-ref assignment name)]
      [(list names ...)  (map loop names)])))

(define nassigns $csp-nassigns)
(define nchecks $csp-nchecks)

(define/contract (reset! csp)
  ($csp? . -> . void?)
  (set-$csp-curr_domains! csp #f)
  (reset-counters! csp))

(define/contract (check-constraints csp varval-hash [limits null] #:conflicts [count-conflicts? #f])
  (($csp? hash?) ((listof variable?) #:conflicts boolean?) . ->* . any/c)
  (define relevant-constraints
    (for/list ([constraint (in-list ($csp-constraints csp))]
               #:when (let ([cnames ($constraint-names constraint)])
                        (and
                         (for/and ([limit (in-list limits)])
                           (memq limit cnames))
                         (for/and ([cname (in-list cnames)])
                           (memq cname (hash-keys varval-hash))))))
      constraint))
  (begin
    ;; ordinary: behave like for/and, stop if #false result.
    ;; count-conflicts mode: behave like for/sum, don't stop till end.
    (define-values (result check-count)
      (for/fold ([result (if count-conflicts? 0 #true)]
                 [check-count 0])
                ([constraint (in-list relevant-constraints)]
                 #:break (false? result)) ; only breaks early in ordinary mode, when #f is result value
        (define vals (assignment-ref varval-hash ($constraint-names constraint)))
        (define res (apply ($constraint-proc constraint) vals))
        (values (if count-conflicts? (+ (if res 0 1) result) res) (add1 check-count))))
    (set-$csp-nchecks! csp (+ check-count ($csp-nchecks csp)))
    result))

(define/contract (reset-counters! csp)
  ($csp? . -> . void?)
  (set-$csp-nassigns! csp 0)
  (set-$csp-nchecks! csp 0))

(define/contract (assign csp var val assignment)
  ($csp? variable? any/c assignment? . -> . void?)
  ;; Add {var: val} to assignment; Discard the old value if any.
  (hash-set! assignment var val)
  (set-$csp-nassigns! csp (add1 ($csp-nassigns csp))))

(define/contract (unassign csp var assignment)
  ($csp? variable? assignment? . -> . void?)
  ;; Remove {var: val} from assignment.
  ;; DO NOT call this if you are changing a variable to a new value;
  ;; just call assign for that.
  (hash-remove! assignment var))

(define/contract (all-variables-assigned? csp assignment)
  ($csp? assignment? . -> . boolean?)
  (= (length (hash-keys assignment)) (length ($csp-variables csp))))

(define asses (make-parameter #f))
(define ncon (make-parameter #f))

(define/contract (nconflicts csp var val assignment)
  ($csp? variable? any/c assignment? . -> . number?)
  ;; Return the number of conflicts var=val has with other variables."""
  ;; Subclasses may implement this more efficiently
  (define ass (update-assignment assignment var val))
  (check-constraints csp ass (list var) #:conflicts #t))


(define (display csp assignment)
  (displayln csp))

;; These methods are for the tree and graph-search interface:

(struct $action (var val) #:transparent #:mutable)
(define/contract (state->assignment state)
  ((listof $action?) . -> . assignment?)
  (for/hasheq ([action (in-list state)])
    (match action
      [($action var val) (values var val)])))

;; todo: test that this works
(define/contract (actions csp state)
  ($csp? (listof $action?) . -> . any/c)
  ;; Return a list of applicable actions: nonconflicting
  ;; assignments to an unassigned variable.
  (cond
    [(all-variables-assigned? csp state) empty]
    [else
     (define assignment (state->assignment state))
     (define var (for/first ([var (in-list ($csp-variables csp))]
                             #:unless (assignment . assigns? . var))
                   var))
     (for/list ([val (in-list (domain csp var))]
                #:when (zero? (nconflicts csp var val assignment)))
       ($action var val))]))

;; todo: test that this works
(define/contract (result csp state action)
  ($csp? (listof $action?) $action? . -> . assignment?)
  ;; Perform an action and return the new state.
  (match-define ($action var val) action)
  (append state (list action)))

;; todo: test that this works
(define/contract (goal_test csp state)
  ($csp? (or/c assignment? (listof $action?)) . -> . boolean?)
  ;; The goal is to assign all variables, with all constraints satisfied.
  (define assignment (if (assignment? state) state (state->assignment state)))
  (and (all-variables-assigned? csp assignment)
       (for/and ([variable ($csp-variables csp)])
         (zero? (nconflicts csp variable (hash-ref assignment variable) assignment)))))

;; These are for constraint propagation

(define/contract (support_pruning csp)
  ($csp? . -> . void?)
  ;; Make sure we can prune values from domains. (We want to pay
  ;; for this only if we use it.)
  (unless ($csp-curr_domains csp)
    (define h (make-hasheq))
    (for ([v ($csp-variables csp)])
      (hash-set! h v (hash-ref ($csp-domains csp) v)))
    (set-$csp-curr_domains! csp h)))
      
(define/contract (suppose csp var value)
  ($csp? variable? any/c . -> . (box/c (listof removal?)))
  ;; Start accumulating inferences from assuming var=value
  (support_pruning csp)
  (begin0
    (box (for/list ([val (in-list (curr_domain csp var))]
                    #:when (not (equal? val value)))
           (cons var val)))
    (hash-set! ($csp-curr_domains csp) var (list value))))

(define/contract (prune csp var value removals)
  ($csp? variable? any/c (or/c #f (box/c (listof removal?))) . -> . (box/c (listof removal?)))
  ;; Rule out var=value
  (hash-update! ($csp-curr_domains csp) var (λ (vals) (remove value vals)))
  (when removals
    (set-box! removals (append (unbox removals) (list (cons var value)))))
  removals)

(define/contract (choices csp var)
  ($csp? variable? . -> . (listof any/c))
  ;; Return all values for var that aren't currently ruled out.
  (hash-ref (or ($csp-curr_domains csp) ($csp-domains csp)) var))

(define/contract (infer_assignment csp)
  ($csp? . -> . assignment?)
  ;; Return the partial assignment implied by the current inferences.
  (support_pruning csp)
  (define assignment (make-hasheq))
  (for ([v (in-list ($csp-variables csp))])
    (match (curr_domain csp v)
      [(list one-value) (hash-set! assignment v one-value)]
      [else #f]))
  assignment)

(define/contract (restore csp removals)
  ($csp? (box/c (listof removal?)) . -> . void?)
  ;; Undo a supposition and all inferences from it.
  (for ([removal (in-list (unbox removals))])
    (match removal
      [(cons B b) (hash-update! ($csp-curr_domains csp) B
                                (λ (vals) (append vals (list b))))])))

;; This is for min_conflicts search
(define/contract (conflicted_vars csp current)
  ($csp? hash? . -> . (listof variable?))
  ;; Return a list of variables in current assignment that are in conflict
  (for/list ([var (in-list ($csp-variables csp))]
             #:when (positive? (nconflicts csp var (hash-ref current var) current)))
    var))

;; ______________________________________________________________________________
;; Constraint Propagation with AC-3

(struct $arc (start end) #:transparent)
(define/contract (AC3 csp [queue #f][removals #f])
  (($csp?) ((or/c #f (listof any/c)) (box/c (listof removal?))) . ->* . boolean?)
  (support_pruning csp)
  (with-handlers ([boolean? values])
    (for/fold ([queue (or queue
                          (for*/list ([Xi (in-list ($csp-variables csp))]
                                      [Xk (in-list (neighbors csp Xi))])
                            ($arc Xi Xk)))]
               #:result #true)
              ([i (in-naturals)]
               #:break (empty? queue))
      (match-define (cons ($arc Xi Xj) other-arcs) queue)
      (cond
        [(revise csp Xi Xj removals)
         (when (empty? (curr_domain csp Xi))
           (raise #false))
         (append other-arcs
                 (for/list ([Xk (in-list (neighbors csp Xi))]
                            #:unless (eq? Xk Xj))
                   ($arc Xk Xi)))]
        [else other-arcs]))))

(define/contract (revise csp Xi Xj removals)
  ($csp? variable? variable? (box/c (listof removal?)) . -> . boolean?)
  ;; Return true if we remove a value.
  (for/fold ([revised #false])
            ([x (in-list (curr_domain csp Xi))])
    ;; If Xi=x is consistent with Xj=y for any y, keep Xi=x, otherwise prune
    (cond
      [(not
        (for/or ([y (in-list (curr_domain csp Xj))])
          (check-constraints csp (hasheq Xi x Xj y) (list Xi))))
       (prune csp Xi x removals)
       #true]
      [else revised])))
;; ______________________________________________________________________________
;; CSP Backtracking Search

;; Variable ordering

(define/contract (first_unassigned_variable assignment csp)
  (assignment? $csp? . -> . (or/c #false variable?))
  ;; The default variable order.
  (for/first ([var (in-list ($csp-variables csp))]
              #:unless (assignment . assigns? . var))
    var))

(define current-shuffle (make-parameter #t))

(define/contract (argmin_random_tie proc xs)
  (procedure? (listof any/c) . -> . any/c)
  (define ordered-xs (sort xs < #:key proc))
  (first ((if (current-shuffle) shuffle values)
          (takef ordered-xs (λ (x) (= (proc (car ordered-xs)) (proc x)))))))

(define/contract (mrv assignment csp)
  (assignment? $csp? . -> . any/c)
  ;; Minimum-remaining-values heuristic.
  ;; with random tiebreaker.
  (define (num_legal_values var)
    (if ($csp-curr_domains csp)
        (length (curr_domain csp var))
        (for/sum ([val (in-list (domain csp var))]
                  #:when (zero? (nconflicts csp var val assignment)))
          1)))
  (struct $mrv-rec (var num) #:transparent)
  (argmin_random_tie
   (λ (var) (num_legal_values var))
   (for/list ([var (in-list ($csp-variables csp))]
              #:unless (assignment . assigns? . var))
     var)))

;; Value ordering

(define/contract (unordered_domain_values var assignment csp)
  (variable? assignment? $csp? . -> . (listof any/c))
  ;; The default value order.
  (choices csp var))

(define/contract (lcv var assignment csp)
  (variable? assignment? $csp? . -> . (listof any/c))
  ;; Least-constraining-values heuristic.
  (sort (choices csp var) < #:key (λ (val) (nconflicts csp var val assignment))))

;; Inference

(define/contract (no_inference csp var value assignment removals)
  ($csp? variable? any/c assignment? (box/c (listof removal?)) . -> . boolean?)
  #true)

(define/contract (forward_checking csp var value assignment removals)
  ($csp? variable? any/c assignment? (box/c (listof removal?)) . -> . boolean?)
  ;; Prune neighbor values inconsistent with var=value.
  (support_pruning csp) ;; necessary to set up curr_domains
  (define ass (update-assignment assignment var value))
  (for/and ([B (in-list (neighbors csp var))]
            #:unless (assignment . assigns? . B))
    (for ([b (in-list (curr_domain csp B))]
          #:unless (check-constraints csp (update-assignment ass B b) (list var B)))
      (prune csp B b removals))
    (not (empty? (curr_domain csp B)))))

(define/contract (mac csp var value assignment removals)
  ($csp? variable? any/c assignment? (box/c (listof removal?)) . -> . boolean?)
  ;; Maintain arc consistency.
  (AC3 csp (for/list ([neighbor (in-list (neighbors csp var))])
             ($arc neighbor var)) removals))
  
(define current-select-variable (make-parameter #f))
(define current-order-values (make-parameter #f))
(define current-inference (make-parameter #f))

(define/contract (backtracking_search
                  csp
                  [select_unassigned_variable (or (current-select-variable) first_unassigned_variable)]
                  [order_domain_values (or (current-order-values) unordered_domain_values)]
                  [inference (or (current-inference) no_inference)])
  (($csp?) (procedure? procedure? procedure?) . ->* . generator?)
  (generator ()
             (let backtrack ([assignment (make-hasheq)])
               (cond
                 [(all-variables-assigned? csp assignment)
                  (unless (goal_test csp assignment) (error 'whut))
                  (yield (hash-copy assignment))]
                 [else
                  (define var (select_unassigned_variable assignment csp))
                  (for ([val (in-list (order_domain_values var assignment csp))]
                        #:when (zero? (nconflicts csp var val assignment)))
                    (assign csp var val assignment)
                    (define removals (suppose csp var val))
                    (when (inference csp var val assignment removals)
                      (backtrack assignment))
                    (restore csp removals))
                  (unassign csp var assignment)]))))
;; ______________________________________________________________________________
;; Min-conflicts hillclimbing search for CSPs

(require sugar/debug)
(define (min-conflicts csp [max_steps (expt 10 5)])
  (($csp?) (integer?) . ->* . generator?)
  ;; Solve a CSP by stochastic hillclimbing on the number of conflicts.
  ;; Generate a complete assignment for all variables (probably with conflicts)
  (generator ()
             (define current (make-hasheq))
             (set-$csp-current! csp current)
             (for ([var (in-list ($csp-variables csp))])
               (define val (min_conflicts_value csp var current))
               (assign csp var val current))
             ;; Now repeatedly choose a random conflicted variable and change it
             (for ([i (in-range max_steps)])
               (define conflicted (conflicted_vars csp current))
               (when (empty? conflicted)
                 (report i)
                 (yield current))
               (define var (first ((if (current-shuffle) shuffle values) conflicted)))
               (define val (min_conflicts_value csp var current))
               (assign csp var val current))))
  
(define/contract (min_conflicts_value csp var current)
  ($csp? variable? hash? . -> . any/c)
  ;; Return the value that will give var the least number of conflicts.
  ;; If there is a tie, choose at random.
  (argmin_random_tie (λ (val) (nconflicts csp var val current)) (domain csp var)))

(define current-reset (make-parameter #t))
(define current-solver (make-parameter #f))

(define/contract (solve* csp [solution-limit +inf.0])
  (($csp?) (integer?) . ->* . (or/c #f (non-empty-listof any/c)))
  (define solver (or (current-solver) backtracking_search))
  (begin0
    (match (for/list ([solution (in-producer (solver csp) (void))]
                      [idx (in-range solution-limit)])
             solution)
      [(list solutions ...) solutions]
      [else #false])
    (when (current-reset)
      (set-$csp-curr_domains! csp #f))))

(define/contract (solve csp)
  ($csp? . -> . any/c)
  (match (solve* csp 1)
    [(list solution) solution]
    [else #false]))

(require rackunit)

(define vs '(wa nsw t q nt v sa))
(define vds (for/list ([k vs])
              ($vd k '(red green blue))))
(define (neq? a b) (not (eq? a b)))
(define cs (list
            ($constraint '(wa nt) neq?)
            ($constraint '(wa sa) neq?)
            ($constraint '(nt sa) neq?)
            ($constraint '(nt q) neq?)
            ($constraint '(q sa) neq?)
            ($constraint '(q nsw) neq?)
            ($constraint '(nsw sa) neq?)
            ($constraint '(nsw v) neq?)
            ($constraint '(v sa) neq?)))
(define csp (make-csp vds cs))


(define (tests)
  (set-$csp-curr_domains! csp #f)
  (check-true ($csp? csp))
  (define a (make-hasheq))
  (assign csp 'key 42 a)
  (check-equal? (hash-ref a 'key) 42)
  (unassign csp 'key a)
  (check-exn exn:fail? (λ () (hash-ref a 'key)))
  (check-equal? 0 (nconflicts csp 'wa 'red (hasheq 'wa 42)))
  (support_pruning csp)
  (check-true (hash? ($csp-curr_domains csp)))

  (check-equal? (suppose csp 'wa 'red) '#&((wa . green) (wa . blue)))
  (check-equal? (curr_domain csp 'wa) '(red))

  (check-equal? (prune csp 'v 'red (box empty)) '#&((v . red)))

  (check-equal? (choices csp 'v) '(green blue))
  (check-equal? (choices csp 'wa) '(red))
  (check-equal? (infer_assignment csp)
                (make-hasheq '((wa . red))))
  (check-equal? (suppose csp 'v 'blue) '#&((v . green)))
  (check-equal? (infer_assignment csp)
                (make-hasheq '((v . blue) (wa . red))))
  (restore csp '#&((wa . green)))
  (check-equal? (infer_assignment csp)
                (make-hasheq '((v . blue))))
  (restore csp '#&((v . blue)))
  (check-equal? (infer_assignment csp) (make-hasheq))

  (check-equal? (first_unassigned_variable (hash) csp) 'wa)
  (check-equal? (unordered_domain_values 'wa (hash) csp) '(red green))

  (set-$csp-curr_domains! csp #f) ; reset current domains
  (check-equal? (solve csp)
                (make-hasheq '((nsw . green) (nt . green) (q . red) (sa . blue) (t . blue) (v . red) (wa . red))))
  (check-equal? (begin0 (list ($csp-nassigns csp) ($csp-nchecks csp)) (reset-counters! csp)) '(40 321))
  (check-equal? (length (solve* csp)) 18)

  (check-equal? (suppose csp 'nsw 'red) '#&((nsw . green) (nsw . blue)))
  (check-equal? (solve csp)
                (make-hasheq '((nsw . red) (nt . red) (q . green) (sa . blue) (t . blue) (v . green) (wa . green))))
  (check-equal? ($csp-nassigns csp) 368)

  (reset-counters! csp)
  (check-equal? (suppose csp 'nsw 'red) '#&((nsw . green) (nsw . blue)))
  (check-equal? (length (solve* csp)) 6)
  (check-equal? (begin0 (list ($csp-nassigns csp) ($csp-nchecks csp)) (reset-counters! csp)) '(111 1035))

  (parameterize ([current-select-variable mrv]
                 [current-shuffle #f])
    (check-equal?
     (solve csp)
     (make-hasheq '((nsw . green) (nt . green) (q . red) (sa . blue) (t . blue) (v . red) (wa . red))))
    (check-equal? (begin0 (list ($csp-nassigns csp) ($csp-nchecks csp)) (reset-counters! csp)) '(39 321)))

  (parameterize ([current-order-values lcv])
    (check-equal?
     (solve csp)
     (make-hasheq '((nsw . green) (nt . green) (q . red) (sa . blue) (t . blue) (v . red) (wa . red))))
    (check-equal? (begin0 (list ($csp-nassigns csp) ($csp-nchecks csp)) (reset-counters! csp)) '(39 1040)))

  (parameterize ([current-inference forward_checking])
    (forward_checking csp 'sa 'blue (make-hasheq) (box null))
    (check-equal? ($csp-curr_domains csp)
                  (make-hasheq '((nsw . (red green)) (nt . (red green)) (q . (red green)) (sa . (red green blue)) (t . (red green blue)) (v . (red green)) (wa . (red green))))))

  (reset-counters! csp)
  (set-$csp-curr_domains! csp #f)
  (parameterize ([current-inference forward_checking])
    (check-equal?
     (solve csp)
     (make-hasheq '((nsw . green) (nt . green) (q . red) (sa . blue) (t . blue) (v . red) (wa . red))))
    (check-equal? (begin0 (list ($csp-nassigns csp) ($csp-nchecks csp)) (reset-counters! csp)) '(25 106)))

  (set-$csp-curr_domains! csp #f)
  (parameterize ([current-inference mac]
                 [current-reset #f])
    (check-equal? (solve csp)
                  (make-hasheq '((nsw . green) (nt . green) (q . red) (sa . blue) (t . blue) (v . red) (wa . red))))
    (check-equal? (begin0 (list ($csp-nassigns csp) ($csp-nchecks csp)) (reset-counters! csp)) '(17 175)))

  (parameterize ([current-select-variable mrv]
                 [current-order-values lcv]
                 [current-inference mac]
                 [current-reset #f])
    (check-equal? (solve csp)
                  (make-hasheq '((nsw . green) (nt . green) (q . red) (sa . blue) (t . blue) (v . red) (wa . red))))
    (check-equal? (begin0 (list ($csp-nassigns csp) ($csp-nchecks csp)) (reset-counters! csp)) '(7 45)))

  (set-$csp-curr_domains! csp #f)
  (parameterize ([current-shuffle #f]
                 [current-solver min-conflicts])
    (check-equal?
     (solve csp)
     (make-hasheq '((nsw . red) (nt . red) (q . green) (sa . blue) (t . red) (v . green) (wa . green))))
    (check-equal? (begin0 (list ($csp-nassigns csp) ($csp-nchecks csp)) (reset-counters! csp)) '(9 220)))

  (define tri (make-csp (list ($vd 'a '(1 2 3))
                              ($vd 'b '(4 5 6))
                              ($vd 'c '(7 8 9)))
                        (list ($constraint '(a b c) (λ (a b c) (= (+ a b c) 18))))))

  (parameterize ([current-select-variable mrv]
                 [current-order-values lcv]
                 [current-inference mac]
                 [current-reset #f]
                 [current-shuffle #f])
    (check-equal? (solve tri) (make-hasheq '((a . 3) (b . 6) (c . 9)))))
  (check-equal? (begin0 (list ($csp-nassigns tri) ($csp-nchecks tri)) (reset-counters! tri)) '(13 68))
  )

(module+ test
  (tests))



#|
(define (abc-test a b c) (/ (+ (* 100 a) (* 10 b) c) (+ a b c))) 
(define abc (make-csp (list ($vd 'a (shuffle (range 1 10)))
                            ($vd 'b (range 1 10))
                            ($vd 'c (range 1 10)))))

(argmin (λ (h)
          (abc-test (hash-ref h 'a) (hash-ref h 'b) (hash-ref h 'c)))
        (parameterize ([current-select-variable mrv]
                       [current-order-values lcv]
                       [current-inference mac]
                       [current-reset #f]
                       [current-shuffle #f])
          (solve* abc)))
|#