(set! *warn-on-reflection* true)

(ns caesar-cipher
 (:require (clojure.contrib [str-utils2 :as s]))
 (:use clojure.contrib.test-is))

;; forward declaring just so I can order the functions how I want...
(declare
  rot13
  rot-msg
  rot-letter
  in-range?
  rot-char)

(defn rot13 "rot13 implementation"
  [msg] (rot-msg msg 13))

(defn rot-msg
  "Cipher a message via caesarian rotation.
   Accepts only uppercase ascii and spaces.
     message - String to be encoded.
     i - factor by which to encode the message."
  [message i]
  (s/map-str #(rot-letter % i) message))

(defn rot-letter
  "applies caesar cipher to a single character. Only ciphers
   ascii a-z and A-Z, leaving other characters unchanged.
   c - a single ascii character.
   i - the factor by which to rotate the character."
  [c i] {:pre [(char? c) (number? i)]}
  (cond
    (in-range? c \A \Z) (rot-char c i \A \Z)
    (in-range? c \a \z) (rot-char c i \a \z)
    :else c))

(defn in-range? [^Character c start end]
  (if (or (.equals c start) (.equals c end))
    true
    (let [compared-to-start (compare c start)
          compared-to-end (compare c end)]
      (or (= 0 compared-to-start)
          (= 0 compared-to-end)
          (and (pos? compared-to-start) (neg? compared-to-end))))))

;; This is a slightly different solution than the one found around the internets,
;; because it doesn't attempt to map the original message to 0-base character lookup table.
;; Instead we just need to be provided with the start and end range (i.e. in a
;; typical cipher of the roman alphabet, the range is 0...25, but here  our range is based
; on the ASCII table). We then shift the character, and wrap around if we overflow the end boundary.
(defn rot-char
  [c i start end]
  (let [shift (mod i 26)
        shifted-char-as-int (+ (int c) shift)
        shifted-char (char shifted-char-as-int)]
    (if (in-range? shifted-char start end)
      shifted-char
      (let [start-minus-1 (- (int start) 1)
            overflow (- shifted-char-as-int (int end))]
        (char (+ start-minus-1 overflow))))))



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Unit tests

(deftest test-rot13
  (testing "rot13 must be reciprocal"
    (are (= _str (rot13 (rot13 _str)))
      "A MesSage"
      "Another message!"
      "Some goofy characters \\@#!^&*(_"
      "Numb3r5 and 13tt3r5")))

(deftest test-rot-1
  (testing "expected rot-1 functionality"
    (are (= _2 (rot-msg _1 1))
      "A" "B"
      "A1" "B1"
      "Aa" "Bb"
      "Zz" "Aa"
      "A1&*(" "B1&*(")))

(deftest test-rot-100
  (testing "rotating some messages 100 characters"
    (is (= "XHWDxhwd 100" (rot-msg "BLAHblah 100" 100)))))

(run-tests)

;; Example manual REPL unit testing output:
;;     user=> (in-ns 'caesar-cipher)
;;     #<Namespace caesar-cipher>
;;     caesar-cipher=> (run-tests)
;;
;;     Testing caesar-cipher
;;
;;     Ran 3 tests containing 10 assertions.
;;     0 failures, 0 errors.
;;     {:type :summary, :test 3, :pass 10, :fail 0, :error 0}
;;     caesar-cipher=>


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Just for fun...

(defn do-all-rotations [msg]
  (loop [i 0]
      (if (= i 26)
        (println "done")
        (do
          (println (rot-msg msg i))
          (recur (inc i))))))

(defn rotate-alphabet []
  (let [uppercase-alphabet "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        lowercase-alphabet "abcdefghijklmnopqrstuvwxyz"]
    (do
      (do-all-rotations uppercase-alphabet)
      (do-all-rotations lowercase-alphabet))))