#lang racket
(require csc151)

;;; File:
;;;   textgen.rkt
;;; Authors:
;;;   Titus Klinge
;;;   Samuel A. Rebelsky
;;;   YOUR NAME HERE
;;; Contents:
;;;   Code for doing randomized text generation.
;;; Citations:
;;;

;;; Procedure:
;;;   valid?
;;; Parameters:
;;;   char, a character
;;; Purpose:
;;;   Determine if char is one of the valid characters used in our
;;;   text generation algorithm (space, #\a through #\z, #\A through #\Z).
;;; Produces:
;;;   ok, a boolean
(define valid?
  (lambda (char)
    (or (char=? char #\space)
        (char-ci<=? #\a char #\z))))

;;; Procedure:
;;;   valid->num
;;; Parameters:
;;;   char, a character
;;; Purpose:
;;;   Convert char to a representative number
;;; Produces:
;;;   number, an integer
;;; Preconditions:
;;;   (valid? char)
;;; Postconditions:
;;;   * #\a is 1
;;;   * ...
;;;   * #\z is 26
;;;   * space is 27
(define valid->num
  (let [(anum (char->integer #\a))]
    (lambda (char)
      (cond
        [(char=? char #\space)
         27]
        [else
         (+ 1 (- (char->integer (char-downcase char)) anum))]))))

;;; Procedure:
;;;   num->valid
;;; Parameters:
;;;   num, an integer
;;; Purpose:
;;;   Convert a number in back to a character.
;;; Produces:
;;;   char, a character
;;; Preconditions:
;;;   num is a value returned by valid->num.
;;; Postconditions:
;;;   (num->valid (valid->num char)) = char
(define num->valid
  (let [(anum (char->integer #\a))]
    (lambda (num)
      (cond
        [(= num 27)
         #\space]
        [else
         (integer->char (+ anum -1 num))]))))

;;; Procedure:
;;;   char-stats
;;; Parameters:
;;;   [None]
;;; Purpose:
;;;   Generate a new "thing" we can use to store statistics on
;;;   character frequency.
;;; Produces:
;;;   stats, something we can use to store statistics on character
;;;     frequency.  (That is, something we can use with `record-char!`
;;;     and `random-char`.)
;;; Philosophy:
;;;   The client need not know how we store the statistics.  But
;;;   we will likely implement the statistics as a vector of length
;;;   (+ 1 "number of chars").  Element 0 of that vectors gives the
;;;   total count of chars stored.  Element i gives the number of
;;;   times we've seen char i+1.
(define charstats
  (lambda ()
    (make-vector 28 0)))

;;; Procedure:
;;;   record-char!
;;; Parameters:
;;;   stats, a value returned by `charstats`
;;;   char, a character
;;; Purpose:
;;;   Record information that we've seen another copy of char.
;;; Produces:
;;;   [Nothing; called for the side effect.]
(define record-char!
  (lambda (stats char)
    (vector-increment! stats 0)
    (vector-increment! stats (valid->num char))))

;;; Procedure:
;;;   random-char
;;; Parameters:
;;;   stats, a value returned by `charstats`
;;; Purpose:
;;;   Get an unpredictable character from `stats` based on the
;;;   distribution of characters represented by `stats`.
;;; Produces:
;;;   char, a character
;;; Preconditions:
;;;   * At least one char has been added to stats using `record-char!`
;;; Postconditions:
;;;   * The distribution of characters returned by this procedure is similar
;;;     to the distribution of characters in stats.
;;; Problems:
;;;   If no characters have been added, returns the space characer.
(define random-char
  (lambda (stats)
    (let ([count (vector-ref stats 0)])
      (if (zero? count)
          #\space
          (let kernel ([ran (random count)]
                       [pos 1])
            (let ([freq (vector-ref stats pos)])
              (if (< ran freq)
                  (num->valid pos)
                  (kernel (- ran freq)
                          (+ pos 1)))))))))

;;; Procedure:
;;;   random-string
;;; Parameters:
;;;   stats, a value return by `charstats`
;;;   len, a positive integer
;;; Purpose:
;;;   Generate a "random" string whose statistical properties match
;;;   those of stats.
;;; Produces:
;;;   str, a string
;;; Preconditions:
;;;   [See the preconditions of random-char]
;;; Postconditions:
;;;   * (string-length str) = len
;;;   * The contents of the string are hard to predict.
;;;   * For large enough len, the distribution of characters in str match
;;;     those in stats.
(define random-string
  (lambda (stats len)
    (let kernel ([remaining len]
                 [chars null])
      (if (zero? remaining)
          (list->string (reverse chars))
          (kernel (decrement remaining)
                  (cons (random-char stats) chars))))))

;;; Procedure:
;;;   vector-increment
;;; Parameters:
;;;   vec, a vector
;;;   pos, an integer
;;; Purpose:
;;;   Increment the value at position pos.
;;; Produces:
;;;   [Nothing; called for the side effect]
;;; Preconditions:
;;;   * 0 <= pos < (vector-length vec)
;;;   * (vector-ref vec pos) contains a number, num
;;; Postconditions:
;;;   * (vector-ref vec pos) contains (+ 1 num)
(define vector-increment!
  (lambda (vec pos)
    (vector-set! vec pos
                 (+ 1 (vector-ref vec pos)))))

;;; Procedure:
;;;   file->stats
;;; Parameters:
;;;   fname, a string
;;; Purpose:
;;;   Convert a file to a set of statistics as given by
;;;   `charstats` and `record-char!`.
;;; Produces:
;;;   stats, a vector of statistics.
;;; Preconditions:
;;;   fname names a valid text file.
;;; Postconditions:
;;;   * `stats` is a set of statistics that can be used by
;;;     `random-char`.
;;;   * The frequencies in `stats` correspond to the frequencies
;;;     in the file named by `fname`.
(define file->stats
  (lambda (fname)
    (let ([stats (charstats)]
          [port (open-input-file fname)])
      (let kernel ()
        (let ([char (read-char port)])
          (cond
            [(eof-object? char)
             (close-input-port port)
             stats]
            [(valid? char)
             (record-char! stats char)
             (kernel)]
            [else
             (kernel)]))))))

(define sample (charstats))
(record-char! sample #\a)
(record-char! sample #\a)
(record-char! sample #\m)
(record-char! sample #\s)
(record-char! sample #\space)
(record-char! sample #\s)
(record-char! sample #\s)

;;; Procedure:
;;;   follow-one-stats
;;; Parameters:
;;;   [None]
;;; Purpose:
;;;   Create a "thing" that allows us to keep track of statistics
;;;   for what characters follow each character.
;;; Produces:
;;;   stats, a thing
(define follow-one-stats
  (lambda ()
    (vector-map! (lambda (val) (charstats))
                 (make-vector 28))))

;;; Procedure:
;;;   record-follow-one!
;;; Parameters:
;;;   stats, a value returned by follow-one-stats
;;;   prev, a valid character
;;;   char, a valid character
;;; Purpose:
;;;   Record the information that char follows prev
;;; Produces:
;;;   [Nothing; called for the side effects]
(define record-follow-one!
  (lambda (stats prev char)
    (record-char! (vector-ref stats (valid->num prev))
                  char)))

;;; Procedure:
;;;   file->follow-one-stats
;;; Parameters:
;;;   fname, a string
;;; Purpose:
;;;   Convert a file to a set of statistics as given by `follow-one-stats`
;;;   and `record-follow-one`.
;;; Produces:
;;;   stats, the thing returned by follow-one-stats
;;; Preconditions:
;;;   fname names a valid text file.
;;; Postconditions:
;;;   * `stats` is a set of statistics that can be used wherever we use
;;;     the values from `follow-one-stats`.
;;;   * The frequencies in `stats` correspond to the frequencies
;;;     in the file named by `fname`.
(define file->follow-one-stats
  (lambda (fname)
    (let ([stats (follow-one-stats)]
          [port (open-input-file fname)])
      (let kernel ([prev (read-char port)])
        (let ([char (read-char port)])
          (cond
            ; When we reach the end, clean up and stop
            [(eof-object? char)
             (close-input-port port)
             stats]
            ; If the previous character is not valid, skip over it
            [(not (valid? prev))
             (kernel char)]
            ; If the current character is not valid, don't record
            ; anything and read over the character.
            [(not (valid? char))
             (kernel (read-char port))]
            ; Otherwise, we record that char follows prev and continue,
            ; updating char to the previous character.
            [else
             (record-follow-one! stats prev char)
             (kernel char)]))))))

;;; Procedure:
;;;   random-string-one
;;; Parameters:
;;;   stats, a value return by `follow-one`
;;;   prev, a valid character
;;;   len, a positive integer
;;; Purpose:
;;;   Generate a "random" string whose statistical properties match
;;;   those of stats.
;;; Produces:
;;;   str, a string
(define random-string-one
  (lambda (stats prev len)
    ; Randomly select among the successors of prev
    (let ([random-next (lambda (prev)
                         (let ([substats (vector-ref stats (valid->num prev))])
                           (random-char substats)))])
      ; Build a list of successors then convert to a string
      (let kernel ([remaining len]
                   [prev prev]
                   [chars null])
        (if (zero? remaining)
            (list->string (reverse chars))
            (let ([char (random-next prev)])
              (kernel (decrement remaining)
                      char
                      (cons char chars))))))))

;;; Procedure:
;;;   follow-two-stats
;;; Parameters:
;;;   [None]
;;; Purpose:
;;;   Create a "thing" that allows us to keep track of statistics
;;;   for what characters follow each pair of characters.
;;; Produces:
;;;   stats, a thing
(define follow-two-stats
  (lambda ()
    (vector-map! (lambda (val) (charstats))
                 (make-vector (+ 1 (* 27 28))))))

;;; Procedure:
;;;   index-two
;;; Parameters:
;;;   prevprev, a valid character
;;;   prev, a valid character
;;; Purpose:
;;;   Find the position in a follow-two-stats vector that we should
;;;   use for the pair (prevprev,prev).
;;; Produces:
;;;   index, an integer
(define index-two
  (lambda (prevprev prev)
    (+ (* 27 (valid->num prevprev))
       (valid->num prev))))

;;; Procedure:
;;;   record-follow-two!
;;; Parameters:
;;;   stats, a value returned by follow-two-stats
;;;   prevprev, a valid character
;;;   prev, a valid character
;;;   char, a valid character
;;; Purpose:
;;;   Record the information that char follows prevprev and prev
;;; Produces:
;;;   [Nothing; called for the side effects]
(define record-follow-two!
  (lambda (stats prevprev prev char)
    (record-char! (vector-ref stats (index-two prevprev prev))
                  char)))

;;; Procedure:
;;;   file->follow-two-stats
;;; Parameters:
;;;   fname, a string
;;; Purpose:
;;;   Convert a file to a set of statistics as given by `follow-two-stats`
;;;   and `record-follow-two`.
;;; Produces:
;;;   stats, the thing returned by follow-two-stats
;;; Preconditions:
;;;   fname names a valid text file.
;;; Postconditions:
;;;   * `stats` is a set of statistics that can be used wherever we use
;;;     the values from `follow-two-stats`.
;;;   * The frequencies in `stats` correspond to the frequencies
;;;     in the file named by `fname`.
(define file->follow-two-stats
  (lambda (fname)
    (let ([stats (follow-two-stats)]
          [port (open-input-file fname)])
      (let kernel ([prevprev (read-char port)]
                   [prev (read-char port)])
        (let ([char (read-char port)])
          (cond
            ; When we reach the end, clean up and stop
            [(eof-object? char)
             (close-input-port port)
             stats]
            ; If any of the characters is not valid, skip
            [(or (not (valid? prevprev))
                 (not (valid? prev))
                 (not (valid? char)))
             (kernel prev char)]
            ; Otherwise, we record that char follows prevprev and prev.
            ; We then continue, updating appropriately
            [else
             (record-follow-two! stats prevprev prev char)
             (kernel prev char)]))))))

;;; Procedure:
;;;   random-string-two
;;; Parameters:
;;;   stats, a value return by `follow-two`
;;;   prevprev, a valid character
;;;   prev, a valid character
;;;   len, a positive integer
;;; Purpose:
;;;   Generate a "random" string whose statistical properties match
;;;   those of stats.
;;; Produces:
;;;   str, a string
(define random-string-two
  (lambda (stats prevprev prev len)
    ; Randomly select among the successors of prevprev and prev
    (let ([random-next (lambda (prevprev prev)
                         (let ([substats
                                (vector-ref stats
                                            (index-two prevprev prev))])
                           (random-char substats)))])
      ; Build a list of successors then convert to a string
      (let kernel ([remaining (- len 2)]
                   [prevprev prevprev]
                   [prev prev]
                   [chars (list prev prevprev)])
        (if (zero? remaining)
            (list->string (reverse chars))
            (let ([char (random-next prevprev prev)])
              (kernel (decrement remaining)
                      prev
                      char
                      (cons char chars))))))))
