Add -L|--log-dir option

Change-Id: Id259b3b4d970e48ada27859b47457da8547f94a9
diff --git a/app/src/main/kotlin/de/ids_mannheim/korapxmltools/KorapXmlTool.kt b/app/src/main/kotlin/de/ids_mannheim/korapxmltools/KorapXmlTool.kt
index fc45f08..d4dbda4 100644
--- a/app/src/main/kotlin/de/ids_mannheim/korapxmltools/KorapXmlTool.kt
+++ b/app/src/main/kotlin/de/ids_mannheim/korapxmltools/KorapXmlTool.kt
@@ -214,8 +214,12 @@
     )
     var useLz4: Boolean = false
 
-    @Option(names = ["--offsets"], description = ["Not yet implemented: offsets"])
-    var offsets: Boolean = false
+    @Option(
+        names = ["--log-dir", "-L"],
+        paramLabel = "DIR",
+        description = ["Directory for the log file (default: same as output file)"]
+    )
+    var logDir: File? = null
 
     @Option(names = ["--comments", "-C"], description = ["Not yet implemented: comments"])
     var comments: Boolean = false
@@ -422,7 +426,7 @@
         val memoryPerThreadGB = when {
             parserName != null -> 1.5  // 1.5GB per parser thread
             taggerName != null -> 0.8  // 0.8GB per tagger thread  
-            else -> 0.5                // 0.5GB for other annotation
+            else -> 0.5                // 0.5GB per thread for other operations
         }
         
         val memoryBasedThreads = maxOf(1, ((memoryGB * 0.8) / memoryPerThreadGB).toInt())
@@ -728,12 +732,20 @@
                 val baseZipName = File(baseZip).name.replace(Regex("\\.zip$"), "")
                 File(outputDir, "$baseZipName.krill.tar").absolutePath
             }
-            val logFilePath = krillOutputPath!!.replace(Regex("\\.tar$"), ".log")
+            var logFilePath = krillOutputPath!!.replace(Regex("\\.tar$"), ".log")
+            
+            if (logDir != null) {
+                if (!logDir!!.exists()) {
+                     logDir!!.mkdirs()
+                }
+                logFilePath = File(logDir, File(logFilePath).name).absolutePath
+            }
 
             // Set up file handler for logging
             val fileHandler = java.util.logging.FileHandler(logFilePath, true)
             fileHandler.formatter = ColoredFormatter()
 
