         (.log js/console "Whats up")
 
         ; Update `ModulesSelected`
-        (update-ModulesSelected-for-affected-days affectedDaysSet)
+        (update-ModulesSelected-for-affected-days! affectedDaysSet)
         (.log js/console "Boo ya!")
         (.log js/console (str "ModulesSelected:" (.stringify js/JSON (clj->js ModulesSelected))))
 

    ; adjust rowspan of <th>
    (attr $thElem "rowspan" (max nrNonEmptyRows 2))))

(defn- create-lesson-div
  "Creates a <div> element for a new lesson using jQuery"
  [& {:keys [moduleCode moduleName lessonType lessonGroup venue slotsOcc
             bgColorCssClass isActuallySelected?]}]
  (let [$divElem  ($ "<div />" (js-obj "class" "lesson"))]
    (.addClass $divElem bgColorCssClass)
    ; add background color css class
    (.append $divElem (text ($ "<p />") (str moduleCode " " moduleName)))
    (.append $divElem (text ($ "<p />") (str lessonType " [" lessonGroup "]")))
    (.append $divElem (text ($ "<p />") venue))
    (width $divElem (str (* half-hour-pixels slotsOcc) "px"))
    ; make the <div> less opaque for a lesson added by jQuery UI draggable
    (if (not isActuallySelected?)
        (.addClass $divElem "lesson-droppable-not-hover"))
    $divElem))

(defn- add-module-lesson!
  "Adds a single lesson of a module the timetable.
   Returns a `ModulesSelectedLessonInfo` object augmented with the `:divElem`,
   `:moduleCode`, `:lessonType` and `:lessonGroup` keys."
  [& {:keys [moduleCode moduleName lessonType lessonGroup
             day startTime endTime venue
             bgColorCssClass isActuallySelected?]}]
  (let [rowNum    (find-free-row-for-lesson day startTime endTime)
        slotsOcc  (- endTime startTime)

        $divElem
        (create-lesson-div :moduleCode moduleCode, :moduleName moduleName,
                           :lessonType lessonType, :lessonGroup lessonGroup,
                           :venue venue, :slotsOcc slotsOcc,
                           :bgColorCssClass bgColorCssClass,
                           :isActuallySelected? isActuallySelected?)]
    ; Create new row if necessary
    (if (= rowNum (timetable-day-get-nr-rows day))
        (add-new-row-to-timetable-day! day))

    ; Update in-memory representation of timetable
    (timetable-add-lesson! day
                           rowNum
                           {:moduleCode moduleCode,
                            :lessonType lessonType,
                            :lessonGroup lessonGroup,
                            :startTime startTime,
                            :endTime endTime}
                           $divElem)

    (let [$td (html-timetable-get-td day rowNum startTime)]
      (.append $td $divElem)
      ; Increase colspan and delete the <td> after it that occupy the timeslot
      ; of the lesson
      (attr $td "colspan" slotsOcc)
      (doseq [$siblingTd (take (- slotsOcc 1) (.nextAll $td))]
        (.remove $siblingTd))

      {:day day, :rowNum rowNum, :startTime startTime, :endTime endTime,
       :moduleCode moduleCode, :lessonType lessonType, :lessonGroup lessonGroup,
       :divElem $divElem})))

(def ^{:doc     "Sequence of `TimetableLessonInfo` objects augmented with the
                 `:divElem`, `:moduleCode`, `:lessonType`, `:lessonGroup` keys.
                 These are 'fake' lessons created when a selected lesson is
                 being dragged by the user, in order for him/her to switch to
                 a different lesson group for that lesson type."
       :private true}
  Lessons-Created-By-Draggable nil)

; Forward declaration
(declare add-module-lesson-group!)
(declare overall-timetable-prune-empty-rows)
(declare add-missing-td-elements-replacing-lesson)
(declare remove-lesson-group-html)
(declare shift-lessons-upwards-to-replace-empty-slots!)
(declare update-ModulesSelected-for-affected-days)

(def ^{:doc     "Key for a data attribute added to a draggable <div> helper
                 when the helper has been dropped onto a droppable <div>.
                 This is used to detect if a lesson group switch has occurred."
       :private true}
  LESSON-GROUP-CHANGE-KEY "lessonGroupChangeTo")

