(ns calc.core
  (:require [clojure.string :as s])
  (:use [clojure.core.match :only (match)]
        [instaparse.core :only (parser)]
        [clojure.math.numeric-tower]))

(def calc-parse
  (parser
    "S = E
     E = ('-'|eps) T (('+'|'-') T)*
     T = P (('*'|'/') P)*
     P = F ('^' F)*
     F = N | '(' E ')'
     N = C+ ('.'C*|eps)
     C = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9' "))

(defn calc-eval [expr]
  (match [expr]
     [[:S exp]] (calc-eval exp)
     [[:E "-" & rest]] (- (calc-eval (into [:E] rest)))
     [[:E term]] (calc-eval term)
     [[:E term "+" & rest]] (+ (calc-eval term) (calc-eval (into [:E] rest)))
     [[:E term "-" & rest]] (- (calc-eval term) (calc-eval (into [:E] rest)))
     [[:T pow]] (calc-eval pow)
     [[:T pow "*" & rest]] (* (calc-eval pow) (calc-eval (into [:T] rest)))
     [[:T pow "/" & rest]] (/ (calc-eval pow) (calc-eval (into [:T] rest)))
     [[:P form]] (calc-eval form)
     [[:P form "^" & rest]] (expt (calc-eval form) (calc-eval (into [:P] rest)))
     [[:F "(" exp ")"]] (calc-eval exp)
     [[:F [:N & digs]]] (calc-eval (into [:N] digs))
     [[:N & digs]] (->> digs
                     (map #(match [%] [[:C c]] c :else %))
                     (s/join "")
                     (read-string))
     :else (str "Invalid expression: " expr)))

(defn calc [str]
   (-> str
     (s/replace #" " "")
     (calc-parse)
     (calc-eval)))