(use goo)

;; ----- Define Classes -----

(dc <bank> (<any>))
 (dp customers (<bank> => <col>) (vec)) ;; A vector of customers, although a map might be more useful.

(dc <customer> (<any>))
 (dp accounts (<customer> => <col>) (vec))
 (dp first-name (<customer> => <str>))
 (dp last-name (<customer> => <str>))

(dc <account> (<any>))
 (dp balance (<account> => <int>))

(dc <checking-account> (<account>))
(dc <savings-account> (<account>))

(dv *minimum-balance* 100) ;; 100 Smackers, or you're in trouble.
(dv *minimum-balance-for-no-fee* 1000)
(dv *monthly-fee* 10)
(dv *monthly-interest* 0.02)

;; ----- Methods -----

;; --- Account Methods

(dm credit (account|<account> amount|<int>)
  ;; Fancy syntax
  (opf (balance account) (+ _ amount))
  ;; Can also be written as: 
  ;;   (set (balance account) (+ (balance account) amount))
  )

(dm debit (account|<account> amount|<int> => <log>)
  (opf (balance account) (- _ amount))
  )

(dm charge-monthly-fee (account|<account>)
  (when (< (balance account) *minimum-balance-for-no-fee*)
    (debit account *monthly-fee*)
    )
  )

;; By default, all accounts earn interest
(dm earn-interest (account|<account>)
  (credit account (floor (* (account-balance account) *monthly-interest*)))
  )

;; But checking accounts don't earn interest. Muahahaha!
(dm earn-interest (account|<checking-account>)
  ;; Do nothing
  )

(dm to-str (account|<account> => <str>)
  (cat (if (isa? account <checking-account>)
           "Checking "
           "Savings ")
       "Account: "
       (to-str (balance account))
       )
  )

;; --- Customer Methods

(dm new-customer (first-name|<str> last-name|<str> => <customer>)
  (def customer (new <customer>))
  ;; We don't have to worry about binding masking here because the setter names
  ;;  are transformed into first-name-setter, last-name-setter, etc.
  (set (first-name customer) first-name)
  (set (last-name customer) last-name)
  ;; Note that there is an alternative syntax where we specify the property
  ;;  names and values in the 'new' clause, but the specific syntax for that
  ;;  may change sometime soon (relative to the writing of this file), so 
  ;;  I'm avoiding it.

  ;; Don't forget to return the customer!
  customer
  )

;; Find a customer using a user-specified propery-getter, ex: first-name or
;;  last-name.  
;; The t? syntax indicates that the return type is either <customer> or #f.
(dm find-customer (customer-list|<col> prop-getter match-val|<any> => (t? <customer>))
  (esc found-it
    (do (fun (current-customer)
          (when (= (prop-getter current-customer) match-val)
            (found-it current-customer)
            )
          )
        customer-list)
    )
  )

(dm to-str (customer|<customer> => <str>)
  (def cat-pair-with-newline (fun (str1 str2)
                               (cat str1 "\n" str2)))
  (def prepend-spaces (fun (str)
                        (cat "  " str)))
  (cat (last-name customer)
       ", "
       (first-name customer)
       "\n"
       ;; Some list-fun worthy of scheme:
       ;;  - First, map over the list of accounts applying to-str, resulting in a list of strings
       ;;  - Next, map over the strings with prepend-spaces, returning a new list of strings
       ;;  - Finally, use fold+ to concatenate all the strings together (with newlines)
       ;;   (fold+ operates like accumulate in SICP... (func z (func y (func w x))) for (w x y z))
       (fold+ cat-pair-with-newline (map prepend-spaces (map to-str (accounts customer))))
       )
  )

;; --- Bank Methods