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.PathplannerSearch;
007
008import java.io.File;
009import java.io.IOException;
010import java.nio.file.Files;
011import java.util.ArrayList;
012import java.util.List;
013import java.util.Map;
014import java.util.regex.Matcher;
015import java.util.regex.Pattern;
016
017import tools.PathplannerSearch.Main.ARGUMENTS;
018import tools.ToolClasses.ArgumentedTool;
019
020import static tools.util.AnsiColors.*;
021
022/**
023 *
024 *
025 * <h2>Utility class that gives search features that Pathplanner lacks</h2>
026 *
027 * <p>
028 * This class is meant to help search for where paths and linked variables are
029 * used so you can
030 * clean up unused ones.
031 */
032public final class PathplannerSearch extends ArgumentedTool<ARGUMENTS> {
033    public enum SearchType {
034        LINKED_WAYPOINT,
035        PATH
036    }
037
038    public PathplannerSearch() {
039        super(Main.ARGUMENTS.class);
040    }
041
042    /**
043     *
044     *
045     * <h4>Functionality</h4>
046     *
047     * <p>
048     * Searches through .path files in <code>src/main/deploy/pathplanner</code> and
049     * logs files that
050     * match a certain search term based on the search type.
051     *
052     * @param parsedArgs the parsed arguments
053     */
054    @Override
055    protected void execute(Map<ARGUMENTS, Object> parsedArgs) {
056        String searchTerm = ((String) parsedArgs.get(ARGUMENTS.SEARCH_TERM)).toLowerCase();
057        SearchType searchType = ((SearchType) parsedArgs.get(ARGUMENTS.SEARCH_TYPE));
058
059        switch (searchType) {
060            case LINKED_WAYPOINT -> {
061                final File[] files = new File("./src/main/deploy/pathplanner/paths/").listFiles();
062                if (files == null)
063                    break;
064
065                final List<String> matches = new ArrayList<>();
066                for (final File file : files) {
067                    try {
068                        final String content = Files.readString(file.toPath()).toLowerCase();
069                        if (content.isBlank())
070                            continue;
071                        if (content.contains("\"linkedname\"") && content.contains("\"" + searchTerm + "\"")) {
072                            final String folderName = parseFolderKeyFromFileContent(content);
073                            matches.add(folderName + "/" + file.getName());
074                        }
075                    } catch (IOException e) {
076                        System.out.println("Error reading file: " + file.getName());
077                        e.printStackTrace();
078                    }
079                }
080
081                logFiles(searchTerm, searchType, matches);
082            }
083            case PATH -> {
084                final File[] files = new File("./src/main/deploy/pathplanner/autos/").listFiles();
085                if (files == null)
086                    break;
087
088                final List<String> matches = new ArrayList<>();
089                for (final File file : files) {
090                    try {
091                        final String content = Files.readString(file.toPath()).toLowerCase();
092                        if (!content.contains("\"sequential\""))
093                            continue;
094                        if (content.contains("\"path\"")
095                                && content.contains("\"pathname\"")
096                                && content.contains("\"" + searchTerm + "\"")) {
097
098                            final String folderName = parseFolderKeyFromFileContent(content);
099                            matches.add(folderName + "/" + file.getName());
100                        }
101                    } catch (IOException e) {
102                        System.out.println("Error reading file: " + file.getName());
103                        e.printStackTrace();
104                    }
105                }
106
107                logFiles(searchTerm, searchType, matches);
108            }
109            default -> {
110                System.out.println("Specify a valid search type by editing the file.");
111                System.exit(1);
112            }
113        }
114    }
115
116    /**
117     * Parses the "folder" key from the content of a .path file.
118     *
119     * <p>
120     * As Java doesn't have a built in JSON parser and for only having to do this
121     * once, it would be
122     * overkill to add a dependency.
123     *
124     * <p>
125     * This method uses regex to parse the "folder" key from the .path file.
126     * <code>.path</code>
127     * files are essentially just JSON files but with a different extension.
128     *
129     * @param content the content of the .path file
130     * @return the value of the "folder" key in the .path file, or an empty string
131     *         if it doesn't exist
132     */
133    public static String parseFolderKeyFromFileContent(String content) {
134        final String folderName;
135        Pattern regex = Pattern.compile(
136                "\"folder\"\\s*:\\s*\"([^\"]*)\"",
137                Pattern.CASE_INSENSITIVE); // ima be honest i used ai for this regex 🤤
138        Matcher match = regex.matcher(content);
139        if (match.find()) {
140            folderName = match.group(1);
141        } else {
142            folderName = "";
143        }
144
145        return folderName;
146    }
147
148    /**
149     * Logs the result of the search to the console with some formatting.
150     *
151     * <p>
152     * Uses ANSI color codes for coloring the output.
153     *
154     * <p>
155     * If no matches are found, it will log "None were found." in red.
156     *
157     * <p>
158     * Otherwise, it will log the list of matches in green on separate lines.
159     *
160     * <p>
161     * <b>Example input and output:</b>
162     *
163     * <pre>
164     * ./gradlew runPathPlannersearch -Pargs="disrupt path"
165     * </pre>
166     *
167     * <pre>
168     * Autons that use the path 'disrupt':
169     * disrupt/LB Disrupt.auto
170     * disrupt/LT Disrupt.auto
171     * disrupt/RB Disrupt.auto
172     * disrupt/RT Disrupt.auto
173     * </pre>
174     *
175     * @param searchTerm
176     * @param searchType
177     * @param matches
178     */
179    public static void logFiles(String searchTerm, SearchType searchType, List<String> matches) {
180        String stem = switch (searchType) {
181            case LINKED_WAYPOINT -> "Paths that use the linked waypoint '";
182            case PATH -> "Autons that use the path '";
183            default -> "";
184        };
185
186        System.out.println(stem + YELLOW + searchTerm + "'" + RESET + ":");
187        if (matches.isEmpty()) {
188            System.out.println(RED + "None were found." + RESET);
189            return;
190        }
191
192        for (String match : matches) {
193            System.out.println(GREEN + match + RESET);
194        }
195    }
196}