(defn- lesson-draggable-start-evt-handler-maker
  "Returns a function that can be used as the `start` event handler for a
   draggable lesson.

   Details of the returned function can be found here:

       http://api.jqueryui.com/draggable/#event-start"
  [moduleCode lessonType selectedLessonGroup bgColorCssClass]
  (fn [evt ui]
    (.addClass (aget ui "helper") "lesson-draggable-helper")
    (.css (aget ui "helper") "cursor" "grabbing")
    (let [allLessonGroups
          (get-all-lesson-group-strings moduleCode lessonType)

          notSelectedLessonGroups
          (remove #{selectedLessonGroup} allLessonGroups)

          ; Add all the <div> elements
          augmentedTTLessonInfoSeq
          (doall
            (flatten
              (map (fn [unselectedLessonGroup]
                     (add-module-lesson-group! moduleCode lessonType
                                               unselectedLessonGroup
                                               bgColorCssClass false))
                   notSelectedLessonGroups)))]
      (set! Lessons-Created-By-Draggable augmentedTTLessonInfoSeq))))

(defn- $lesson-div-remove
  "Removes the <div> object of a lesson module, and replaces the former
   columns occupied by its parent <td> with new <td> elements."
  [$lessonDiv startTime endTime]
  (let [$parentTd (parent $lessonDiv)]
    (remove-attr $parentTd "colspan")
    (.remove $lessonDiv)
    (add-missing-td-elements-replacing-lesson $parentTd startTime endTime)))

(defn- lesson-draggable-stop-evt-handler-maker
  "Returns a function used as the `stop` event handler for a draggable lesson."
  [moduleCode lessonType lessonGroup bgColorCssClass]
  (fn [evt ui]
    (let [$helper         (aget ui "helper")
          destLessonGroup (data $helper LESSON-GROUP-CHANGE-KEY)

          affectedDaysSet
          (get-set-from-seq-of-maps-with-day-key Lessons-Created-By-Draggable)]

      (doseq [augTTLessonInfo Lessons-Created-By-Draggable]
        (timetable-remove-lesson! (:day augTTLessonInfo)
                                  (:rowNum augTTLessonInfo)
                                  (dissoc augTTLessonInfo :divElem :day
                                          :rowNum)))

      (doseq [augTTLessonInfo Lessons-Created-By-Draggable]
        (let [$divElem  (:divElem augTTLessonInfo)]
          ($lesson-div-remove $divElem (:startTime augTTLessonInfo)
                              (:endTime augTTLessonInfo))))

      (overall-timetable-prune-empty-rows affectedDaysSet)
      (set! Lessons-Created-By-Draggable nil)

      ; User has dropped the draggable helper <div> onto a droppable
      (if (not (nil? destLessonGroup))
          (let [augTTLessonInfoSeq (remove-lesson-group-html moduleCode
                                                             lessonType)
                affectedDaysSet    (get-set-from-seq-of-maps-with-day-key
                                     augTTLessonInfoSeq)]
            ; remove the current lesson group
            (doseq [augTTLessonInfo augTTLessonInfoSeq]
              (let [day          (:day augTTLessonInfo)
                    rowNum       (:rowNum augTTLessonInfo)
                    ttLessonInfo (dissoc augTTLessonInfo :day :rowNum)]
                (timetable-remove-lesson! day rowNum ttLessonInfo)))

            (shift-lessons-upwards-to-replace-empty-slots! augTTLessonInfoSeq)
            (overall-timetable-prune-empty-rows affectedDaysSet)
            (update-ModulesSelected-for-affected-days affectedDaysSet)

            ; add the newly selected lesson group
            (add-module-lesson-group! moduleCode lessonType destLessonGroup
                                      bgColorCssClass true)

            (update-document-location-hash-with-changed-lesson-group!
              moduleCode lessonType destLessonGroup))))))

(defn- make-added-lessons-draggable
  "Makes the <div> elements of selected lessons draggable."
  [$divElemSeq moduleCode lessonType lessonGroup bgColorCssClass]
  ; And Drag event handler
  (doseq [$divElem $divElemSeq]
    (.css $divElem "cursor" "grab")
    (.draggable $divElem
                (js-obj "zIndex" 100
                        "helper" "clone"
                        "revert" "invalid"
                        "start"  (lesson-draggable-start-evt-handler-maker
                                   moduleCode lessonType lessonGroup
                                   bgColorCssClass)
                        "stop"   (lesson-draggable-stop-evt-handler-maker
                                   moduleCode lessonType lessonGroup
                                   bgColorCssClass)))))

(defn- make-fake-lessons-droppable
  "For 'fake' lessons created due to a lesson <div> being dragged, we make
   them droppable in order for the user to switch lesson groups."
  [augModuleSelectedLessonInfoSeq]
  (doseq [augModuleSelectedLessonInfo augModuleSelectedLessonInfoSeq]
    (let [$divElem (:divElem augModuleSelectedLessonInfo)]
      (.droppable
        $divElem
        (js-obj "tolerance" "intersect"

                "over"
                (fn [evt ui]
                  (.removeClass $divElem "lesson-droppable-not-hover")
                  (.addClass $divElem "lesson-droppable-hover"))

                "out"
                (fn [evt ui]
                  (.removeClass $divElem "lesson-droppable-hover")
                  (.addClass $divElem "lesson-droppable-not-hover"))

                "drop"
                (fn [evt ui]
                  (let [$helper (aget ui "helper")]
                    ; Set a data attribute on the draggable helper to signal
                    ; that the user has dropped the <div> on a droppable to
                    ; change the lesson group
                    (data $helper
                          LESSON-GROUP-CHANGE-KEY
                          (:lessonGroup augModuleSelectedLessonInfo)))))))))

(defn- add-module-lesson-group!
  "Adds a lesson group of a module to the timetable.

   The `isActuallySelected?` param is used to indicate if the current lesson
   group is selected by the user.
   A value of `true` indicates that the user actually selected the lesson group.
   A value of `false` indicates that the current lesson group is added by the
   `lesson-draggable-start-evt-handler-maker` function and is not selected by
   the user.

   Returns a sequence of `ModulesSelectedLessonInfo` objects each augmented
   with the `:divElem`, `:moduleCode`, `:lessonType`, `:lessonGroup` keys."
  [moduleCode lessonType lessonGroup bgColorCssClass isActuallySelected?
   & {:keys [addedViaUrlHashInit?] :or {addedViaUrlHashInit? false}}]
  (if (module-has-lesson-group? moduleCode lessonType lessonGroup)
      (let [moduleName          (get-module-name-from-module-code moduleCode)
            modulesMapLessonSeq (get-ModulesMapLesson-seq moduleCode lessonType
                                                          lessonGroup)

            augLessonInfoSeq
            (doall
              (map (fn [modulesMapLesson]
                     (add-module-lesson!
                       :moduleCode moduleCode, :moduleName moduleName,
                       :lessonType lessonType, :lessonGroup lessonGroup,
                       :day        (:day modulesMapLesson),
                       :startTime  (:startTime modulesMapLesson),
                       :endTime    (:endTime modulesMapLesson),
                       :venue      (:venue modulesMapLesson),
                       :bgColorCssClass bgColorCssClass,
                       :isActuallySelected? isActuallySelected?))
                   modulesMapLessonSeq))

            lessonInfoSeq       (map #(dissoc %1 :divElem :moduleCode
                                              :lessonType :lessonGroup)
                                     augLessonInfoSeq)
            $divElemSeq         (map #(:divElem %1) augLessonInfoSeq)]
        (if isActuallySelected?
            (do
              (update-ModulesSelected-with-lesson-group! moduleCode lessonType
                                                         lessonGroup
                                                         lessonInfoSeq)
              (if (module-not-in-ModulesSelectedOrder? moduleCode)
                  (do
                    (add-to-ModulesSelectedOrder! moduleCode)
                    ; Micro optimization for modules added via url hash
                    ; during initialization
                    (if (not addedViaUrlHashInit?)
                        (select2-box-update-modules))))

              ; Only lesson types with more than 1 option of lesson group
              ; will be draggable
              (if (module-lesson-type-has-multiple-choices? moduleCode
                                                            lessonType)
                  (make-added-lessons-draggable $divElemSeq moduleCode
                                                lessonType lessonGroup
                                                bgColorCssClass)))

            ; lesson was added due to draggable <div>
            ; Make it droppable
            (make-fake-lessons-droppable augLessonInfoSeq))
        augLessonInfoSeq)))

(defn add-module!
  "Adds a module to the timetable.

   A random lesson group of each type of lesson (Lecture, Tutorial, etc) will
   be added."
  [moduleCode]
  (if (not (is-module-selected? moduleCode))
      (let [lessonTypes     (get-all-lesson-types-for-module moduleCode)
            bgColorCssClass (get-next-lesson-bg-color-css-class)

            newModInfoSeq
            (reduce (fn [moduleInfoSeq lessonType]
                      (conj moduleInfoSeq
                            {:moduleCode  moduleCode,
                             :lessonType  lessonType,
                             :lessonGroup
                             (get-first-lesson-group-for-module-lesson-type
                               moduleCode lessonType)}))
                    []
                    lessonTypes)]

        (doseq [moduleInfo newModInfoSeq]
          (add-module-lesson-group! (:moduleCode moduleInfo)
                                    (:lessonType moduleInfo)
                                    (:lessonGroup moduleInfo)
                                    bgColorCssClass
                                    true))

        ; Update URL hash with newly added module
        (update-document-location-hash-with-new-module!
          {:moduleCode moduleCode,

           :preToUrlHashLessonGroupSeqSorted
           (sort-PreToUrlHashLessonGroup-seq
             (map #(dissoc
                     (assoc %1 :lessonType
                            (Lesson-Type-Long-To-Short-Form (:lessonType %1)))
                     :moduleCode)
                  newModInfoSeq))}))))

(defn- get-module-info-from-url-hash-module-info
  "Computes the final module info sequence for modules added via the url hash
   on page initialization.

   NOTE: This function should only be called by
         `add-module-lesson-groups-from-url-hash!`"
  [moduleInfoSeq]
  (let [moduleLessonGroupsMap
        (reduce (fn [lgMap modInfo]
                  ; Replaces earlier lesson groups with later ones if we
                  ; encounter a duplicate lesson type
                  (assoc-in lgMap [(:moduleCode modInfo) (:lessonType modInfo)]
                            (:lessonGroup modInfo)))
                {}
                moduleInfoSeq)

        moduleCodesSeq (distinct (map #(:moduleCode %1) moduleInfoSeq))

        ; Map of Module Code -> Sequence of lesson type strings
        moduleLessonTypes
        (reduce (fn [lessonTypesMap moduleCode]
                  (let [ltMap (get moduleLessonGroupsMap moduleCode)]
                    (assoc lessonTypesMap moduleCode (keys ltMap))))
                {}
                moduleCodesSeq)

        ; Similar to `moduleLessonTypes`, but contains the full sequence of
        ; lesson type strings
        moduleLessonTypesFull
        (reduce (fn [lessonTypesMap moduleCode]
                  (assoc lessonTypesMap moduleCode
                         (get-all-lesson-types-for-module moduleCode)))
                {}
                moduleCodesSeq)

        ; Obtained from the difference between `moduleLessonTypesFull` and
        ; `moduleLessonTypes`
        missingModuleLessonTypes
        (reduce (fn [missingLessonTypesMap moduleCode]
                  (let [missingLessonTypes
                        (clojure.set/difference
                          (set (get moduleLessonTypesFull moduleCode))
                          (set (get moduleLessonTypes moduleCode)))]
                    (if (empty? missingLessonTypes)
                        missingLessonTypesMap
                        (assoc missingLessonTypesMap moduleCode
                               (seq missingLessonTypes)))))
                {}
                moduleCodesSeq)

        ; similar to `moduleLessonGroupsMap`, but includes missing lesson types
        moduleLessonGroupsMapFinal
        (reduce (fn [lgMapFinal [moduleCode missingLessonTypes]]
                  (reduce
                    (fn [lgMap lessonType]
                      (assoc-in lgMap [moduleCode lessonType]
                                (get-first-lesson-group-for-module-lesson-type
                                  moduleCode lessonType)))
                    lgMapFinal
                    missingLessonTypes))
                moduleLessonGroupsMap
                missingModuleLessonTypes)]

    ; Produce the final module info sequence
    (flatten
      (map (fn [moduleCode]
             (map (fn [[lessonType lessonGroup]]
                    {:moduleCode  moduleCode
                     :lessonType  lessonType
                     :lessonGroup lessonGroup})
                  (get moduleLessonGroupsMapFinal moduleCode)))
           moduleCodesSeq))))

(defn add-module-lesson-groups-from-url-hash!
  "Adds the module lesson groups available in the url hash. Erroneous lesson
   groups are ignored. If there are missing lesson groups for any module after
   going through the url hash, a random lesson group is chosen.

   NOTE: This function should only be called once."
  [urlHash]
  (let [moduleUrlHashArray                (.split urlHash "&")

        modUrlHashRegex
        #"^([A-Z]+\d{4}[A-Z]*)_(DL|L|LAB|PL|PT|R|SEM|ST|T|T2|T3)=([A-Z0-9]+)$"

        ; Sequence of non-nil match arrays
        matchArraySeq
        (filter (fn [matchArray] (not (nil? matchArray)))
                (map (fn [modUrlHash] (.exec modUrlHashRegex modUrlHash))
                     moduleUrlHashArray))

        ; Sequence of existing lesson groups
        moduleInfoExistent
        (filter
          #(module-has-lesson-group? (:moduleCode %1) (:lessonType %1)
                                     (:lessonGroup %1))
          ; convert matchArrays to maps
          (map (fn [matchArray]
                 {:moduleCode  (nth matchArray 1)
                  :lessonType  (Lesson-Type-Short-To-Long-Form
                                 (nth matchArray 2))
                  :lessonGroup (nth matchArray 3)})
               matchArraySeq))

        ; Module code to CSS class for lesson div background color
        moduleToColorsMap
        (reduce
          (fn [m2cMap moduleCode]
            (assoc m2cMap moduleCode (get-next-lesson-bg-color-css-class)))
          {}
          (distinct (map (fn [modInfo] (:moduleCode modInfo))
                         moduleInfoExistent)))

        moduleInfoFinal
        (get-module-info-from-url-hash-module-info moduleInfoExistent)]

    (.log js/console (str "moduleInfoExistent = " (.stringify js/JSON (clj->js moduleInfoExistent))))
    (.log js/console (str "moduleInfoFinal = " (.stringify js/JSON (clj->js moduleInfoFinal))))
    ; Add the module lesson groups
    (doseq [modInfo moduleInfoFinal]
      (let [moduleCode      (:moduleCode modInfo)
            bgColorCssClass (get moduleToColorsMap moduleCode)]
        (add-module-lesson-group! moduleCode
                                  (:lessonType modInfo)
                                  (:lessonGroup modInfo)
                                  bgColorCssClass
                                  true
                                  :addedViaUrlHashInit? true)))

    ; Update Select2 box, since this is not done in `add-module-lesson-group!`.
    ; Repeated work will be done there if we did so
    (select2-box-update-modules)
    ; Update url hash
    (set-document-location-hash-based-on-modules-order!)))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Functions for Removing Modules
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- add-missing-td-elements-replacing-lesson
  "Adds <td> elements that were removed by the `add-module-lesson!` function
   to make way for the lesson."
  [$parentTd startTime endTime]
  (loop [$currentTd $parentTd
         timeIdx    (inc startTime)]
    (if (>= timeIdx endTime)
        nil
        (let [[hourClass minuteClass]
              (get-css-hour-minute-classes-for-time timeIdx)

              $newTdElem
              ($ "<td />" (js-obj "class" (str hourClass " " minuteClass)))]
          ; Insert after new <td> after the current <td>
          (insert-after $newTdElem $currentTd)
          ; Then use the new <td> as the current <td> in the next iteration
          (recur $newTdElem (inc timeIdx))))))

(defn- remove-lesson-group-html
  "Removes a lesson group for a module from the HTML timetable.
   This does NOT remove any empty rows / perform module shifting.

   Returns a sequence of `ttLessonInfo` objects which contain
   additional `:day` and `:rowNum` keys."
  [moduleCode lessonType]
  (let [lessonGroupDetails (get-in ModulesSelected [moduleCode lessonType])
        lessonGroup        (:label lessonGroupDetails)
        lessonInfoSeq      (:info lessonGroupDetails)]
    (map (fn [modSelLessonInfo]
           (let [day          (:day modSelLessonInfo)
                 rowNum       (:rowNum modSelLessonInfo)
                 startTime    (:startTime modSelLessonInfo)
                 endTime      (:endTime modSelLessonInfo)
                 ttLessonInfo {:moduleCode moduleCode, :lessonType lessonType,
                               :lessonGroup lessonGroup, :startTime startTime,
                               :endTime endTime}
                 $lessonDiv   (timetable-get-lesson-div day rowNum
                                                        ttLessonInfo)]
             ($lesson-div-remove $lessonDiv startTime endTime)
             (assoc ttLessonInfo :day day, :rowNum rowNum)))
         lessonInfoSeq)))

(defn- get-largest-consecutive-time-slot-freed-up-by-lesson
  "Given a removed lesson, find the largest interval on the same row which is
   freed up due to the its removal.

   For instance, given this timetable row:

   x | x | x |  |  |  | y | y | y | y |  |  | z | z

   And we remove the lesson occupying the slots labelled 'y'. The 4 slots
   currently occupied by 'y', along with the 3 empty slots to its left and the
   2 empty slots to its right will be freed up.

   Given that the first slot occupied by the label 'x' is the index 0, this
   function will return the vector [3 11].

   The parameter `removedAugTTLessonInfo` is a `TimetableLessonInfo` object
   augmented with the `:day` and `:rowNum` keys."
  [removedAugTTLessonInfo]
  (let [day        (:day removedAugTTLessonInfo)
        rowNum     (:rowNum removedAugTTLessonInfo)
        startTime  (:startTime removedAugTTLessonInfo)
        endTime    (:endTime removedAugTTLessonInfo)
        minTimeIdx time-helper/TIME-INDEX-MIN
        maxTimeIdx time-helper/TIME-INDEX-MAX]

    (.log js/console "get-largest-consecutive-time-slot-freed-up-by-lesson")
    [
      ; Find the lesson right before the removed lesson
      (let [immediateTTLessonInfoBefore
            (timetable-get-closest-TimetableLessonInfo-before-time
              day rowNum startTime)]
        (if (nil? immediateTTLessonInfoBefore)
            minTimeIdx
            (:endTime immediateTTLessonInfoBefore)))

      ; Find the lesson right after the removed lesson
      (let [immediateTTLessonInfoAfter
            (timetable-get-closest-TimetableLessonInfo-on-or-after-time
              day rowNum endTime)]
        (if (nil? immediateTTLessonInfoAfter)
            maxTimeIdx
            (dec (:startTime immediateTTLessonInfoAfter))))
    ]))

(defn- find-replacement-lessons-within-time
  "For a removed lesson, find other lessons on the same day but in a row below
   the removed lesson which could be shifted upwards and take the timeslots
   freed up by the removed lesson.

   The `removedAugTTLessonInfo` parameter is an `TimetableLessonInfo` object
   augmented with the `:day` and `:rowNum` keys.

   Returns a map, where keys are `TimetableLessonInfo` objects augmented with
   `:day` and `:rowNum` keys, and values are jQuery <div> objects for the
   shifted lesson."
  [{:keys [day rowNum startTime endTime] :as removedAugTTLessonInfo}]
  (let [; This is the time range freed up by removal of the lesson
        [lowTimeIdx highTimeIdx]
        (get-largest-consecutive-time-slot-freed-up-by-lesson
          removedAugTTLessonInfo)

        not-occupied?
        (fn [occupiedVec startTime endTime]
          (.log js/console (str "inside not-occupied?, occupiedVec = " (.stringify js/JSON (clj->js occupiedVec))))
          (every? #(nil? %1)
                  (map #(nth occupiedVec %1)
                       (range startTime endTime))))]

    (.log js/console "find-replacement-lessons-within-time")
    (.log js/console (str "lowTimeIdx = " lowTimeIdx ", highTimeIdx = " highTimeIdx))
    (:augTTLessonInfoMapToShift
      (reduce
        (fn [{:keys [augTTLessonInfoMapToShift occupiedVec]
              :as   reduceResult}
             rowIdx]
          (if (timetable-row-empty? day rowIdx)
              ; Skip empty row
              reduceResult

              ; Non-empty row
              (let [ttLessonInfoToShift
                    (timetable-row-get-lessons-satisfying-pred
                      day rowIdx
                      (fn [{:keys [startTime endTime]}]
                        (and (>= startTime lowTimeIdx)
                             (<= (dec endTime) highTimeIdx)
                             (not-occupied? occupiedVec startTime endTime))))]
                (.log js/console "suspect above")
                (.log js/console (str "rowIdx = " rowIdx))
                (.log js/console (str "occupiedVec = " (.stringify js/JSON (clj->js occupiedVec))))
                {:augTTLessonInfoMapToShift
                 (reduce (fn [augTTLessonInfoMap [ttLessonInfo $divElem]]
                           (.log js/console ":augTTLessonInfoMapToShift")
                           (assoc augTTLessonInfoMap
                                  (assoc ttLessonInfo :day day :rowNum rowIdx)
                                  $divElem))
                         augTTLessonInfoMapToShift
                         ttLessonInfoToShift)

                 :occupiedVec
                 (reduce (fn [occVec [{:keys [startTime endTime]}  _]]
                           (.log js/console ":occupiedVec")
                           (reduce (fn [ov timeIdx] (assoc ov timeIdx true))
                                   occVec
                                   (range startTime endTime)))
                         occupiedVec
                         ttLessonInfoToShift)})))
        {:augTTLessonInfoMapToShift {},
         ; nil = free slot, true = occupied slot
         :occupiedVec      (vec (map (fn [t] nil)
                                     (range time-helper/TIME-INDEX-MIN
                                            (inc time-helper/TIME-INDEX-MAX))))}
        ; go through each row in the day, below the current row
        (drop (inc rowNum) (range (timetable-day-get-nr-rows day)))))))

(defn- shift-lesson-to-row!
  "Shifts a lesson from its original row up to the given row.

   The parameter `augTTLessonInfo` is a `TimetableLessonInfo` object augmented
   with the `:day` and `:rowNum` keys."
  [destinationRowNum {:keys [day rowNum startTime endTime] :as augTTLessonInfo}
   $divElem]
  (let [slotsOccupied (- endTime startTime)
        $sourceTd     (html-timetable-get-td day rowNum startTime)
        $destTd       (html-timetable-get-td day destinationRowNum startTime)
        ttLessonInfo  (dissoc augTTLessonInfo :day :rowNum)]
    (.log js/console (str "day = " day ", sourceRowNum = " rowNum ", destinationRowNum = " destinationRowNum))
    ; Transfer lesson <div> to the destination <td>
    (.append $destTd $divElem)
    ; restore the colspan of the source <td>
    (remove-attr $sourceTd "colspan")
    ; fill in removed <td> for the source <td>
    (add-missing-td-elements-replacing-lesson $sourceTd startTime endTime)
    ; remove sibling <td> for the destination <td> to make way for columns
    ; occupied by $divElem
    (doseq [$siblingTd (take (dec slotsOccupied) (.nextAll $destTd))]
      (.remove $siblingTd))
    ; update colspan of destination <td>
    (attr $destTd "colspan" slotsOccupied)
    ; update `Timetable` global
    (timetable-remove-lesson! day rowNum ttLessonInfo)
    (timetable-add-lesson! day destinationRowNum ttLessonInfo $divElem)))

(defn- shift-lessons-upwards-to-replace-empty-slots!
  "For each removed lesson, see if there are any lessons in rows below it that
   can be shifted upwards to occupy the empty slots resulting from its removal.

   The parameter `removedAugmentedTTLessonInfoSeq` is a sequence of
   `TimetableLessonInfo` objects augmented with the `:day` and `:rowNum` keys."
  [removedAugmentedTTLessonInfoSeq]
  (let [; we do not consider any lessons on empty rows for ease of
        ; implementation
        exclude-lessons-on-empty-rows
        (fn [lessonInfoSeq]
          (filter (fn [{:keys [day rowNum]}]
                    (timetable-row-not-empty? day rowNum))
                  lessonInfoSeq))

        sort-lesson-info-seq
        (fn [lessonInfoSeq]
          (sort #(let [dayA (:day %1)
                       dayB (:day %2)]
                   (if (= dayA dayB)
                       (< (:rowNum %1) (:rowNum %2))
                       (< dayA dayB)))
                lessonInfoSeq))]

    (.log js/console "Hoo hoo")
    (loop [sortedAugTTLessonInfoSeq
           (sort-lesson-info-seq (exclude-lessons-on-empty-rows
                                   removedAugmentedTTLessonInfoSeq))

           nextLessonInfoVec    []]
      (.log js/console "Inside the loop, the loop")
      (cond (and (empty? sortedAugTTLessonInfoSeq) (empty? nextLessonInfoVec))
            nil

            ; we're done for the current set of lessons. Move on to the
            ; set of lessons that were shifted upwards.
            (empty? sortedAugTTLessonInfoSeq)
            (recur (sort-lesson-info-seq (exclude-lessons-on-empty-rows
                                           nextLessonInfoVec))
                   [])

            ; find timeslot to shift up to replace gap
            :else
            (let [currentAugTTLessonInfo (first sortedAugTTLessonInfoSeq)
                  rowNum                 (:rowNum currentAugTTLessonInfo)

                  ; map of augmented `TimeTableLessonInfo` objects to the
                  ; jQuery <div> element
                  augTTLessonInfoTo$DivElem
                  (find-replacement-lessons-within-time currentAugTTLessonInfo)]

              (doseq [[augTTLessonInfo $divElem] augTTLessonInfoTo$DivElem]
                (.log js/console "shift-lesson-to-row")
                (shift-lesson-to-row! rowNum augTTLessonInfo $divElem))
              (.log js/console "gonna recur...")
              (recur (rest sortedAugTTLessonInfoSeq)
                     ; append newly shifted lessons to nextLessonInfoVec
                     (reduce (fn [augTTLessonInfoVec [augTTLessonInfo _]]
                               (conj augTTLessonInfoVec augTTLessonInfo))
                             nextLessonInfoVec
                             augTTLessonInfoTo$DivElem)))))))

