001package com.studentgui.apphelpers;
002
003import java.io.IOException;
004import java.nio.file.Files;
005import java.nio.file.Path;
006import java.nio.file.Paths;
007import java.util.ArrayList;
008import java.util.List;
009
010/**
011 * Miscellaneous filesystem and small utility helpers used by the UI pages.
012 *
013 * Responsibilities include selecting and creating the application home
014 * directory, creating per-student folder hierarchies, and providing a
015 * small roster fallback when no students.json exists.
016 */
017public class Helpers {
018    /**
019     * Private constructor to prevent instantiation of this utility class.
020     */
021    private Helpers() {
022        throw new AssertionError("Helpers is a utility class");
023    }
024    private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(Helpers.class);
025    /** The project working directory (where the process was started). */
026    public static final Path PROJECT_ROOT = Paths.get(System.getProperty("user.dir"));
027    /** Application home used for storing app-specific files (defaults to ./app_home). */
028    public static final Path APP_HOME = selectAppHome();
029    /** Root directory for persisted application data (alias of APP_HOME). */
030    public static final Path DATA_ROOT = APP_HOME;
031    /** Directory that holds the database file. */
032    public static final Path DATABASE_ROOT = DATA_ROOT.resolve("StudentDatabase");
033    /** Canonical database file path used by SQLite operations. */
034    public static final Path DATABASE_PATH = DATABASE_ROOT.resolve("students20252026.db");
035
036    /**
037     * Select a suitable application home directory. Attempts to use a
038     * ./app_home subdirectory of the working directory and falls back to the
039     * system temporary directory if creation fails.
040     */
041    private static Path selectAppHome() {
042        try {
043            Path candidate = PROJECT_ROOT.resolve("app_home");
044            Files.createDirectories(candidate);
045            // test write
046            Path test = candidate.resolve(".write_test");
047            Files.writeString(test, "");
048            Files.deleteIfExists(test);
049            return candidate;
050        } catch (IOException e) {
051                LOG.debug("Unable to create app_home; falling back to temp dir", e);
052                try {
053                    Path tmp = Paths.get(System.getProperty("java.io.tmpdir"), "StudentDataGUI");
054                    Files.createDirectories(tmp);
055                    return tmp;
056                } catch (IOException ex) {
057                    LOG.debug("Unable to create fallback temp dir; using CWD", ex);
058                    return Paths.get(".");
059                }
060        }
061    }
062
063    /**
064     * Attempt to set the JVM working directory to APP_HOME. Fails silently if
065     * the property cannot be set in the running environment.
066     */
067    public static void setStartDir() {
068        /**
069         * Set the JVM working directory to the application home when possible.
070         * Fail silently if the property cannot be set.
071         */
072        try {
073            System.setProperty("user.dir", APP_HOME.toString());
074        } catch (SecurityException se) {
075            LOG.debug("Unable to set user.dir to APP_HOME {}", APP_HOME, se);
076        }
077    }
078
079    /**
080     * Ensure the working data directory exists under APP_HOME. This is
081     * idempotent and safe to call on startup.
082     */
083    public static void workingDir() {
084        /**
085         * Ensure the working data directory exists under the application home.
086         */
087        try {
088            Path studentDataDir = APP_HOME.resolve("StudentDataFiles");
089            Files.createDirectories(studentDataDir);
090        } catch (IOException ioe) {
091            LOG.debug("Unable to create StudentDataFiles directory under {}", APP_HOME, ioe);
092        }
093    }
094
095    /**
096     * Create a basic folder hierarchy under DATA_ROOT for each student.
097     * This will create StudentDataFiles, backups and errorLogs and a
098     * per-student folder with subfolders for data sheets and materials.
099     */
100    public static void createFolderHierarchy() {
101        /**
102         * Create a basic folder hierarchy under DATA_ROOT for each student.
103         * This is idempotent and will create per-student subfolders and an
104         * omnibus csv file when missing.
105         */
106        // Create basic folders for each student in a simple roster
107        List<String> students = getStudents();
108        Path studentDatafilesRoot = DATA_ROOT.resolve("StudentDataFiles");
109        Path studentErrorlogsRoot = DATA_ROOT.resolve("errorLogs");
110        Path studentBackupsRoot = DATA_ROOT.resolve("backups");
111        try {
112            Files.createDirectories(studentDatafilesRoot);
113            Files.createDirectories(studentErrorlogsRoot);
114            Files.createDirectories(studentBackupsRoot);
115        } catch (IOException ioe) {
116            LOG.debug("Unable to create one or more data folders under {}", DATA_ROOT, ioe);
117        }
118
119        for (String name : students) {
120            String safe = sanitize(name);
121            Path studentFolder = studentDatafilesRoot.resolve(safe);
122            try {
123                Files.createDirectories(studentFolder.resolve("StudentDataSheets"));
124                Files.createDirectories(studentFolder.resolve("StudentInstructionMaterials"));
125                Files.createDirectories(studentFolder.resolve("StudentVisionAssessments"));
126                Path omnibus = studentFolder.resolve("omnibusDatabase.csv");
127                if (!Files.exists(omnibus)) {
128                    Files.createFile(omnibus);
129                }
130            } catch (IOException ioe) {
131                LOG.debug("Unable to create per-student folder or omnibus file for {}", name, ioe);
132            }
133        }
134    }
135
136    /**
137     * Make a filesystem-safe folder name by stripping or replacing forbidden
138     * characters.
139     */
140    private static String sanitize(final String s) {
141        if (s == null) {
142            return "";
143        }
144        String t = s.trim();
145        // remove control characters (newline, carriage return, etc.)
146        t = t.replaceAll("[\\p{Cntrl}]", "");
147        // replace common filesystem-forbidden characters with underscore
148        char[] forbidden = new char[]{'<','>',';',':','"','/','\\','|','?','*'};
149        for (char c : forbidden) {
150            t = t.replace(c, '_');
151        }
152        // collapse runs of whitespace into single space
153        t = t.replaceAll("\\s+", " ").trim();
154        // prevent names that are just dots
155        if (t.matches("^[.]+$")) {
156            t = "_";
157        }
158        return t;
159    }
160
161    /**
162     * Public safe name helper for filesystem paths. Mirrors the internal
163     * sanitize implementation but is callable from other packages.
164     *
165     * @param s input display name
166     * @return sanitized filesystem-safe name (never null)
167     */
168    public static String safeName(final String s) {
169        if (s == null) {
170            return "";
171        }
172        return sanitize(s);
173    }
174
175    /**
176     * Find the latest PNG plot file for a named student with the given prefix.
177     * Returns null when no matching files exist.
178    *
179    * @param studentName display name of student
180    * @param prefix file prefix such as "iOS" or "ScreenReader"
181    * @return path to the most recently modified matching PNG, or null
182     */
183    public static java.nio.file.Path latestPlotPath(final String studentName, final String prefix) {
184        if (studentName == null || studentName.trim().isEmpty()) {
185            return null;
186        }
187        java.nio.file.Path dir = studentPlotsDir(studentName);
188        if (!java.nio.file.Files.exists(dir)) {
189            return null;
190        }
191        java.nio.file.Path latest = null;
192        try (java.nio.file.DirectoryStream<java.nio.file.Path> ds = java.nio.file.Files.newDirectoryStream(dir, prefix + "-*.png")) {
193            for (java.nio.file.Path p : ds) {
194                try {
195                    if (latest == null) {
196                        latest = p;
197                    } else {
198                        java.nio.file.attribute.FileTime t1 = java.nio.file.Files.getLastModifiedTime(p);
199                        java.nio.file.attribute.FileTime t2 = java.nio.file.Files.getLastModifiedTime(latest);
200                        if (t1.compareTo(t2) > 0) {
201                            latest = p;
202                        }
203                    }
204                } catch (IOException ioe) {
205                    LOG.debug("Error reading file metadata for {}", p, ioe);
206                }
207            }
208        } catch (IOException ioe) {
209            LOG.debug("Error listing plot directory {}", dir, ioe);
210        }
211        return latest;
212    }
213
214    /**
215     * Return the per-student plots directory path (APP_HOME/StudentDataFiles/{safeName}/plots).
216     *
217     * @param studentName display name of the student
218     * @return path to the student's plots directory (never null)
219     */
220    public static java.nio.file.Path studentPlotsDir(final String studentName) {
221        return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("plots");
222    }
223
224    /**
225     * Return the per-student reports directory path (APP_HOME/StudentDataFiles/{safeName}/reports).
226     *
227     * @param studentName display name of the student
228     * @return path to the student's reports directory (never null)
229     */
230    public static java.nio.file.Path studentReportsDir(final String studentName) {
231        return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("reports");
232    }
233
234    /**
235     * Return the per-student collected data directory path (APP_HOME/StudentDataFiles/{safeName}/collected_data).
236     *
237     * @param studentName display name of the student
238     * @return path to the student's collected data directory (never null)
239     */
240    public static java.nio.file.Path studentCollectedDataDir(final String studentName) {
241        return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("collected_data");
242    }
243
244    /**
245     * Attempt to return a simple list of students from PROJECT_ROOT/json_Files/students.json.
246     * Falls back to a single 'Test Student' entry when the file is missing or cannot be read.
247     *
248     * @return list of student display names (never null)
249     */
250    public static List<String> getStudents() {
251        // Attempt to read a simple students.json in PROJECT_ROOT/json_Files/students.json
252        List<String> list = new ArrayList<>();
253        Path p = PROJECT_ROOT.resolve("json_Files").resolve("students.json");
254        if (Files.exists(p)) {
255            try {
256                String text = Files.readString(p);
257                // try to isolate the array portion if present
258                int start = text.indexOf('[');
259                int end = text.lastIndexOf(']');
260                String body = (start >= 0 && end > start) ? text.substring(start, end + 1) : text;
261                java.util.regex.Pattern pat = java.util.regex.Pattern.compile("\"([^\"]+)\"");
262                java.util.regex.Matcher m = pat.matcher(body);
263                while (m.find()) {
264                    String candidate = m.group(1).trim();
265                    if (!candidate.isEmpty()) {
266                        list.add(candidate);
267                    }
268                }
269            } catch (IOException ioe) {
270                LOG.debug("Unable to read students.json {}", p, ioe);
271            }
272        }
273        if (list.isEmpty()) {
274            // fallback roster
275            list.add("Test Student");
276        }
277        return list;
278    }
279
280    /**
281     * Return the default student to use when none is provided by the caller.
282     * This is the first entry from getStudents() or a sensible fallback when
283     * the roster is empty.
284     *
285     * @return display name of the default student (never null)
286     */
287    public static String defaultStudent() {
288        /**
289         * Note: UI pages use this helper to provide a non-null default student
290         * when constructed with a null/empty student name so that charts and
291         * page logic can operate without requiring an immediate user selection.
292         */
293        List<String> s = getStudents();
294        if (s == null || s.isEmpty()) {
295            return "Demo Student";
296        }
297        return s.get(0);
298    }
299}