Move SSD box label helpers to typed Kotlin

ober

aff26ccd33f7b0b16922ca918dd80e04d9415303

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index ede2d89..1740de2 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -2786,6 +2786,51 @@
                     "."
                     (string-append (int->string tenths) " s")))))))))
 
+    (typed-kotlin-file "com/sfb/ssdreview/BoxTypeLabelAndroid.kt"
+      (kotlin-imports (android content Context))
+      (typed-library (com sfb ssdreview)
+        (export defaultLabelForBoxTypeAndroid
+                resolvedLabelForBoxTypeAndroid
+                shouldReplaceLabelForBoxTypeAndroid)
+        (type Context)
+        (extern (boxTypesResolve (context : Context) (raw : String)) : (Nullable BoxType)
+          (kotlin-call BoxTypes resolve))
+        (def (nullableBoxTypeName (boxType : (Nullable BoxType))) : (Nullable String)
+          (if (nullable-null? boxType)
+              (nullable-none String)
+              (nullable-some (BoxType-name (nullable-get boxType)))))
+        (def (nullableBoxTypeDisplay (boxType : (Nullable BoxType))) : (Nullable String)
+          (if (nullable-null? boxType)
+              (nullable-none String)
+              (nullable-some (box-type-display (nullable-get boxType)))))
+        (def (boxTypeLabelMatchesAndroid (context : Context)
+                                         (label : String)
+                                         (boxType : String)) : Bool
+          (let ((resolved (boxTypesResolve context boxType)))
+            (boxTypeLabelMatches
+              label
+              boxType
+              (nullableBoxTypeName resolved)
+              (nullableBoxTypeDisplay resolved))))
+        (def (defaultLabelForBoxTypeAndroid (context : Context) (boxType : String)) : String
+          (defaultBoxTypeLabel
+            boxType
+            (nullableBoxTypeName (boxTypesResolve context boxType))))
+        (def (resolvedLabelForBoxTypeAndroid (context : Context)
+                                             (label : String)
+                                             (boxType : String)
+                                             (previousBoxType : String)) : String
+          (resolvedBoxTypeLabel
+            label
+            (defaultLabelForBoxTypeAndroid context boxType)
+            (boxTypeLabelMatchesAndroid context (string-trim label) previousBoxType)))
+        (def (shouldReplaceLabelForBoxTypeAndroid (context : Context)
+                                                  (label : String)
+                                                  (previousBoxType : String)) : Bool
+          (shouldReplaceBoxTypeLabel
+            label
+            (boxTypeLabelMatchesAndroid context (string-trim label) previousBoxType)))))
+
     (typed-kotlin-file "com/sfb/ssdreview/Guessing.kt"
       (kotlin-imports (android content Context))
       (typed-library (com sfb ssdreview)
@@ -3324,37 +3369,17 @@
        "        update()"
        "    }"
        ""
-       "    private fun resolvedLabelForBoxType(label: String, boxType: String, previousBoxType: String = \"\"): String {"
-       "        val trimmed = label.trim()"
-       "        return resolvedBoxTypeLabel(label, defaultLabelForBoxType(boxType), labelMatchesBoxType(trimmed, previousBoxType))"
-       "    }"
-       ""
        "    private fun replaceAutoLabelForBoxType(label: EditText, boxType: String, previousBoxType: String) {"
-       "        if (!shouldReplaceLabelForBoxType(label.text.toString(), previousBoxType)) return"
-       "        val replacement = defaultLabelForBoxType(boxType)"
+       "        if (!shouldReplaceLabelForBoxTypeAndroid(this, label.text.toString(), previousBoxType)) return"
+       "        val replacement = defaultLabelForBoxTypeAndroid(this, boxType)"
        "        if (replacement.isBlank()) return"
        "        label.setText(replacement)"
        "        label.setSelection(label.text.length)"
        "    }"
        ""
-       "    private fun shouldReplaceLabelForBoxType(label: String, previousBoxType: String): Boolean {"
-       "        val trimmed = label.trim()"
-       "        return shouldReplaceBoxTypeLabel(label, labelMatchesBoxType(trimmed, previousBoxType))"
-       "    }"
-       ""
-       "    private fun labelMatchesBoxType(label: String, boxType: String): Boolean {"
-       "        val previous = BoxTypes.resolve(this, boxType)"
-       "        return boxTypeLabelMatches(label, boxType, previous?.name, previous?.let { box_type_display(it) })"
-       "    }"
-       ""
-       "    private fun defaultLabelForBoxType(boxType: String): String {"
-       "        val resolved = BoxTypes.resolve(this, boxType)"
-       "        return defaultBoxTypeLabel(boxType, resolved?.name)"
-       "    }"
-       ""
        "    private fun finalizeGroupLabel(group: SsdGroup) {"
        "        val typeDisplay = BoxTypes.load(this).firstOrNull { it.id == group.boxTypeId }?.let { box_type_display(it) } ?: group.boxTypeId"
-       "        val resolved = resolvedLabelForBoxType(group.label, typeDisplay, group.boxTypeId)"
+       "        val resolved = resolvedLabelForBoxTypeAndroid(this, group.label, typeDisplay, group.boxTypeId)"
        "        if (resolved.isNotBlank()) {"
        "            group.label = resolved"
        "        }"
@@ -3899,7 +3924,7 @@
        "            .show()"
        "        dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {"
        "            val boxTypeText = boxType.text.toString()"
-       "            val labelText = resolvedLabelForBoxType(label.text.toString(), boxTypeText)"
+       "            val labelText = resolvedLabelForBoxTypeAndroid(this, label.text.toString(), boxTypeText, \"\")"
        "            val firingArcText = firingArc.text.toString()"
        "            val countValue = count.text.toString().toIntOrNull() ?: 1"
        "            boxType.dismissDropDown()"
@@ -3982,7 +4007,7 @@
        "    ) {"
        "        val session = currentSession ?: return"
        "        val resolvedType = BoxTypes.resolve(this, boxType)"
-       "        val cleanLabel = resolvedLabelForBoxType(label, boxType).ifBlank { resolvedType?.name?.lowercase() ?: \"\" }"
+       "        val cleanLabel = resolvedLabelForBoxTypeAndroid(this, label, boxType, \"\").ifBlank { resolvedType?.name?.lowercase() ?: \"\" }"
        "        val ids = if (detectedIds.isNotEmpty()) {"
        "            detectedIds.toMutableList()"
        "        } else {"
@@ -4055,7 +4080,7 @@
        "            inputType = InputType.TYPE_CLASS_TEXT"
        "            val existing = BoxTypes.load(this@MainActivity).firstOrNull { it.id == group.boxTypeId }"
        "            val typeDisplay = existing?.let { box_type_display(it) } ?: group.boxTypeId"
-       "            setText(resolvedLabelForBoxType(group.label, typeDisplay, group.boxTypeId))"
+       "            setText(resolvedLabelForBoxTypeAndroid(this@MainActivity, group.label, typeDisplay, group.boxTypeId))"
        "            selectAll()"
        "        }"
        "        val boxType = boxTypeInput().apply {"
@@ -4085,7 +4110,7 @@
        "            .show()"
        "        dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {"
        "            val boxTypeText = boxType.text.toString()"
-       "            val labelText = resolvedLabelForBoxType(label.text.toString(), boxTypeText, group.boxTypeId)"
+       "            val labelText = resolvedLabelForBoxTypeAndroid(this, label.text.toString(), boxTypeText, group.boxTypeId)"
        "            val firingArcText = firingArc.text.toString()"
        "            val countValue = count.text.toString().toIntOrNull()"
        "            boxType.dismissDropDown()"