(defn- overall-timetable-prune-empty-rows
  "Removes empty rows resulting from the removal of lessons on given days."
  [affectedDaysSet]
  (doseq [day affectedDaysSet]
    (let [rowsPartition (timetable-day-get-empty-and-non-empty-rows day)]
      (.log js/console (str "day = " day ", emptyRows = "
                            (.stringify js/JSON (clj->js (:emptyRows rowsPartition)))
                            ", nonEmptyRows = "
                            (.stringify js/JSON (clj->js (:nonEmptyRows rowsPartition)))))

      (timetable-prune-empty-rows-for-day! day rowsPartition)
      (html-timetable-prune-empty-rows-for-day day rowsPartition))))

(defn- update-ModulesSelected-for-affected-days
  "Given a set of days that may possibly be affected by removal and shifting of
   lessons, update the individual entries in the `ModulesSelected` global."
  [affectedDaysSet]

                         :startTime startTime,
                         :endTime endTime}))))))))))

(defn- create-lesson-div
  "Creates a <div> element for a new lesson using jQuery"
  [& {:keys [moduleCode moduleName lessonType lessonGroup venue slotsOcc
             bgColorCssClass isActuallySelected?]}]
  (let [$divElem  ($ "<div />" (js-obj "class" "lesson"))]
    (.addClass $divElem bgColorCssClass)
    ; add background color css class
    (.append $divElem (text ($ "<p />") (str moduleCode " " moduleName)))
    (.append $divElem (text ($ "<p />") (str lessonType " [" lessonGroup "]")))
    (.append $divElem (text ($ "<p />") venue))
    (width $divElem (str (* half-hour-pixels slotsOcc) "px"))
    ; make the <div> less opaque for a lesson added by jQuery UI draggable
    (if (not isActuallySelected?)
        (.addClass $divElem "lesson-droppable-not-hover"))
    $divElem))

