From 02906dbcabb348d2e65cc8db9bdc238bd129bc8d Mon Sep 17 00:00:00 2001
From: Justin Brooks <justin@jzbrooks.com>
Date: Thu, 3 Oct 2024 22:22:36 -0400
Subject: [PATCH] Start decoupling AGP

---
 settings.gradle.kts                           |   1 +
 vgo-cli/build.gradle.kts                      | 106 ++++++++++++++++++
 .../kotlin/com/jzbrooks/vgo/cli}/ArgReader.kt |  14 ++-
 .../main/kotlin/com/jzbrooks/vgo/cli/main.kt  |  38 +++++++
 .../vgo/core/optimization/MergePaths.kt       |   3 +-
 vgo/build.gradle.kts                          |  90 +--------------
 .../kotlin/com/jzbrooks/vgo/Application.kt    |  62 +++++-----
 7 files changed, 186 insertions(+), 128 deletions(-)
 create mode 100644 vgo-cli/build.gradle.kts
 rename {vgo/src/main/kotlin/com/jzbrooks/vgo => vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli}/ArgReader.kt (82%)
 create mode 100644 vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/main.kt

diff --git a/settings.gradle.kts b/settings.gradle.kts
index 14521e78..70cdefe4 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -14,3 +14,4 @@ dependencyResolutionManagement {
 }
 
 include("vgo-core", "vgo", "vgo-plugin")
