Move SSD group selection to typed Kotlin

ober

4b84072a5c61d67b746d0d8c62c81c04f10eb243

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index e619e15..68687d7 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -461,6 +461,9 @@
                 SsdSession-ssdArea SsdSession-ssdArea-set!
                 ssdSessionCellInsideSsdArea ssdSessionGroupInsideSsdArea
                 ssdSessionSuppressedGroupPresent?
+                ssdSessionFindGroupById ssdSessionSelectedGroup
+                ssdSessionSelectedGroupId ssdSessionRelativeGroupId
+                ssdSessionCellIdsForGroupId
                 ssdSessionFindCellById ssdSessionCellsByIds
                 ssdSessionBBoxForCellIds ssdSessionRecomputeGroup
                 appendUniqueCellIdsToGroup
@@ -655,6 +658,66 @@
                   (nullable-some cell)
                   found))
               found)))
+        (def (ssdSessionFindGroupById (session : SsdSession)
+                                      (id : (Nullable String))) : (Nullable SsdGroup)
+          (if (nullable-null? id)
+            (nullable-none SsdGroup)
+            (let ((wanted (nullable-get id)))
+              (for/fold ((found (nullable-none SsdGroup)))
+                        ((i (in-range (int32 0) (list-size (SsdSession-groups session)))))
+                (if (nullable-null? found)
+                  (let ((group (list-ref (SsdSession-groups session) i)))
+                    (if (equal? (SsdGroup-id group) wanted)
+                      (nullable-some group)
+                      found))
+                  found)))))
+        (def (ssdSessionFirstGroup (session : SsdSession)) : (Nullable SsdGroup)
+          (if (= (list-size (SsdSession-groups session)) (int32 0))
+            (nullable-none SsdGroup)
+            (nullable-some (list-ref (SsdSession-groups session) (int32 0)))))
+        (def (ssdSessionSelectedGroup (session : SsdSession)
+                                      (id : (Nullable String))) : (Nullable SsdGroup)
+          (let ((found (ssdSessionFindGroupById session id)))
+            (if (nullable-null? found)
+              (ssdSessionFirstGroup session)
+              found)))
+        (def (ssdSessionSelectedGroupId (session : SsdSession)
+                                        (id : (Nullable String))) : (Nullable String)
+          (let ((group (ssdSessionSelectedGroup session id)))
+            (if (nullable-null? group)
+              (nullable-none String)
+              (nullable-some (SsdGroup-id (nullable-get group))))))
+        (def (ssdSessionGroupIndexById (session : SsdSession)
+                                       (id : (Nullable String))) : Int32
+          (if (nullable-null? id)
+            (int32 -1)
+            (let ((wanted (nullable-get id)))
+              (for/fold ((found (int32 -1)))
+                        ((i (in-range (int32 0) (list-size (SsdSession-groups session)))))
+                (if (>= found (int32 0))
+                  found
+                  (if (equal? (SsdGroup-id (list-ref (SsdSession-groups session) i)) wanted)
+                    i
+                    found))))))
+        (def (ssdSessionRelativeGroupId (session : SsdSession)
+                                        (id : (Nullable String))
+                                        (delta : Int32)) : (Nullable String)
+          (let ((size (list-size (SsdSession-groups session))))
+            (if (= size (int32 0))
+              (nullable-none String)
+              (let ((index (ssdSessionGroupIndexById session id)))
+                (let ((current (if (< index (int32 0)) (int32 0) index)))
+                  (nullable-some
+                    (SsdGroup-id
+                      (list-ref
+                        (SsdSession-groups session)
+                        (mod (+ (+ current delta) size) size)))))))))
+        (def (ssdSessionCellIdsForGroupId (session : SsdSession)
+                                          (id : (Nullable String))) : (MutableList String)
+          (let ((group (ssdSessionFindGroupById session id)))
+            (if (nullable-null? group)
+              (mutable-list-empty String)
+              (SsdGroup-cellIds (nullable-get group)))))
         (def (ssdSessionCellsByIds (session : SsdSession)
                                    (ids : (List String))) : (MutableList SsdCell)
           (for/fold ((out (mutable-list-empty SsdCell)))
@@ -3770,7 +3833,7 @@
        ""
        "    private fun startAppendMode() {"
        "        val session = currentSession ?: return"
-       "        val group = session.groups.firstOrNull { it.id == selectedGroupId }"
+       "        val group = ssdSessionFindGroupById(session, selectedGroupId)"
        "        if (group == null) {"
        "            appendMode = false"
        "            reviewView.selectMode = false"
@@ -3835,8 +3898,7 @@
        "        ssdSessionStoreSsdArea(session, area)"
        "        val outsideCount = ssdSessionSsdAreaOutsideCount(session)"
        "        val removed = ssdSessionPruneGroupsOutsideSsdArea(session)"
-	       "        selectedGroupId = session.groups.firstOrNull { it.id == selectedGroupId }?.id"
-	       "            ?: session.groups.firstOrNull()?.id"
+	       "        selectedGroupId = ssdSessionSelectedGroupId(session, selectedGroupId)"
 	       "        reviewView.ssdArea = session.ssdArea"
 	       "        reviewView.selectedGroupId = selectedGroupId"
 	       "        truthStore.appendEvent(\"ssd_area_set\", session, JSONObject()"
@@ -3856,7 +3918,7 @@
 	       ""
 	       "    private fun selectGroup(groupId: String?) {"
        "        val session = currentSession ?: return"
-       "        val group = session.groups.firstOrNull { it.id == groupId } ?: return"
+       "        val group = ssdSessionFindGroupById(session, groupId) ?: return"
 	       "        selectedGroupId = group.id"
 	       "        reviewView.selectedGroupId = group.id"
 	       "        reviewView.centerOnGroup(group)"
@@ -3867,19 +3929,16 @@
        ""
        "    private fun selectedGroup(): SsdGroup? {"
        "        val session = currentSession ?: return null"
-       "        return session.groups.firstOrNull { it.id == selectedGroupId }"
-       "            ?: session.groups.firstOrNull()?.also {"
-       "                selectedGroupId = it.id"
-       "                reviewView.selectedGroupId = it.id"
-       "            }"
+       "        val group = ssdSessionSelectedGroup(session, selectedGroupId) ?: return null"
+       "        selectedGroupId = group.id"
+       "        reviewView.selectedGroupId = group.id"
+       "        return group"
        "    }"
        ""
        "    private fun selectRelativeGroup(delta: Int) {"
        "        val session = currentSession ?: return"
-       "        if (session.groups.isEmpty()) return"
-       "        val current = session.groups.indexOfFirst { it.id == selectedGroupId }.let { if (it < 0) 0 else it }"
-       "        val next = (current + delta + session.groups.size) % session.groups.size"
-       "        selectGroup(session.groups[next].id)"
+       "        val next = ssdSessionRelativeGroupId(session, selectedGroupId, delta) ?: return"
+       "        selectGroup(next)"
        "    }"
        ""
        "    private fun editSelectedGroup() {"
@@ -4173,7 +4232,7 @@
        ""
        "    private fun showAppendDialog(rect: FloatArray, detectedIds: List<String>) {"
        "        val session = currentSession ?: return"
-       "        val group = session.groups.firstOrNull { it.id == selectedGroupId }"
+       "        val group = ssdSessionFindGroupById(session, selectedGroupId)"
        "        if (group == null) {"
        "            appendMode = false"
        "            reviewView.selectMode = false"
@@ -4389,7 +4448,7 @@
        "        group.notes = \"Deleted on Android\""
        "        session.suppressedGroups.add(group)"
        "        truthStore.appendEvent(\"group_deleted\", session)"
-       "        selectedGroupId = session.groups.firstOrNull()?.id"
+	       "        selectedGroupId = ssdSessionSelectedGroupId(session, null)"
        "        reviewView.selectedGroupId = selectedGroupId"
        "        reviewView.performHapticFeedback(HapticFeedbackConstants.REJECT)"
        "        reviewView.invalidate()"
@@ -5483,7 +5542,7 @@
        ""
        "    private fun drawCells(canvas: Canvas) {"
        "        val sess = session ?: return"
-       "        val selectedIds = sess.groups.firstOrNull { it.id == selectedGroupId }?.cellIds?.toSet() ?: return"
+       "        val selectedIds = ssdSessionCellIdsForGroupId(sess, selectedGroupId).toSet()"
        "        paint.style = Paint.Style.STROKE"
        "        paint.strokeWidth = 2f / scale"
        "        for (cell in sess.cells) {"