(defn- add-module-lesson!
  "Adds a single lesson of a module the timetable.
   Returns a `ModulesSelectedLessonInfo` object augmented with the `:divElem`,
   `:moduleCode`, `:lessonType` and `:lessonGroup` keys."
  [& {:keys [moduleCode moduleName lessonType lessonGroup
             day startTime endTime venue
             bgColorCssClass isActuallySelected?]}]
  (let [rowNum    (find-free-row-for-lesson day startTime endTime)
        slotsOcc  (- endTime startTime)

        $divElem
        (create-lesson-div :moduleCode moduleCode, :moduleName moduleName,
                           :lessonType lessonType, :lessonGroup lessonGroup,
                           :venue venue, :slotsOcc slotsOcc,
                           :bgColorCssClass bgColorCssClass,
                           :isActuallySelected? isActuallySelected?)]
    ; Create new row if necessary
    (if (= rowNum (timetable-day-get-nr-rows day))
        (add-new-row-to-timetable-day! day))

    ; Update in-memory representation of timetable
    (timetable-add-lesson! day
                           rowNum
                           {:moduleCode moduleCode,
                            :lessonType lessonType,
                            :lessonGroup lessonGroup,
                            :startTime startTime,
                            :endTime endTime}
                           $divElem)

    (let [$td (html-timetable-get-td day rowNum startTime)]
      (.append $td $divElem)
      ; Increase colspan and delete the <td> after it that occupy the timeslot
      ; of the lesson
      (attr $td "colspan" slotsOcc)
      (doseq [$siblingTd (take (- slotsOcc 1) (.nextAll $td))]
        (.remove $siblingTd))

      {:day day, :rowNum rowNum, :startTime startTime, :endTime endTime,
       :moduleCode moduleCode, :lessonType lessonType, :lessonGroup lessonGroup,
       :divElem $divElem})))