+include("vgo-cli")
diff --git a/vgo-cli/build.gradle.kts b/vgo-cli/build.gradle.kts
new file mode 100644
index 00000000..24d38e90
--- /dev/null
+++ b/vgo-cli/build.gradle.kts
@@ -0,0 +1,106 @@
+plugins {
+    id("org.jetbrains.kotlin.jvm")
+    id("com.vanniktech.maven.publish")
+}
+
+tasks {
+    jar {
+        dependsOn(configurations.runtimeClasspath)
+        manifest {
+            attributes["Main-Class"] = "com.jzbrooks.vgo.cli.MainKt"
+            attributes["Bundle-Version"] = project.properties["VERSION_NAME"]
+        }
+
+        val sourceClasses = sourceSets.main.get().output.classesDirs
+        inputs.files(sourceClasses)
+        destinationDirectory.set(layout.buildDirectory.dir("libs/debug"))
+
+        doFirst {
+            from(files(sourceClasses))
+            from(configurations.runtimeClasspath.get().asFileTree.files.map(::zipTree))
+
+            exclude(
+                "**/*.kotlin_metadata",
+                "**/*.kotlin_module",
+                "**/*.kotlin_builtins",
+                "**/module-info.class",
+                "META-INF/maven/**",
+                "META-INF/*.version",
+                "META-INF/LICENSE*",
+                "META-INF/LGPL2.1",
+                "META-INF/DEPENDENCIES",
+                "META-INF/AL2.0",
+                "META-INF/BCKEY.DSA",
+                "META-INF/BC2048KE.DSA",
+                "META-INF/BCKEY.SF",
+                "META-INF/BC2048KE.SF",
+                "**/NOTICE*",
+                "javax/activation/**",
+                "xsd/catalog.xml",
+            )
+        }
+    }
+
+
+    val optimize by registering(JavaExec::class) {
+        description = "Runs proguard on the jar application."
+        group = "build"
+
+        inputs.file("build/libs/debug/vgo.jar")
+        outputs.file("build/libs/vgo.jar")
+
+        val javaHome = System.getProperty("java.home")
+
+        classpath(r8)
+        mainClass = "com.android.tools.r8.R8"
+
+        args(
+            "--release",
+            "--classfile",
+            "--lib",
+            javaHome,
+            "--output",
+            "build/libs/vgo.jar",
+            "--pg-conf",
+            "$rootDir/optimize.pro",
+            "build/libs/debug/vgo.jar",
+        )
+
+        dependsOn(getByName("jar"))
+    }
+
+    val binaryFileProp = layout.buildDirectory.file("libs/vgo")
+    val binary by registering {
+        description = "Prepends shell script in the jar to improve CLI"
+        group = "build"
+
+        dependsOn(optimize)
+
+        inputs.file("build/libs/vgo.jar")
+        outputs.file(binaryFileProp)
+
+        doLast {
+            val binaryFile = binaryFileProp.get().asFile
+            binaryFile.parentFile.mkdirs()
+            binaryFile.delete()
+            binaryFile.appendText("#!/bin/sh\n\nexec java \$JAVA_OPTS -jar \$0 \"\$@\"\n\n")
+            file("build/libs/vgo.jar").inputStream().use { binaryFile.appendBytes(it.readBytes()) }
+            binaryFile.setExecutable(true, false)
+        }
+    }
+}
+
+val r8: Configuration by configurations.creating
+
+dependencies {
+    implementation(project(":vgo"))
+
+    implementation("com.android.tools:sdk-common:31.7.0")
+
+    r8("com.android.tools:r8:8.5.35")
+
+    testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1")
+    testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.0")
+    testImplementation("org.junit.jupiter:junit-jupiter-params")
+    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
+}
diff --git a/vgo/src/main/kotlin/com/jzbrooks/vgo/ArgReader.kt b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt
similarity index 82%
rename from vgo/src/main/kotlin/com/jzbrooks/vgo/ArgReader.kt
rename to vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt
index 29d005fc..182b9277 100644
--- a/vgo/src/main/kotlin/com/jzbrooks/vgo/ArgReader.kt
+++ b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt
@@ -1,4 +1,14 @@
-package com.jzbrooks.vgo
+package com.jzbrooks.vgo.cli
+
+import kotlin.collections.any
+import kotlin.collections.first
+import kotlin.collections.firstOrNull
+import kotlin.collections.getOrElse
+import kotlin.collections.indexOfFirst
+import kotlin.collections.isNotEmpty
+import kotlin.collections.map
+import kotlin.text.isNotBlank
+import kotlin.text.split
 
 class ArgReader(private val args: MutableList<String>) {
     private val hasArguments
@@ -41,7 +51,7 @@ class ArgReader(private val args: MutableList<String>) {
     }
 
     fun readArguments(): List<String> {
-        val arguments = mutableListOf<String>()
+        val arguments = kotlin.collections.mutableListOf<String>()
         while (hasArguments) {
             arguments.add(readArgument())
         }
diff --git a/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/main.kt b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/main.kt
new file mode 100644
index 00000000..50b5b848
--- /dev/null
+++ b/vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/main.kt
@@ -0,0 +1,38 @@
+import com.jzbrooks.vgo.Application
+import com.jzbrooks.vgo.cli.ArgReader
+import kotlin.system.exitProcess
+
+fun main(args: Array<String>) {
+    val argReader = ArgReader(args.toMutableList())
+
+    val printHelp = argReader.readFlag("help|h")
+    val printVersion = argReader.readFlag("version|v")
+    val printStats = argReader.readFlag("stats|s")
+    val indent = argReader.readOption("indent")?.toIntOrNull()
+
+    val outputs = run {
+        val outputPaths = mutableListOf<String>()
+        var output = argReader.readOption("output|o")
+        while (output != null) {
+            outputPaths.add(output)
+            output = argReader.readOption("output|o")
+        }
+        outputPaths.toList()
+    }
+
+    var format = argReader.readOption("format")
+
+    var inputs = argReader.readArguments()
+
+    val options = Application.Options(
+        printHelp = printHelp,
+        printVersion = printVersion,
+        printStats = printStats,
+        indent = indent,
+        output = outputs,
+        format = format,
+        input = inputs
+    )
+
+    exitProcess(Application(options).run())
+}
\ No newline at end of file
diff --git a/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt b/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt
index 999dab10..4c2d1d44 100644
--- a/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt
+++ b/vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/optimization/MergePaths.kt
@@ -7,6 +7,7 @@ import com.jzbrooks.vgo.core.graphic.Extra
 import com.jzbrooks.vgo.core.graphic.Graphic
 import com.jzbrooks.vgo.core.graphic.Group
 import com.jzbrooks.vgo.core.graphic.Path
+import com.jzbrooks.vgo.core.util.math.computeAbsoluteCoordinates
 
 /**
  * Merges multiple paths into a single path where possible
@@ -64,7 +65,7 @@ class MergePaths : BottomUpOptimization {
             //
             // There might be a reasonable way to deduce that situation more
             // specifically, which could enable merging of some even odd paths.
-            if (!haveSameAttributes(current, previous) || current.fillRule == Path.FillRule.EVEN_ODD) {
+            if (!haveSameAttributes(current, previous)) {
                 mergedPaths.add(current)
             } else {
                 previous.commands += current.commands
diff --git a/vgo/build.gradle.kts b/vgo/build.gradle.kts
index a291569c..df9433fc 100644
--- a/vgo/build.gradle.kts
+++ b/vgo/build.gradle.kts
@@ -11,18 +11,16 @@ plugins {
 
 kotlin.sourceSets.getByName("main").kotlin.srcDir("src/generated/kotlin")
 
-val r8: Configuration by configurations.creating
-
 dependencies {
     implementation(project(":vgo-core"))
-    implementation("com.android.tools:sdk-common:31.7.0")
+
+    compileOnly("com.android.tools:sdk-common:31.7.0")
 
     testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1")
     testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.1")
     testImplementation("org.junit.jupiter:junit-jupiter-params")
     testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
 
-    r8("com.android.tools:r8:8.5.35")
 }
 
 tasks {
@@ -30,43 +28,6 @@ tasks {
         dependsOn("generateConstants")
     }
 
-    jar {
-        dependsOn(configurations.runtimeClasspath)
-        manifest {
-            attributes["Main-Class"] = "com.jzbrooks.vgo.Application"
-            attributes["Bundle-Version"] = project.properties["VERSION_NAME"]
-        }
-
-        val sourceClasses = sourceSets.main.get().output.classesDirs
-        inputs.files(sourceClasses)
-        destinationDirectory.set(layout.buildDirectory.dir("libs/debug"))
-
-        doFirst {
-            from(files(sourceClasses))
-            from(configurations.runtimeClasspath.get().asFileTree.files.map(::zipTree))
-
-            exclude(
-                "**/*.kotlin_metadata",
-                "**/*.kotlin_module",
-                "**/*.kotlin_builtins",
-                "**/module-info.class",
-                "META-INF/maven/**",
-                "META-INF/*.version",
-                "META-INF/LICENSE*",
-                "META-INF/LGPL2.1",
-                "META-INF/DEPENDENCIES",
-                "META-INF/AL2.0",
-                "META-INF/BCKEY.DSA",
-                "META-INF/BC2048KE.DSA",
-                "META-INF/BCKEY.SF",
-                "META-INF/BC2048KE.SF",
-                "**/NOTICE*",
-                "javax/activation/**",
-                "xsd/catalog.xml",
-            )
-        }
-    }
-
     val generateConstants by registering {
         finalizedBy("compileKotlin")
 
@@ -115,53 +76,6 @@ tasks {
         mustRunAfter(generateConstants)
     }
 
-    val optimize by registering(JavaExec::class) {
-        description = "Runs proguard on the jar application."
-        group = "build"
-
-        inputs.file("build/libs/debug/vgo.jar")
-        outputs.file("build/libs/vgo.jar")
-
-        val javaHome = System.getProperty("java.home")
-
-        classpath(r8)
-        mainClass = "com.android.tools.r8.R8"
-
-        args(
-            "--release",
-            "--classfile",
-            "--lib",
-            javaHome,
-            "--output",
-            "build/libs/vgo.jar",
-            "--pg-conf",
-            "$rootDir/optimize.pro",
-            "build/libs/debug/vgo.jar",
-        )
-
-        dependsOn(getByName("jar"))
-    }
-
-    val binaryFileProp = layout.buildDirectory.file("libs/vgo")
-    val binary by registering {
-        description = "Prepends shell script in the jar to improve CLI"
-        group = "build"
-
-        dependsOn(optimize)
-
-        inputs.file("build/libs/vgo.jar")
-        outputs.file(binaryFileProp)
-
-        doLast {
-            val binaryFile = binaryFileProp.get().asFile
-            binaryFile.parentFile.mkdirs()
-            binaryFile.delete()
-            binaryFile.appendText("#!/bin/sh\n\nexec java \$JAVA_OPTS -jar \$0 \"\$@\"\n\n")
-            file("build/libs/vgo.jar").inputStream().use { binaryFile.appendBytes(it.readBytes()) }
-            binaryFile.setExecutable(true, false)
-        }
-    }
-
     val updateBaselineOptimizations by registering(Copy::class) {
         description = "Updates baseline assets with the latest integration test outputs."
         group = "Build Setup"
diff --git a/vgo/src/main/kotlin/com/jzbrooks/vgo/Application.kt b/vgo/src/main/kotlin/com/jzbrooks/vgo/Application.kt
index 4ffc8cb8..d5a131ac 100644
--- a/vgo/src/main/kotlin/com/jzbrooks/vgo/Application.kt
+++ b/vgo/src/main/kotlin/com/jzbrooks/vgo/Application.kt
@@ -19,51 +19,32 @@ import java.io.File
 import javax.xml.parsers.DocumentBuilderFactory
 import kotlin.math.absoluteValue
 import kotlin.math.roundToInt
-import kotlin.system.exitProcess
 
-class Application {
-    private var printStats = false
+class Application(private val options: Options) {
     private var printFileNames = false
     private var totalBytesBefore = 0.0
     private var totalBytesAfter = 0.0
-    private var outputFormat: String? = null
 
-    fun run(args: Array<String>): Int {
-        val argReader = ArgReader(args.toMutableList())
+    fun run(): Int {
 
-        if (argReader.readFlag("help|h")) {
+        if (options.printHelp) {
             println(HELP_MESSAGE)
             return 0
         }
 
-        if (argReader.readFlag("version|v")) {
+        if (options.printVersion) {
             println(BuildConstants.VERSION_NAME)
             return 0
         }
 
         val writerOptions = mutableSetOf<Writer.Option>()
-        argReader.readOption("indent")?.toIntOrNull()?.let { indentColumns ->
+        options.indent?.let { indentColumns ->
             writerOptions.add(Writer.Option.Indent(indentColumns))
         }
 
-        printStats = argReader.readFlag("stats|s")
-
-        val outputs =
-            run {
-                val outputPaths = mutableListOf<String>()
-                var output = argReader.readOption("output|o")
-                while (output != null) {
-                    outputPaths.add(output)
-                    output = argReader.readOption("output|o")
-                }
-                outputPaths.toList()
-            }
-
-        outputFormat = argReader.readOption("format")
-
-        var inputs = argReader.readArguments()
+        var inputs = options.input
         if (inputs.isEmpty()) {
-            require(outputs.isEmpty())
+            require(options.output.isEmpty())
 
             var path = readlnOrNull()
             val standardInPaths = mutableListOf<String>()
@@ -76,8 +57,8 @@ class Application {
         }
 
         val inputOutputMap =
-            if (outputs.isNotEmpty()) {
-                inputs.zip(outputs) { a, b ->
+            if (options.output.isNotEmpty()) {
+                inputs.zip(options.output) { a, b ->
                     Pair(File(a), File(b))
                 }
             } else {
@@ -88,7 +69,7 @@ class Application {
 
         val files = inputOutputMap.count { (input, _) -> input.isFile }
         val containsDirectory = inputOutputMap.any { (input, _) -> input.isDirectory }
-        printFileNames = printStats && (files > 1 || containsDirectory)
+        printFileNames = options.printStats && (files > 1 || containsDirectory)
 
         return handleFiles(inputOutputMap, writerOptions)
     }
@@ -151,7 +132,7 @@ class Application {
             var graphic =
                 when {
                     rootNodes.any { it.nodeName == "svg" || input.extension == "svg" } -> {
-                        if (outputFormat == "vd") {
+                        if (this.options.format == "vd") {
                             ByteArrayOutputStream().use { pipeOrigin ->
                                 val errors = Svg2Vector.parseSvgToXml(input.toPath(), pipeOrigin)
                                 if (errors != "") {
@@ -187,7 +168,7 @@ class Application {
                     else -> if (input == output) return else null
                 }
 
-            if (graphic is VectorDrawable && outputFormat == "svg") {
+            if (graphic is VectorDrawable && this.options.format == "svg") {
                 graphic = graphic.toSvg()
             }
 
@@ -220,7 +201,7 @@ class Application {
                     inputStream.copyTo(outputStream)
                 }
 
-                if (printStats) {
+                if (this.options.printStats) {
                     val sizeAfter = outputStream.channel.size()
                     val percentSaved = ((sizeBefore - sizeAfter) / sizeBefore.toDouble()) * 100
                     totalBytesBefore += sizeBefore
@@ -249,12 +230,12 @@ class Application {
             handleFile(file, File(output, file.name), options)
         }
 
-        if (printStats) {
+        if (this.options.printStats) {
             val message = "| Total bytes saved: ${(totalBytesBefore - totalBytesAfter).roundToInt()} |"
             val border = "-".repeat(message.length)
             println(
                 """
-                
+
                 $border
                 $message
                 $border
@@ -290,6 +271,16 @@ class Application {
     private val Map.Entry<File, File>.isDirectoryPair
         get() = key.isDirectory && (value.isDirectory || !value.exists())
 
+    data class Options(
+        val printHelp: Boolean = false,
+        val printVersion: Boolean = false,
+        val printStats: Boolean = false,
+        val output: List<String> = emptyList(),
+        val input: List<String> = emptyList(),
+        val indent: Int? = null,
+        val format: String? = null,
+    )
+
     companion object {
         private const val HELP_MESSAGE = """
 > vgo [options] [file/directory]
@@ -302,8 +293,5 @@ Options:
   --indent value  write files with value columns of indentation
   --format value  output format (svg, vd, etc)
         """
-
-        @JvmStatic
-        fun main(args: Array<String>): Unit = exitProcess(Application().run(args))
     }
 }