001/************************* PROJECT RON *************************/
002/* Copyright (c) 2026 StuyPulse Robotics. All rights reserved. */
003/* Use of this source code is governed by an MIT-style license */
004/* that can be found in the repository LICENSE file.           */
005/***************************************************************/
006package tools.ToolClasses;
007
008import java.util.Arrays;
009import java.util.HashMap;
010import java.util.Map;
011
012/**
013 * <h2>ArgumentedTool</h2>
014 * A class that represents a tool that takes in arguments.
015 * <p>Using gradle tasks with arguments, it automatically parses the arguments based on an enum that the user defines and passes in.
016 * <p>The enum must implement the {@link ArgumentEnum} interface, which must specify a type via {@link ArgumentEnum#getArgumentType()}, 
017 * default value (can be null) via {@link ArgumentEnum#getDefaultValue()}, and description for each argument via {@link ArgumentEnum#getDescription()}.
018 * The order of the arguments is determined by the order of the enum constants.
019 * 
020 * <p>When parsing, it does the following:
021 * <ol>
022 *  <li>Splits the raw arguments by spaces, unless they are within quotes. (e.g. <code>-Pargs="arg1 'arg 2' arg3"</code> => <code>[arg1, 'arg 2', arg3]</code>)</li>
023 *  <li>Removes the surrounding quotes from the split arguments (e.g. <code>[arg1, 'arg 2', arg3]</code> => <code>[arg1, arg 2, arg3]</code>)</li>
024 *  <li>Iterates through the split arguments and attempts to parse them into the type specified by the enum constant {@link ArgumentEnum#getArgumentType()}.
025 *    <ul>
026 *      <li>If the type is a string, then it will stay as is.</li>
027 *      <li>If the type is an integer, then it will attempt to parse it into an integer.</li>
028 *      <li>If the type is a double, then it will attempt to parse it into a double.</li>
029 *      <li>If the type is a boolean, then it will attempt to parse it into a boolean.</li>
030 *      <li>If the type is an enum, then it will attempt to parse it into the enum by matching the name of the enum constant (case-insensitive).</li>
031 *    </ul>
032 *  </li>
033 *   <li>If the argument is missing or fails to parse, then it will use the default value specified by the enum. 
034 *   If there is no default value and the argument fails to parse, then it will throw an exception.</li>
035 *  <li>Extra arguments that are specified, greater than the length of the enum constant, will be ignored.</li>
036 * </ol>
037 * <p>The constructor takes in an enum class that defines the arguments. To use the, the {@link #execute(Map)} method must be implemented.
038 * This method has the parsed arguments passed in as a map, where the keys are the from the argument enum constants, and the values are the parsed arguments
039 * from the raw string passed into the {@link #run(String)} method.
040 * 
041 * <p>The {@link #run(String)} method is the main entry point for the tool. This will automatically parse the raw string arguments and call the 
042 * {@link #execute(Map)} method with the parsed arguments. 
043 */
044public abstract class ArgumentedTool<E extends Enum<E> & ArgumentEnum> {
045    private final Class<E> argumentsEnumClass;
046
047    @SuppressWarnings("unchecked")
048    private Map<E, Object> parseArguments(String rawArgs) {
049        String[] splitArgs = rawArgs.split(" (?=(?:[^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); // split by spaces if not within single or double quotes
050                                                                                               // all regex statements in this repo are vibe coded
051        Object[] argumentValues = new Object[splitArgs.length];
052        for (int i = 0; i < splitArgs.length; i++) {
053            argumentValues[i] = splitArgs[i].replaceAll("^\"|\"$|^'|'$", ""); // remove surrounding quotes
054            
055            E currentEnum = argumentsEnumClass.getEnumConstants()[i];
056            if (currentEnum.getArgumentType() == String.class) continue;
057
058            switch (argumentsEnumClass.getEnumConstants()[i].getArgumentType().getSimpleName()) {
059                case "Integer", "int" -> {
060                    try {
061                        argumentValues[i] = Integer.parseInt((String) argumentValues[i]);
062                    } catch (Exception e) {
063                        if (currentEnum.getDefaultValue() == null || currentEnum.getDefaultValue().getClass() != Integer.class) {
064                            throw new IllegalArgumentException("Argument " + currentEnum.name() + "(" + currentEnum.getDescription() + ") must be an integer");
065                        }
066                    }
067                    break;
068                }
069                case "Double", "double" -> {
070                    try {
071                        argumentValues[i] = Double.parseDouble((String) argumentValues[i]);
072                    } catch (Exception e) {
073                        if (currentEnum.getDefaultValue() == null || currentEnum.getDefaultValue().getClass() != Double.class) {
074                            throw new IllegalArgumentException("Argument #" + (i + 1) + " (" + currentEnum.getDescription() + ") must be a double");
075                        }
076                    }
077                    break;
078                }
079                case "Boolean", "boolean" -> {
080                    if (((String) argumentValues[i]).equalsIgnoreCase("true") || ((String) argumentValues[i]).equalsIgnoreCase("false")) {
081                        argumentValues[i] = Boolean.parseBoolean((String) argumentValues[i]);
082                    } else {
083                        if (currentEnum.getDefaultValue() == null || currentEnum.getDefaultValue().getClass() != Boolean.class) {
084                            throw new IllegalArgumentException("Argument #" + (i + 1) + " (" + currentEnum.getDescription() + ") must be a boolean");
085                        }
086                    }
087                    break;
088                }
089                default -> {
090                    if (currentEnum.getArgumentType().isEnum()) {
091                        Enum<E>[] enumConstants = (Enum<E>[]) currentEnum.getArgumentType().getEnumConstants();
092                        try {
093                            String enumName = (String) argumentValues[i];
094                            Enum<?> match = null;
095                            for (Enum<?> c : (Enum<?>[]) currentEnum.getArgumentType().getEnumConstants()) {
096                                if (c.name().equalsIgnoreCase(enumName)) {
097                                    match = c;
098                                    break;
099                                }
100                            }
101
102                            if (match == null) {
103                                throw new IllegalArgumentException(); // to be caught by catch block
104                            }
105                            argumentValues[i] = match;
106                        } catch (Exception e) {
107                            throw new IllegalArgumentException("Argument #" + (i + 1) + " (" + currentEnum.getDescription() + ") must be one of " + Arrays.toString(enumConstants));
108                        }
109                    } else {
110                        argumentValues[i] = currentEnum.getDefaultValue();
111                    }
112                }
113            }
114        }
115
116        Map<E, Object> parsedArgs = new HashMap<>();
117        for (int i = 0; i < argumentsEnumClass.getEnumConstants().length; i++) {
118            E currentEnum = argumentsEnumClass.getEnumConstants()[i];
119            if (i < argumentValues.length) {
120                parsedArgs.put(currentEnum, argumentValues[i]);
121            } else {
122                parsedArgs.put(currentEnum, currentEnum.getDefaultValue());
123            }
124        }
125
126        System.out.println(parsedArgs + " - Parsed Args");
127        return parsedArgs;
128    }
129
130    protected ArgumentedTool(Class<E> argumentsEnumClass) {
131        this.argumentsEnumClass = argumentsEnumClass;
132    }
133
134    /**
135     * The functionality for the tool. Must be implemented.
136     * @param parsedArgs the parsed arguments
137     */
138    protected abstract void execute(Map<E, Object> parsedArgs);
139    /**
140     * The main entry point for the tool. This will call the {@link #execute(Map)} method with the parsed arguments.
141     */
142    public final void run(String rawArgs) {
143        Map<E, Object> parsedArgs = parseArguments(rawArgs);
144        execute(parsedArgs);
145    }
146}