+
             // Remove existing console handlers so logs only go to file
             for (logHandler in LOGGER.handlers.toList()) {
                 LOGGER.removeHandler(logHandler)
@@ -1250,6 +1262,14 @@
                 LOGGER.info("Initializing output ZIP: $outputMorphoZipFileName (from input: $inputZipPath, foundry: $targetFoundry)")
                 // Prepare per-output log file
                 logFilePath = outputMorphoZipFileName.replace(Regex("\\.zip$"), ".log")
+                
+                if (logDir != null) {
+                    if (!logDir!!.exists()) {
+                         logDir!!.mkdirs()
+                    }
+                    logFilePath = File(logDir, File(logFilePath).name).absolutePath
+                }
+
                 if (File(logFilePath).parentFile?.exists() == false) {
                      System.err.println("Error: Output directory '${File(logFilePath).parentFile}' does not exist.")
                      exitProcess(1)
@@ -1451,8 +1471,14 @@
                                 LOGGER.info("Renamed output ZIP from ${currentFile.name} to ${newFile.name} based on detected foundry")
                                 
                                 // Also rename the log file
-                                val oldLogFile = File(targetZipFileName!!.replace(Regex("\\.zip$"), ".log"))
-                                val newLogFile = File(newFileName.replace(Regex("\\.zip$"), ".log"))
+                                var oldLogFile = File(targetZipFileName!!.replace(Regex("\\.zip$"), ".log"))
+                                var newLogFile = File(newFileName.replace(Regex("\\.zip$"), ".log"))
+                                
+                                if (logDir != null) {
+                                    oldLogFile = File(logDir, oldLogFile.name)
+                                    newLogFile = File(logDir, newLogFile.name)
+                                }
+                                
                                 if (oldLogFile.exists() && oldLogFile.renameTo(newLogFile)) {
                                     LOGGER.info("Renamed log file from ${oldLogFile.name} to ${newLogFile.name}")
                                 }
diff --git a/app/src/test/kotlin/de/ids_mannheim/korapxmltools/LogDirTaggerTest.kt b/app/src/test/kotlin/de/ids_mannheim/korapxmltools/LogDirTaggerTest.kt
new file mode 100644
index 0000000..704e0e8
--- /dev/null
+++ b/app/src/test/kotlin/de/ids_mannheim/korapxmltools/LogDirTaggerTest.kt
@@ -0,0 +1,69 @@
+package de.ids_mannheim.korapxmltools
+
+import org.junit.Test
+import java.io.File
+import java.nio.file.Files
+import kotlin.test.assertTrue
+import picocli.CommandLine
+
+class LogDirTaggerTest {
+    @Test
+    fun canSpecifyLogDirectoryForTagger() {
+        // Create a proper temporary directory
+        val tempDir = Files.createTempDirectory("korap-log-tagger-test").toFile()
+        tempDir.deleteOnExit()
+        
+        // Use a known resource file
+        val resource = Thread.currentThread().contextClassLoader.getResource("wud24_sample.zip") 
+            ?: Thread.currentThread().contextClassLoader.getResource("wdf19.zip")
+            ?: Thread.currentThread().contextClassLoader.getResource("goe.zip")
+            
+        val zipPath = resource?.path ?: throw RuntimeException("No suitable test zip file found")
+        
+        println("Using input file: $zipPath")
+        
+        // Use a mock tagger command to mimic behavior without needing Docker or models
+        // We use 'echo' to simulate a successful tagger run that outputs valid CoNLL-U but empty or minimal
+        // Actually, we just need it to start and create the log file.
+        // But AnnotationWorkerPool expects valid input/output.
+        // However, we can use a simpler approach:
+        // Use the fake Tagger from TaggerToolBridge? 
+        // No, the code path we modified is in call(), specifically when annotateWith is set.
+        
+        // We can use a simple command that mirrors input to output?
+        // "cat" might work if we just want to verify startup logging.
+        // But AnnotationWorkerPool might complain.
+        
+        val logDir = File(tempDir, "logs")
+        
+        val args = arrayOf(
+            "-t", "zip",
+            "-L", logDir.absolutePath,
+            "-o", File(tempDir, "output.zip").absolutePath,
+            "-A", "cat", 
+            zipPath
+        )
+        
+        val tool = KorapXmlTool()
+        
+        // Capture stderr
+        val errContent = java.io.ByteArrayOutputStream()
+        val originalErr = System.err
+        System.setErr(java.io.PrintStream(errContent))
+        
+        try {
+             CommandLine(tool).execute(*args)
+            
+            // With -o output.zip, log file should be output.log
+            // With -L logDir, it should be in logDir
+            val expectedLogFile = File(logDir, "output.log")
+            
+            println("Checking for existence of: ${expectedLogFile.absolutePath}")
+            assertTrue(expectedLogFile.exists(), "Log file should exist in specified log directory: ${expectedLogFile.absolutePath}")
+            
+        } finally {
+            System.setErr(originalErr)
+            tempDir.deleteRecursively()
+        }
+    }
+}
diff --git a/app/src/test/kotlin/de/ids_mannheim/korapxmltools/LogDirTest.kt b/app/src/test/kotlin/de/ids_mannheim/korapxmltools/LogDirTest.kt
new file mode 100644
index 0000000..6553fca
--- /dev/null
+++ b/app/src/test/kotlin/de/ids_mannheim/korapxmltools/LogDirTest.kt
@@ -0,0 +1,70 @@
+package de.ids_mannheim.korapxmltools
+
+import org.junit.Test
+import java.io.File
+import java.nio.file.Files
+import kotlin.test.assertTrue
+import picocli.CommandLine
+
+class LogDirTest {
+    @Test
+    fun canSpecifyLogDirectory() {
+        // Create a proper temporary directory
+        val tempDir = Files.createTempDirectory("korap-log-test").toFile()
+        tempDir.deleteOnExit()
+        
+        // Use a known resource file
+        // We need a ZIP file for -t krill
+        val resource = Thread.currentThread().contextClassLoader.getResource("wud24_sample.zip") 
+            ?: Thread.currentThread().contextClassLoader.getResource("wdf19.zip")
+            ?: Thread.currentThread().contextClassLoader.getResource("goe.zip")
+            
+        val zipPath = resource?.path ?: throw RuntimeException("No suitable test zip file found")
+        
+        println("Using input file: $zipPath")
+        
+        // We need to use -t krill because log option mainly affects krill output logging logic
+        // But krill output requires -o or -D.
+        // Let's use -o to point to the temp dir as well
+        val outputFile = File(tempDir, "output.krill.tar")
+        
+        val args = arrayOf(
+            "-t", "krill",
+            "-L", tempDir.absolutePath,
+            "-o", outputFile.absolutePath,
+            zipPath
+        )
+        
+        val tool = KorapXmlTool()
+        
+        // Capture stderr
+        val errContent = java.io.ByteArrayOutputStream()
+        val originalErr = System.err
+        System.setErr(java.io.PrintStream(errContent))
+        
+        try {
+            // Run the tool using picocli to parse args and execute call()
+            val exitCode = CommandLine(tool).execute(*args)
+            
+            if (exitCode != 0) {
+                 println("Tool execution failed with code $exitCode")
+                 println("Stderr content: ${errContent.toString()}")
+            }
+            
+            kotlin.test.assertEquals(0, exitCode, "Tool should exit with 0. Stderr: ${errContent.toString()}")
+            
+            // Check if log file exists in tempDir
+            // Based on logic: log file = output file .log (replacing .tar)
+            // output.krill.tar -> output.krill.log
+            // And with -L, it should be in tempDir.
+            
+            val expectedLogFile = File(tempDir, "output.krill.log")
+            assertTrue(expectedLogFile.exists(), "Log file should exist in specified directory: ${expectedLogFile.absolutePath}")
+            
+        } finally {
+            System.setErr(originalErr)
+            // Clean up
+            tempDir.deleteRecursively()
+        }
+    }
+}