(def ^{:doc     "Sequence of `TimetableLessonInfo` objects augmented with the
                 `:divElem`, `:moduleCode`, `:lessonType`, `:lessonGroup` keys.
                 These are 'fake' lessons created when a selected lesson is
                 being dragged by the user, in order for him/her to switch to
                 a different lesson group for that lesson type."
       :private true}
  Lessons-Created-By-Draggable nil)

; Forward declaration
(declare add-module-lesson-group!)
(declare overall-timetable-prune-empty-rows)
(declare add-missing-td-elements-replacing-lesson)
(declare remove-lesson-group-html)
(declare shift-lessons-upwards-to-replace-empty-slots!)

(def ^{:doc     "Key for a data attribute added to a draggable <div> helper
                 when the helper has been dropped onto a droppable <div>.
                 This is used to detect if a lesson group switch has occurred."
       :private true}
  LESSON-GROUP-CHANGE-KEY "lessonGroupChangeTo")

(defn- lesson-draggable-start-evt-handler-maker
  "Returns a function that can be used as the `start` event handler for a
   draggable lesson.

   Details of the returned function can be found here:

       http://api.jqueryui.com/draggable/#event-start"
  [moduleCode lessonType selectedLessonGroup bgColorCssClass]
  (fn [evt ui]
    (.addClass (aget ui "helper") "lesson-draggable-helper")
    (.css (aget ui "helper") "cursor" "grabbing")
    (let [allLessonGroups
          (get-all-lesson-group-strings moduleCode lessonType)

          notSelectedLessonGroups
          (remove #{selectedLessonGroup} allLessonGroups)

          ; Add all the <div> elements
          augmentedTTLessonInfoSeq
          (doall
            (flatten
              (map (fn [unselectedLessonGroup]
                     (add-module-lesson-group! moduleCode lessonType
                                               unselectedLessonGroup
                                               bgColorCssClass false))
                   notSelectedLessonGroups)))]
      (set! Lessons-Created-By-Draggable augmentedTTLessonInfoSeq))))

(defn- $lesson-div-remove
  "Removes the <div> object of a lesson module, and replaces the former
   columns occupied by its parent <td> with new <td> elements."
  [$lessonDiv startTime endTime]
  (let [$parentTd (parent $lessonDiv)]
    (remove-attr $parentTd "colspan")
    (.remove $lessonDiv)
    (add-missing-td-elements-replacing-lesson $parentTd startTime endTime)))

(defn- lesson-draggable-stop-evt-handler-maker
  "Returns a function used as the `stop` event handler for a draggable lesson."
  [moduleCode lessonType lessonGroup bgColorCssClass]
  (fn [evt ui]
    (let [$helper         (aget ui "helper")
          destLessonGroup (data $helper LESSON-GROUP-CHANGE-KEY)

          affectedDaysSet
          (get-set-from-seq-of-maps-with-day-key Lessons-Created-By-Draggable)]

      (doseq [augTTLessonInfo Lessons-Created-By-Draggable]
        (timetable-remove-lesson! (:day augTTLessonInfo)
                                  (:rowNum augTTLessonInfo)
                                  (dissoc augTTLessonInfo :divElem :day
                                          :rowNum)))

      (doseq [augTTLessonInfo Lessons-Created-By-Draggable]
        (let [$divElem  (:divElem augTTLessonInfo)]
          ($lesson-div-remove $divElem (:startTime augTTLessonInfo)
                              (:endTime augTTLessonInfo))))

      (overall-timetable-prune-empty-rows affectedDaysSet)
      (set! Lessons-Created-By-Draggable nil)

      ; User has dropped the draggable helper <div> onto a droppable
      (if (not (nil? destLessonGroup))
          (let [augTTLessonInfoSeq (remove-lesson-group-html moduleCode
                                                             lessonType)
                affectedDaysSet    (get-set-from-seq-of-maps-with-day-key
                                     augTTLessonInfoSeq)]
            ; remove the current lesson group
            (doseq [augTTLessonInfo augTTLessonInfoSeq]
              (let [day          (:day augTTLessonInfo)
                    rowNum       (:rowNum augTTLessonInfo)
                    ttLessonInfo (dissoc augTTLessonInfo :day :rowNum)]
                (timetable-remove-lesson! day rowNum ttLessonInfo)))

            (shift-lessons-upwards-to-replace-empty-slots! augTTLessonInfoSeq)
            (overall-timetable-prune-empty-rows affectedDaysSet)
            (update-ModulesSelected-for-affected-days! affectedDaysSet)

            ; add the newly selected lesson group
            (add-module-lesson-group! moduleCode lessonType destLessonGroup
                                      bgColorCssClass true)

            (update-document-location-hash-with-changed-lesson-group!
              moduleCode lessonType destLessonGroup))))))

(defn- make-added-lessons-draggable
  "Makes the <div> elements of selected lessons draggable."
  [$divElemSeq moduleCode lessonType lessonGroup bgColorCssClass]
  ; And Drag event handler
  (doseq [$divElem $divElemSeq]
    (.css $divElem "cursor" "grab")
    (.draggable $divElem
                (js-obj "zIndex" 100
                        "helper" "clone"
                        "revert" "invalid"
                        "start"  (lesson-draggable-start-evt-handler-maker
                                   moduleCode lessonType lessonGroup
                                   bgColorCssClass)
                        "stop"   (lesson-draggable-stop-evt-handler-maker
                                   moduleCode lessonType lessonGroup
                                   bgColorCssClass)))))

(defn- make-fake-lessons-droppable
  "For 'fake' lessons created due to a lesson <div> being dragged, we make
   them droppable in order for the user to switch lesson groups."
  [augModuleSelectedLessonInfoSeq]
  (doseq [augModuleSelectedLessonInfo augModuleSelectedLessonInfoSeq]
    (let [$divElem (:divElem augModuleSelectedLessonInfo)]
      (.droppable
        $divElem
        (js-obj "tolerance" "intersect"

                "over"
                (fn [evt ui]
                  (.removeClass $divElem "lesson-droppable-not-hover")
                  (.addClass $divElem "lesson-droppable-hover"))

                "out"
                (fn [evt ui]
                  (.removeClass $divElem "lesson-droppable-hover")
                  (.addClass $divElem "lesson-droppable-not-hover"))

                "drop"
                (fn [evt ui]
                  (let [$helper (aget ui "helper")]
                    ; Set a data attribute on the draggable helper to signal
                    ; that the user has dropped the <div> on a droppable to
                    ; change the lesson group
                    (data $helper
                          LESSON-GROUP-CHANGE-KEY
                          (:lessonGroup augModuleSelectedLessonInfo)))))))))

(defn- add-module-lesson-group!
  "Adds a lesson group of a module to the timetable.

   The `isActuallySelected?` param is used to indicate if the current lesson
   group is selected by the user.
   A value of `true` indicates that the user actually selected the lesson group.
   A value of `false` indicates that the current lesson group is added by the
   `lesson-draggable-start-evt-handler-maker` function and is not selected by
   the user.

   Returns a sequence of `ModulesSelectedLessonInfo` objects each augmented
   with the `:divElem`, `:moduleCode`, `:lessonType`, `:lessonGroup` keys."
  [moduleCode lessonType lessonGroup bgColorCssClass isActuallySelected?
   & {:keys [addedViaUrlHashInit?] :or {addedViaUrlHashInit? false}}]
  (if (module-has-lesson-group? moduleCode lessonType lessonGroup)
      (let [moduleName          (get-module-name-from-module-code moduleCode)
            modulesMapLessonSeq (get-ModulesMapLesson-seq moduleCode lessonType
                                                          lessonGroup)

            augLessonInfoSeq
            (doall
              (map (fn [modulesMapLesson]
                     (add-module-lesson!
                       :moduleCode moduleCode, :moduleName moduleName,
                       :lessonType lessonType, :lessonGroup lessonGroup,
                       :day        (:day modulesMapLesson),
                       :startTime  (:startTime modulesMapLesson),
                       :endTime    (:endTime modulesMapLesson),
                       :venue      (:venue modulesMapLesson),
                       :bgColorCssClass bgColorCssClass,
                       :isActuallySelected? isActuallySelected?))
                   modulesMapLessonSeq))

            lessonInfoSeq       (map #(dissoc %1 :divElem :moduleCode
                                              :lessonType :lessonGroup)
                                     augLessonInfoSeq)
            $divElemSeq         (map #(:divElem %1) augLessonInfoSeq)]
        (if isActuallySelected?
            (do
              (update-ModulesSelected-with-lesson-group! moduleCode lessonType
                                                         lessonGroup
                                                         lessonInfoSeq)
              (if (module-not-in-ModulesSelectedOrder? moduleCode)
                  (do
                    (add-to-ModulesSelectedOrder! moduleCode)
                    ; Micro optimization for modules added via url hash
                    ; during initialization
                    (if (not addedViaUrlHashInit?)
                        (select2-box-update-modules))))

              ; Only lesson types with more than 1 option of lesson group
              ; will be draggable
              (if (module-lesson-type-has-multiple-choices? moduleCode
                                                            lessonType)
                  (make-added-lessons-draggable $divElemSeq moduleCode
                                                lessonType lessonGroup
                                                bgColorCssClass)))

            ; lesson was added due to draggable <div>
            ; Make it droppable
            (make-fake-lessons-droppable augLessonInfoSeq))
        augLessonInfoSeq)))

(defn add-module!
  "Adds a module to the timetable.

   A random lesson group of each type of lesson (Lecture, Tutorial, etc) will
   be added."
  [moduleCode]
  (if (not (is-module-selected? moduleCode))
      (let [lessonTypes     (get-all-lesson-types-for-module moduleCode)
            bgColorCssClass (get-next-lesson-bg-color-css-class)

            newModInfoSeq
            (reduce (fn [moduleInfoSeq lessonType]
                      (conj moduleInfoSeq
                            {:moduleCode  moduleCode,
                             :lessonType  lessonType,
                             :lessonGroup
                             (get-first-lesson-group-for-module-lesson-type
                               moduleCode lessonType)}))
                    []
                    lessonTypes)]

        (doseq [moduleInfo newModInfoSeq]
          (add-module-lesson-group! (:moduleCode moduleInfo)
                                    (:lessonType moduleInfo)
                                    (:lessonGroup moduleInfo)
                                    bgColorCssClass
                                    true))

        ; Update URL hash with newly added module
        (update-document-location-hash-with-new-module!
          {:moduleCode moduleCode,

           :preToUrlHashLessonGroupSeqSorted
           (sort-PreToUrlHashLessonGroup-seq
             (map #(dissoc
                     (assoc %1 :lessonType
                            (Lesson-Type-Long-To-Short-Form (:lessonType %1)))
                     :moduleCode)
                  newModInfoSeq))}))))

(defn- get-module-info-from-url-hash-module-info
  "Computes the final module info sequence for modules added via the url hash
   on page initialization.

   NOTE: This function should only be called by
         `add-module-lesson-groups-from-url-hash!`"
  [moduleInfoSeq]
  (let [moduleLessonGroupsMap
        (reduce (fn [lgMap modInfo]
                  ; Replaces earlier lesson groups with later ones if we
                  ; encounter a duplicate lesson type
                  (assoc-in lgMap [(:moduleCode modInfo) (:lessonType modInfo)]
                            (:lessonGroup modInfo)))
                {}
                moduleInfoSeq)

        moduleCodesSeq (distinct (map #(:moduleCode %1) moduleInfoSeq))

        ; Map of Module Code -> Sequence of lesson type strings
        moduleLessonTypes
        (reduce (fn [lessonTypesMap moduleCode]
                  (let [ltMap (get moduleLessonGroupsMap moduleCode)]
                    (assoc lessonTypesMap moduleCode (keys ltMap))))
                {}
                moduleCodesSeq)

        ; Similar to `moduleLessonTypes`, but contains the full sequence of
        ; lesson type strings
        moduleLessonTypesFull
        (reduce (fn [lessonTypesMap moduleCode]
                  (assoc lessonTypesMap moduleCode
                         (get-all-lesson-types-for-module moduleCode)))
                {}
                moduleCodesSeq)

        ; Obtained from the difference between `moduleLessonTypesFull` and
        ; `moduleLessonTypes`
        missingModuleLessonTypes
        (reduce (fn [missingLessonTypesMap moduleCode]
                  (let [missingLessonTypes
                        (clojure.set/difference
                          (set (get moduleLessonTypesFull moduleCode))
                          (set (get moduleLessonTypes moduleCode)))]
                    (if (empty? missingLessonTypes)
                        missingLessonTypesMap
                        (assoc missingLessonTypesMap moduleCode
                               (seq missingLessonTypes)))))
                {}
                moduleCodesSeq)

        ; similar to `moduleLessonGroupsMap`, but includes missing lesson types
        moduleLessonGroupsMapFinal
        (reduce (fn [lgMapFinal [moduleCode missingLessonTypes]]
                  (reduce
                    (fn [lgMap lessonType]
                      (assoc-in lgMap [moduleCode lessonType]
                                (get-first-lesson-group-for-module-lesson-type
                                  moduleCode lessonType)))
                    lgMapFinal
                    missingLessonTypes))
                moduleLessonGroupsMap
                missingModuleLessonTypes)]

    ; Produce the final module info sequence
    (flatten
      (map (fn [moduleCode]
             (map (fn [[lessonType lessonGroup]]
                    {:moduleCode  moduleCode
                     :lessonType  lessonType
                     :lessonGroup lessonGroup})
                  (get moduleLessonGroupsMapFinal moduleCode)))
           moduleCodesSeq))))

(defn add-module-lesson-groups-from-url-hash!
  "Adds the module lesson groups available in the url hash. Erroneous lesson
   groups are ignored. If there are missing lesson groups for any module after
   going through the url hash, a random lesson group is chosen.

   NOTE: This function should only be called once."
  [urlHash]
  (let [moduleUrlHashArray                (.split urlHash "&")

        modUrlHashRegex
        #"^([A-Z]+\d{4}[A-Z]*)_(DL|L|LAB|PL|PT|R|SEM|ST|T|T2|T3)=([A-Z0-9]+)$"

        ; Sequence of non-nil match arrays
        matchArraySeq
        (filter (fn [matchArray] (not (nil? matchArray)))
                (map (fn [modUrlHash] (.exec modUrlHashRegex modUrlHash))
                     moduleUrlHashArray))

        ; Sequence of existing lesson groups
        moduleInfoExistent
        (filter
          #(module-has-lesson-group? (:moduleCode %1) (:lessonType %1)
                                     (:lessonGroup %1))
          ; convert matchArrays to maps
          (map (fn [matchArray]
                 {:moduleCode  (nth matchArray 1)
                  :lessonType  (Lesson-Type-Short-To-Long-Form
                                 (nth matchArray 2))
                  :lessonGroup (nth matchArray 3)})
               matchArraySeq))

        ; Module code to CSS class for lesson div background color
        moduleToColorsMap
        (reduce
          (fn [m2cMap moduleCode]
            (assoc m2cMap moduleCode (get-next-lesson-bg-color-css-class)))
          {}
          (distinct (map (fn [modInfo] (:moduleCode modInfo))
                         moduleInfoExistent)))

        moduleInfoFinal
        (get-module-info-from-url-hash-module-info moduleInfoExistent)]

    (.log js/console (str "moduleInfoExistent = " (.stringify js/JSON (clj->js moduleInfoExistent))))
    (.log js/console (str "moduleInfoFinal = " (.stringify js/JSON (clj->js moduleInfoFinal))))
    ; Add the module lesson groups
    (doseq [modInfo moduleInfoFinal]
      (let [moduleCode      (:moduleCode modInfo)
            bgColorCssClass (get moduleToColorsMap moduleCode)]
        (add-module-lesson-group! moduleCode
                                  (:lessonType modInfo)
                                  (:lessonGroup modInfo)
                                  bgColorCssClass
                                  true
                                  :addedViaUrlHashInit? true)))

    ; Update Select2 box, since this is not done in `add-module-lesson-group!`.
    ; Repeated work will be done there if we did so
    (select2-box-update-modules)
    ; Update url hash
    (set-document-location-hash-based-on-modules-order!)))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Functions for Removing Modules
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- add-missing-td-elements-replacing-lesson
  "Adds <td> elements that were removed by the `add-module-lesson!` function
   to make way for the lesson."
  [$parentTd startTime endTime]
  (loop [$currentTd $parentTd
         timeIdx    (inc startTime)]
    (if (>= timeIdx endTime)
        nil
        (let [[hourClass minuteClass]
              (get-css-hour-minute-classes-for-time timeIdx)

              $newTdElem
              ($ "<td />" (js-obj "class" (str hourClass " " minuteClass)))]
          ; Insert after new <td> after the current <td>
          (insert-after $newTdElem $currentTd)
          ; Then use the new <td> as the current <td> in the next iteration
          (recur $newTdElem (inc timeIdx))))))

(defn- remove-lesson-group-html
  "Removes a lesson group for a module from the HTML timetable.
   This does NOT remove any empty rows / perform module shifting.

   Returns a sequence of `ttLessonInfo` objects which contain
   additional `:day` and `:rowNum` keys."
  [moduleCode lessonType]
  (let [lessonGroupDetails (get-in ModulesSelected [moduleCode lessonType])
        lessonGroup        (:label lessonGroupDetails)
        lessonInfoSeq      (:info lessonGroupDetails)]
    (map (fn [modSelLessonInfo]
           (let [day          (:day modSelLessonInfo)
                 rowNum       (:rowNum modSelLessonInfo)
                 startTime    (:startTime modSelLessonInfo)
                 endTime      (:endTime modSelLessonInfo)
                 ttLessonInfo {:moduleCode moduleCode, :lessonType lessonType,
                               :lessonGroup lessonGroup, :startTime startTime,
                               :endTime endTime}
                 $lessonDiv   (timetable-get-lesson-div day rowNum
                                                        ttLessonInfo)]
             ($lesson-div-remove $lessonDiv startTime endTime)
             (assoc ttLessonInfo :day day, :rowNum rowNum)))
         lessonInfoSeq)))

(defn- get-largest-consecutive-time-slot-freed-up-by-lesson
  "Given a removed lesson, find the largest interval on the same row which is
   freed up due to the its removal.

   For instance, given this timetable row:

   x | x | x |  |  |  | y | y | y | y |  |  | z | z

   And we remove the lesson occupying the slots labelled 'y'. The 4 slots
   currently occupied by 'y', along with the 3 empty slots to its left and the
   2 empty slots to its right will be freed up.

   Given that the first slot occupied by the label 'x' is the index 0, this
   function will return the vector [3 11].

   The parameter `removedAugTTLessonInfo` is a `TimetableLessonInfo` object
   augmented with the `:day` and `:rowNum` keys."
  [removedAugTTLessonInfo]
  (let [day        (:day removedAugTTLessonInfo)
        rowNum     (:rowNum removedAugTTLessonInfo)
        startTime  (:startTime removedAugTTLessonInfo)
        endTime    (:endTime removedAugTTLessonInfo)
        minTimeIdx time-helper/TIME-INDEX-MIN
        maxTimeIdx time-helper/TIME-INDEX-MAX]

    (.log js/console "get-largest-consecutive-time-slot-freed-up-by-lesson")
    [
      ; Find the lesson right before the removed lesson
      (let [immediateTTLessonInfoBefore
            (timetable-get-closest-TimetableLessonInfo-before-time
              day rowNum startTime)]
        (if (nil? immediateTTLessonInfoBefore)
            minTimeIdx
            (:endTime immediateTTLessonInfoBefore)))

      ; Find the lesson right after the removed lesson
      (let [immediateTTLessonInfoAfter
            (timetable-get-closest-TimetableLessonInfo-on-or-after-time
              day rowNum endTime)]
        (if (nil? immediateTTLessonInfoAfter)
            maxTimeIdx
            (dec (:startTime immediateTTLessonInfoAfter))))
    ]))

(defn- find-replacement-lessons-within-time
  "For a removed lesson, find other lessons on the same day but in a row below
   the removed lesson which could be shifted upwards and take the timeslots
   freed up by the removed lesson.

   The `removedAugTTLessonInfo` parameter is an `TimetableLessonInfo` object
   augmented with the `:day` and `:rowNum` keys.

   Returns a map, where keys are `TimetableLessonInfo` objects augmented with
   `:day` and `:rowNum` keys, and values are jQuery <div> objects for the
   shifted lesson."
  [{:keys [day rowNum startTime endTime] :as removedAugTTLessonInfo}]
  (let [; This is the time range freed up by removal of the lesson
        [lowTimeIdx highTimeIdx]
        (get-largest-consecutive-time-slot-freed-up-by-lesson
          removedAugTTLessonInfo)

        not-occupied?
        (fn [occupiedVec startTime endTime]
          (.log js/console (str "inside not-occupied?, occupiedVec = " (.stringify js/JSON (clj->js occupiedVec))))
          (every? #(nil? %1)
                  (map #(nth occupiedVec %1)
                       (range startTime endTime))))]

    (.log js/console "find-replacement-lessons-within-time")
    (.log js/console (str "lowTimeIdx = " lowTimeIdx ", highTimeIdx = " highTimeIdx))
    (:augTTLessonInfoMapToShift
      (reduce
        (fn [{:keys [augTTLessonInfoMapToShift occupiedVec]
              :as   reduceResult}
             rowIdx]
          (if (timetable-row-empty? day rowIdx)
              ; Skip empty row
              reduceResult

              ; Non-empty row
              (let [ttLessonInfoToShift
                    (timetable-row-get-lessons-satisfying-pred
                      day rowIdx
                      (fn [{:keys [startTime endTime]}]
                        (and (>= startTime lowTimeIdx)
                             (<= (dec endTime) highTimeIdx)
                             (not-occupied? occupiedVec startTime endTime))))]
                (.log js/console "suspect above")
                (.log js/console (str "rowIdx = " rowIdx))
                (.log js/console (str "occupiedVec = " (.stringify js/JSON (clj->js occupiedVec))))
                {:augTTLessonInfoMapToShift
                 (reduce (fn [augTTLessonInfoMap [ttLessonInfo $divElem]]
                           (.log js/console ":augTTLessonInfoMapToShift")
                           (assoc augTTLessonInfoMap
                                  (assoc ttLessonInfo :day day :rowNum rowIdx)
                                  $divElem))
                         augTTLessonInfoMapToShift
                         ttLessonInfoToShift)

                 :occupiedVec
                 (reduce (fn [occVec [{:keys [startTime endTime]}  _]]
                           (.log js/console ":occupiedVec")
                           (reduce (fn [ov timeIdx] (assoc ov timeIdx true))
                                   occVec
                                   (range startTime endTime)))
                         occupiedVec
                         ttLessonInfoToShift)})))
        {:augTTLessonInfoMapToShift {},
         ; nil = free slot, true = occupied slot
         :occupiedVec      (vec (map (fn [t] nil)
                                     (range time-helper/TIME-INDEX-MIN
                                            (inc time-helper/TIME-INDEX-MAX))))}
        ; go through each row in the day, below the current row
        (drop (inc rowNum) (range (timetable-day-get-nr-rows day)))))))

(defn- shift-lesson-to-row!
  "Shifts a lesson from its original row up to the given row.

   The parameter `augTTLessonInfo` is a `TimetableLessonInfo` object augmented
   with the `:day` and `:rowNum` keys."
  [destinationRowNum {:keys [day rowNum startTime endTime] :as augTTLessonInfo}
   $divElem]
  (let [slotsOccupied (- endTime startTime)
        $sourceTd     (html-timetable-get-td day rowNum startTime)
        $destTd       (html-timetable-get-td day destinationRowNum startTime)
        ttLessonInfo  (dissoc augTTLessonInfo :day :rowNum)]
    (.log js/console (str "day = " day ", sourceRowNum = " rowNum ", destinationRowNum = " destinationRowNum))
    ; Transfer lesson <div> to the destination <td>
    (.append $destTd $divElem)
    ; restore the colspan of the source <td>
    (remove-attr $sourceTd "colspan")
    ; fill in removed <td> for the source <td>
    (add-missing-td-elements-replacing-lesson $sourceTd startTime endTime)
    ; remove sibling <td> for the destination <td> to make way for columns
    ; occupied by $divElem
    (doseq [$siblingTd (take (dec slotsOccupied) (.nextAll $destTd))]
      (.remove $siblingTd))
    ; update colspan of destination <td>
    (attr $destTd "colspan" slotsOccupied)
    ; update `Timetable` global
    (timetable-remove-lesson! day rowNum ttLessonInfo)
    (timetable-add-lesson! day destinationRowNum ttLessonInfo $divElem)))

(defn- shift-lessons-upwards-to-replace-empty-slots!
  "For each removed lesson, see if there are any lessons in rows below it that
   can be shifted upwards to occupy the empty slots resulting from its removal.

   The parameter `removedAugmentedTTLessonInfoSeq` is a sequence of
   `TimetableLessonInfo` objects augmented with the `:day` and `:rowNum` keys."
  [removedAugmentedTTLessonInfoSeq]
  (let [; we do not consider any lessons on empty rows for ease of
        ; implementation
        exclude-lessons-on-empty-rows
        (fn [lessonInfoSeq]
          (filter (fn [{:keys [day rowNum]}]
                    (timetable-row-not-empty? day rowNum))
                  lessonInfoSeq))

        sort-lesson-info-seq
        (fn [lessonInfoSeq]
          (sort #(let [dayA (:day %1)
                       dayB (:day %2)]
                   (if (= dayA dayB)
                       (< (:rowNum %1) (:rowNum %2))
                       (< dayA dayB)))
                lessonInfoSeq))]

    (.log js/console "Hoo hoo")
    (loop [sortedAugTTLessonInfoSeq
           (sort-lesson-info-seq (exclude-lessons-on-empty-rows
                                   removedAugmentedTTLessonInfoSeq))

           nextLessonInfoVec    []]
      (.log js/console "Inside the loop, the loop")
      (cond (and (empty? sortedAugTTLessonInfoSeq) (empty? nextLessonInfoVec))
            nil

            ; we're done for the current set of lessons. Move on to the
            ; set of lessons that were shifted upwards.
            (empty? sortedAugTTLessonInfoSeq)
            (recur (sort-lesson-info-seq (exclude-lessons-on-empty-rows
                                           nextLessonInfoVec))
                   [])

            ; find timeslot to shift up to replace gap
            :else
            (let [currentAugTTLessonInfo (first sortedAugTTLessonInfoSeq)
                  rowNum                 (:rowNum currentAugTTLessonInfo)

                  ; map of augmented `TimeTableLessonInfo` objects to the
                  ; jQuery <div> element
                  augTTLessonInfoTo$DivElem
                  (find-replacement-lessons-within-time currentAugTTLessonInfo)]

              (doseq [[augTTLessonInfo $divElem] augTTLessonInfoTo$DivElem]
                (.log js/console "shift-lesson-to-row")
                (shift-lesson-to-row! rowNum augTTLessonInfo $divElem))
              (.log js/console "gonna recur...")
              (recur (rest sortedAugTTLessonInfoSeq)
                     ; append newly shifted lessons to nextLessonInfoVec
                     (reduce (fn [augTTLessonInfoVec [augTTLessonInfo _]]
                               (conj augTTLessonInfoVec augTTLessonInfo))
                             nextLessonInfoVec
                             augTTLessonInfoTo$DivElem)))))))

(defn- overall-timetable-prune-empty-rows
  "Removes empty rows resulting from the removal of lessons on given days."
  [affectedDaysSet]
  (doseq [day affectedDaysSet]
    (let [rowsPartition (timetable-day-get-empty-and-non-empty-rows day)]
      (.log js/console (str "day = " day ", emptyRows = "
                            (.stringify js/JSON (clj->js (:emptyRows rowsPartition)))
                            ", nonEmptyRows = "
                            (.stringify js/JSON (clj->js (:nonEmptyRows rowsPartition)))))

      (timetable-prune-empty-rows-for-day! day rowsPartition)
      (html-timetable-prune-empty-rows-for-day day rowsPartition))))

(defn remove-module!
  "Removes a module from the timetable.

