hibiscus/src/main/kotlin/codes/som/hibiscus/api/command/CommandManager.kt

235 lines
8.0 KiB
Kotlin

package codes.som.hibiscus.api.command
import codes.som.hibiscus.api.command.ExecutableCommand.CommandBranch
import codes.som.hibiscus.api.command.context.CommandExecutionContext
import codes.som.hibiscus.api.command.exceptions.*
import codes.som.hibiscus.api.command.parser.ArgumentParser
import codes.som.hibiscus.api.command.utils.PeekableIterator
import codes.som.hibiscus.api.command.utils.splitExceptingQuotes
import java.util.*
class CommandManager(private val registerDefaultParsers: Boolean = true) {
private val parserRegistry = mutableMapOf<Class<*>, ArgumentParser<*>>()
val commands = mutableListOf<ExecutableCommand>()
var context: CommandContext = CommandContext.OTHER
init {
registerDefaultParsersIfApplicable()
}
private fun registerDefaultParsersIfApplicable() {
if (registerDefaultParsers) {
addDefaultParsers(parserRegistry)
}
}
fun <T> registerParser(type: Class<T>, parser: ArgumentParser<T>) {
parserRegistry[type] = parser
}
private fun verifyCommand(command: ExecutableCommand) {
// TODO: Prevent name collisions for commands / aliases
command.branches.forEach { branch ->
branch.parameterTypes
.filter { it !in parserRegistry }
.forEach { throw MissingParserException(it) }
}
}
@Throws(CommandRegistrationException::class)
fun register(declaration: Command) {
try {
val command = declaration.buildCommand()
verifyCommand(command)
commands += command
} catch (e: Exception) {
throw CommandRegistrationException(e)
}
}
@Throws(CommandExecutionException::class)
fun executeCommand(fullCommand: String) {
try {
val args = splitExceptingQuotes(fullCommand, true).toList()
if (args.isEmpty())
throw CommandNotFoundException("")
val commandName = args[0]
val matchingCommands = commands.filter { commandMatches(commandName, it) }
when {
matchingCommands.isEmpty() -> throw CommandNotFoundException(commandName)
matchingCommands.size > 1 -> throw CommandIsAmbiguousException(commandName)
}
val command = matchingCommands.first()
fun isBranchViable(branch: CommandBranch, args: List<String>): Boolean {
try {
val argObjects = parseArgumentsForBranch(branch, args)
if (argObjects.size == branch.parameterTypes.size) {
return true
}
} catch (e: Exception) {
return false
}
return false
}
val viableBranches = command.branches.filter { branchMatches(args, it) }.filter { isBranchViable(it, args) }
val viableNamedBranches = viableBranches.filter { it.name != null }
if (viableNamedBranches.size > 1)
throw BranchesAreAmbiguousException(commandName, args[1])
// Loop through named branches first, as branch names have priority over arguments.
for (branch in viableNamedBranches) {
executeBranch(branch, args)
return
}
for (branch in viableBranches.filter { it.name == null }) {
executeBranch(branch, args)
return
}
throw NoMatchingBranchesException()
} catch (e: Exception) {
throw CommandExecutionException(e)
}
}
fun completeCommand(fullCommand: String): Array<String> {
return completeCommandDuplicatesSpaces(fullCommand)
.map { s ->
if (s.toCharArray().any { it.isWhitespace() }) "\"$s\"" else s
} // Wrap suggestions containing spaces in quotes
.toSet() // Remove duplicates
.toTypedArray()
}
private fun completeCommandDuplicatesSpaces(fullCommand: String): List<String> {
val args = splitExceptingQuotes(fullCommand, true).toList()
if (args.isEmpty())
return commands.map { it.name }
if (args.size == 1) {
val namesAndAliasesOfCommands = mutableListOf<String>().apply {
addAll(commands.map { it.name })
commands.forEach { addAll(it.aliases) }
}
return namesAndAliasesOfCommands.filter {
it.lowercase(Locale.getDefault()).startsWith(fullCommand.lowercase(Locale.getDefault()))
}
}
val commandName = args[0]
val matchingCommands = commands.filter { commandMatches(commandName, it) }
when {
matchingCommands.isEmpty() -> return emptyList()
matchingCommands.size > 1 -> throw CommandIsAmbiguousException(commandName)
}
val command = matchingCommands.first()
if (args[1].isBlank())
return command.branches.mapNotNull { it.name }
val matchingBranches = mutableListOf<String>().apply {
addAll(command.branches.mapNotNull { it.name })
command.branches.forEach { addAll(it.aliases) }
}.filter { it.startsWith(args[1]) }
if (args.size == 2)
return matchingBranches
// TODO: Tab completion for parameters
// - Solve ambiguity issues
// ????
return emptyList()
}
private fun parseArgumentsForBranch(branch: CommandBranch, args: List<String>): Array<Any> {
val parsers = mutableListOf<ArgumentParser<*>>()
for ((index, parameterType) in branch.parameterTypes.withIndex()) {
if (index in branch.typeHints) {
parsers.add(branch.typeHints[index]!!)
} else {
if (parameterType !in parserRegistry)
throw MissingParserException(parameterType)
parsers.add(parserRegistry[parameterType]!!)
}
}
val startIndex = if (branch.name == null) 1 else 2
if (parsers.isEmpty() && args.size > startIndex) {
throw ParsingException(
InvalidArgumentCount(
branch.name
?: "<blank>"
)
)
}
val argsIterator = PeekableIterator(args.listIterator(startIndex))
val context = CommandExecutionContext(argsIterator, branch.parameterTypes, 0)
val argumentObjects = mutableListOf<Any>()
val minArgs = parsers.sumOf { it.minimumAcceptedArguments() }
if (args.size - startIndex >= minArgs && args.size - startIndex >= parsers.size) {
for ((index, parser) in parsers.withIndex()) {
context.currentParameter = index
try {
argumentObjects += parser.parse(context)!!
} catch (e: Exception) {
throw ParsingException(e)
}
}
} else {
throw ParsingException(
InvalidArgumentCount(
branch.name
?: "<blank>"
)
)
}
return argumentObjects.toTypedArray()
}
private fun executeBranch(branch: CommandBranch, args: List<String>) {
val argumentObjects = parseArgumentsForBranch(branch, args)
branch.execute(*argumentObjects)
}
private fun commandMatches(commandName: String, command: ExecutableCommand): Boolean {
return command.name.equals(commandName, ignoreCase = true) ||
command.aliases.any { it.equals(commandName, ignoreCase = true) }
}
private fun branchMatches(args: List<String>, branch: CommandBranch): Boolean {
return branch.name == null ||
(args.size > 1 && (branch.name.equals(args[1], ignoreCase = true) ||
branch.aliases.any { it.equals(args[1], ignoreCase = true) }))
}
fun reset() {
commands.clear()
parserRegistry.clear()
registerDefaultParsersIfApplicable()
}
}