    (bisection 1 f a b tolerance nmax)))
 
 (defn estimate-b
+  "Estimates parameter b of an exponential function based on `indegree-histogram`."
   [indegree-histogram b]
   (letfn [(sum-map [f] (reduce + (map f indegree-histogram)))]
     (- (* (sum-map (fn [[x _]] (* x (Math/exp (* 2 b x)))))
           (/ (sum-map (fn [[x y]] (* y (Math/exp (* b x)))))
              (sum-map (fn [[x _]] (Math/exp (* 2 b x))))))
        (sum-map (fn [[x y]] (* y x (Math/exp (* b x))))))))
+
+(defn compute-a
+  "Computes parameter a of an exponential function with parameter `b`
+  based on `indegree-histogram`."
+  [indegree-histogram b]
+  (letfn [(sum-map [f] (reduce + (map f indegree-histogram)))]
+    (/ (sum-map (fn [[x y]] (* y (Math/exp (* b x)))))
+       (sum-map (fn [[x _]] (Math/exp (* 2 b x)))))))
+
+(defn get-indegree-limit
+  "Get limiting indegree for exponential function parameterized with `a` and `b` at `angle` in degrees."
+  [a b angle]
+  (round-2 (/ (Math/log (/ (Math/tan (degrees->radians angle))
+                           (* a b)))
+              b)))
 
 (defn get-indegree-histogram
   "Compute indegree histogram based on the provided configuration.

(ns db-quiz-prep.prepare
  (:require [db-quiz-prep.sparql :as sparql]
            [clojure.java.io :as io]
            [clojure.edn :as edn]))

(defn- sign
  [number]
  (if (neg? number) - +))

(defn bisection
  ([n f a b tolerance nmax]
   (if (<= n nmax)
     (let [c (/ (+ a b) 2)
           fc (f c)]
       (if (< (/ (- b a) 2) tolerance)
         c
         (if (= (sign fc) (sign (f a)))
           (recur (inc n) f c b tolerance nmax)

           (mapv parse-int [indegree frequency]))
         (sparql/execute-unlimited-select-query config query))))

(defn materialize-difficulties
  "Precompute difficulties of questions based on their indegree."
  [config]
  (let [{{:keys [easy normal]} :split-angles} config
        indegree-histogram (get-indegree-histogram config)
        b (float (bisection (partial estimate-b indegree-histogram) -10 0 0.001 100))
        a (float (compute-a indegree-histogram b))
        maximum-indegree (get-indegree-limit a b easy)
        minimum-indegree (get-indegree-limit a b normal)
        update-template (slurp (io/resource "materialize_difficulties.mustache"))]
    (sparql/execute-unlimited-update config
                                     update-template
                                     :data {:maximum-indegree maximum-indegree
                                            :minimum-indegree minimum-indegree})))

(defn delete-difficulties
  "Delete materialized difficulties for given class.
  Used for testing."
  [{:keys [data params]
    :as config}]
  (let [update-template (slurp (io/resource "delete_difficulties.mustache"))
        update-string (render-string update-template (merge params data))]
    (sparql/execute-update config update-string)))

(defn materialize-questions
  "Materialize questions based on provided configuration"
  [config]
  (let [update-template (slurp (io/resource "materialize_questions.mustache"))]
    (sparql/execute-unlimited-update config update-template)))

(defn execute
  "Execute `task` given configuration in `config`."
  [{{:keys [page-size]
      :or {page-size 10000}} :sparql-endpoint
     :keys [data start-from]
     :or {start-from 0}
     :as config}
   task]
  (let [config' (merge config {:page-size page-size
                               :params data
                               :start-from start-from})]
    (case task
          "questions" (materialize-questions config')
          "difficulties" (materialize-difficulties config')
          "delete-difficulties" (delete-difficulties config'))))
