001package com.studentgui.app;
002
003import java.awt.BorderLayout;
004import java.awt.CardLayout;
005import java.awt.FlowLayout;
006import java.awt.Font;
007import java.time.LocalDate;
008import java.time.format.DateTimeParseException;
009import java.util.List;
010
011import javax.swing.JButton;
012import javax.swing.JComboBox;
013import javax.swing.JComponent;
014import javax.swing.JFrame;
015import javax.swing.JLabel;
016import javax.swing.JPanel;
017import javax.swing.JTextField;
018import javax.swing.SwingUtilities;
019import javax.swing.UIManager;
020
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024import com.studentgui.apphelpers.Helpers;
025import com.studentgui.apphelpers.SqlGenerate;
026import com.studentgui.apppages.Abacus;
027import com.studentgui.apppages.Braille;
028import com.studentgui.apppages.BrailleNote;
029import com.studentgui.apppages.BrailleSense;
030import com.studentgui.apppages.CVI;
031import com.studentgui.apppages.ContactLog;
032import com.studentgui.apppages.DigitalLiteracy;
033import com.studentgui.apppages.Homepage;
034import com.studentgui.apppages.IOS;
035import com.studentgui.apppages.InstructionalMaterials;
036import com.studentgui.apppages.JLineGraph;
037import com.studentgui.apppages.Keyboarding;
038import com.studentgui.apppages.Observations;
039import com.studentgui.apppages.ScreenReader;
040import com.studentgui.apppages.SessionNotes;
041import com.studentgui.apptheming.Theme;
042
043/**
044 * Main application entry and UI wiring for the Student Skills Progressions app.
045 *
046 * This class builds the top-level window, menu, and registers the skill pages
047 * (each page is a JPanel). It's intentionally lightweight; most functionality
048 * for database access and page logic lives in helper classes and the page
049 * components under com.studentgui.apppages.
050 */
051/**
052 * Application bootstrap and top-level UI wiring. Builds the main JFrame,
053 * registers pages, and provides a small top control bar for switching
054 * students and pages.
055 */
056/**
057 * Application bootstrap and top-level UI wiring. Builds the main JFrame,
058 * registers pages, and provides a small top control bar for switching
059 * students and pages.
060 */
061/**
062 * Application entry point and top-level UI wiring for the Student Skills
063 * Progressions application. Builds the main frame, menu and registers per-page
064 * panels under a CardLayout.
065 */
066public class Main {
067    /**
068     * Bootstrap logging/system properties very early so Logback can resolve
069     * file locations and the per-run timestamp before any logger is
070     * initialized. This static block sets APP_HOME and LOG_TS and performs
071     * a cleanup of old log files older than 7 days.
072     */
073    static {
074        try {
075            // Ensure Helpers.APP_HOME is initialized and use it for logging
076            String appHome = com.studentgui.apphelpers.Helpers.APP_HOME.toString();
077            System.setProperty("APP_HOME", appHome);
078            // unix epoch seconds appended to per-run log filename
079            String ts = String.valueOf(java.time.Instant.now().getEpochSecond());
080            System.setProperty("LOG_TS", ts);
081
082            // create logs dir
083            java.nio.file.Path logs = java.nio.file.Paths.get(appHome).resolve("logs");
084            java.nio.file.Files.createDirectories(logs);
085
086            // Cleanup: remove log files older than 7 days (by last modified time)
087            long cutoff = java.time.Instant.now().minus(java.time.Duration.ofDays(7)).toEpochMilli();
088            try (java.nio.file.DirectoryStream<java.nio.file.Path> ds = java.nio.file.Files.newDirectoryStream(logs, "log_*.log")) {
089                for (java.nio.file.Path p : ds) {
090                    try {
091                        java.nio.file.attribute.FileTime ft = java.nio.file.Files.getLastModifiedTime(p);
092                        if (ft.toMillis() < cutoff) {
093                            java.nio.file.Files.deleteIfExists(p);
094                        }
095                    } catch (Exception ex) {
096                        // Swallow cleanup exceptions; logging isn't available yet.
097                    }
098                }
099            } catch (Exception ex) {
100                // ignore
101            }
102            // also remove consolidated data dump files older than retention
103            try (java.nio.file.DirectoryStream<java.nio.file.Path> ds2 = java.nio.file.Files.newDirectoryStream(logs, "data_dumps_*.log")) {
104                for (java.nio.file.Path p : ds2) {
105                    try {
106                        java.nio.file.attribute.FileTime ft = java.nio.file.Files.getLastModifiedTime(p);
107                        if (ft.toMillis() < cutoff) {
108                            java.nio.file.Files.deleteIfExists(p);
109                        }
110                    } catch (Exception ex) {
111                        // Swallow cleanup exceptions; logging isn't available yet.
112                    }
113                }
114            } catch (Exception ex) {
115                // ignore
116            }
117        } catch (Exception ex) {
118            // If anything here fails, continue — logging may not be configured yet.
119        }
120    }
121
122    private static final Logger LOG = LoggerFactory.getLogger(Main.class);
123    private static JFrame frame;
124    private static JPanel contentPanel;
125    private static JLineGraph sharedGraph;
126    /**
127     * Shared JLineGraph instance used across pages.
128     *
129     * Pages are constructed with the shared graph passed into their
130     * constructors (see recreatePages). The shared graph is registered
131     * with {@link #addSettingsChangeListener(SettingsChangeListener)} so
132     * it receives runtime preference updates. If a page creates its own
133     * page-local JLineGraph instance it should register it with
134     * {@link #addSettingsChangeListener(SettingsChangeListener)} and remove
135     * it when disposed to ensure it receives preference changes and to
136     * avoid leaking listeners.
137     */
138    // current date used by the top bar (can be updated without recreating pages)
139    private static java.time.LocalDate currentDate;
140    private static String currentStudent;
141    // Listeners to notify when the top-bar date changes
142    private static final java.util.List<DateChangeListener> dateListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
143
144    /**
145     * Register a listener to be notified when the application date is changed via the top bar.
146     *
147     * @param l listener to register (ignored when null)
148     */
149    public static void addDateChangeListener(final DateChangeListener l) { 
150        if (l != null) {
151            dateListeners.add(l);
152        }
153    }
154
155    /**
156     * Remove a previously registered date change listener.
157     *
158     * @param l listener to remove (ignored when null)
159     */
160    public static void removeDateChangeListener(final DateChangeListener l) { 
161        if (l != null) {
162            dateListeners.remove(l);
163        }
164    }
165
166    /**
167     * Clear all registered date change listeners.
168     */
169    public static void clearDateChangeListeners() { 
170        dateListeners.clear();
171    }
172
173    /**
174     * Notify all registered date listeners that the application date has changed.
175     *
176     * @param d new application date
177     */
178    private static void notifyDateChanged(final java.time.LocalDate d) {
179        for (DateChangeListener l : dateListeners) {
180            try {
181                l.dateChanged(d);
182            } catch (Exception ex) {
183                LOG.warn("DateChangeListener threw: {}", ex.toString());
184            }
185        }
186    }
187    // Student change listeners
188    private static final java.util.List<StudentChangeListener> studentListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
189    /**
190     * Register a listener to be notified when the selected student is changed.
191     *
192     * @param l listener to register (ignored when null)
193     */
194    public static void addStudentChangeListener(final StudentChangeListener l) {
195        if (l != null) {
196            studentListeners.add(l);
197        }
198    }
199
200    /**
201     * Remove a previously registered student change listener.
202     *
203     * @param l listener to remove (ignored when null)
204     */
205    public static void removeStudentChangeListener(final StudentChangeListener l) {
206        if (l != null) {
207            studentListeners.remove(l);
208        }
209    }
210
211    /**
212     * Clear all registered student change listeners.
213     */
214    public static void clearStudentChangeListeners() {
215        studentListeners.clear();
216    }
217
218    /**
219     * Notify registered student change listeners that the selected student has changed.
220     *
221     * @param s new selected student name
222     */
223    private static void notifyStudentChanged(final String s) {
224        currentStudent = s;
225        for (StudentChangeListener l : studentListeners) {
226            try {
227                l.studentChanged(s);
228            } catch (Exception ex) {
229                LOG.warn("StudentChangeListener threw: {}", ex.toString());
230            }
231        }
232    }
233
234
235    // Settings change listeners
236    private static final java.util.List<SettingsChangeListener> settingsListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
237
238    /**
239     * Register a listener to be notified when application settings change.
240     * Implementations should read values from {@link com.studentgui.apphelpers.Settings}
241     * when {@link SettingsChangeListener#settingsChanged()} is invoked.
242     *
243     * @param l listener to register (ignored when null)
244     */
245    public static void addSettingsChangeListener(final SettingsChangeListener l) {
246        if (l != null) {
247            settingsListeners.add(l);
248        }
249    }
250
251    /**
252     * Remove a previously registered settings change listener.
253     *
254     * @param l listener to remove (ignored when null)
255     */
256    public static void removeSettingsChangeListener(final SettingsChangeListener l) {
257        if (l != null) {
258            settingsListeners.remove(l);
259        }
260    }
261
262    /**
263     * Clear all registered settings change listeners.
264     */
265    public static void clearSettingsChangeListeners() {
266        settingsListeners.clear();
267    }
268
269    /**
270     * Notify all registered settings listeners that application settings have been changed.
271     * This is typically invoked after persisting preferences through
272     * {@link com.studentgui.apphelpers.Settings}.
273     */
274    public static void notifySettingsChanged() {
275        for (SettingsChangeListener l : settingsListeners) {
276            try {
277                l.settingsChanged();
278            } catch (Exception ex) {
279                LOG.warn("SettingsChangeListener threw: {}", ex.toString());
280            }
281        }
282    }
283
284    /**
285     * Application entry point. Initializes helpers, database, and launches the
286     * Swing UI on the EDT.
287     *
288     * @param args command-line arguments (unused)
289     */
290    public static void main(final String[] args) {
291        // Apply saved look and feel (default to light)
292        // Settings.get and setTheme handle any expected failures internally;
293        // call directly so we avoid a broad RuntimeException catch.
294        String saved = com.studentgui.apphelpers.Settings.get("theme", "light");
295        setTheme(saved);
296
297        // Initialize helpers and DB
298        Helpers.setStartDir();
299        Helpers.createFolderHierarchy();
300        SqlGenerate.initializeDatabase();
301
302        SwingUtilities.invokeLater(() -> {
303            frame = new JFrame("Student Skills Progressions");
304            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
305            frame.setSize(1000, 700);
306            frame.setLocationRelativeTo(null);
307
308            // Menu bar: obtain the app menu bar from Theme, insert a File->Exit menu at the far left
309            javax.swing.JMenuBar themeBar = Theme.createMenuBar();
310            if (themeBar == null) {
311                themeBar = new javax.swing.JMenuBar();
312            }
313            javax.swing.JMenu fileMenu = new javax.swing.JMenu("File");
314            javax.swing.JMenuItem exitItem = new javax.swing.JMenuItem("Exit");
315            exitItem.addActionListener(e -> {
316                LOG.info("Exit requested via File->Exit");
317                if (frame != null) {
318                    frame.dispose();
319                }
320                System.exit(0);
321            });
322            fileMenu.add(exitItem);
323            // Insert file menu at position 0 so it appears on the far left
324            themeBar.add(fileMenu, 0);
325            // Ensure the Themes menu (if present) appears immediately after File
326            int themesIdx = -1;
327            for (int i = 0; i < themeBar.getMenuCount(); i++) {
328                javax.swing.JMenu m = themeBar.getMenu(i);
329                if (m != null && "Themes".equals(m.getText())) { themesIdx = i; break; }
330            }
331            if (themesIdx > 1) {
332                javax.swing.JMenu themesMenu = themeBar.getMenu(themesIdx);
333                themeBar.remove(themesIdx);
334                themeBar.add(themesMenu, 1);
335            }
336            frame.setJMenuBar(themeBar);
337
338
339            contentPanel = new JPanel(new CardLayout());
340            frame.add(contentPanel, BorderLayout.CENTER);
341
342            // Top control bar: student selector, date, and navigation
343            JPanel topBar = buildTopBar();
344            frame.add(topBar, BorderLayout.NORTH);
345
346            // Create initial shared graph and pages for the first student
347            sharedGraph = new JLineGraph();
348            // Register shared graph to receive settings change notifications
349            addSettingsChangeListener(sharedGraph);
350            List<String> students = Helpers.getStudents();
351            String demoStudent = students.isEmpty() ? "Demo Student" : students.get(0);
352            LocalDate today = LocalDate.now();
353            currentDate = today;
354            recreatePages(demoStudent, today);
355
356            frame.setVisible(true);
357        });
358    }
359
360    /**
361     * Change application theme at runtime. Supported values: "light", "dark", "darcula".
362     * This method updates the installed Look and Feel and refreshes the main frame.
363    *
364    * @param theme human-friendly theme name or fully-qualified LookAndFeel class name
365     */
366    public static void setTheme(final String theme) {
367        try {
368            String t = theme == null ? "light" : theme;
369            // Common keywords for bundled themes
370            switch (t.toLowerCase()) {
371                case "dark":
372                case "flatdarklaf":
373                    UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarkLaf());
374                    break;
375                case "darcula":
376                    // Darcula-like: use FlatDarkLaf as fallback
377                    UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarkLaf());
378                    break;
379                case "light":
380                case "flatlightlaf":
381                    UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf());
382                    break;
383                default:
384                    // If the string looks like a fully-qualified class name, try to set it directly.
385                    if (t.contains(".")) {
386                        try {
387                            UIManager.setLookAndFeel(t);
388                        } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException ex) {
389                            // Try to instantiate via reflection
390                            try {
391                                Class<?> c = Class.forName(t);
392                                Object o = c.getDeclaredConstructor().newInstance();
393                                if (o instanceof javax.swing.LookAndFeel) {
394                                    UIManager.setLookAndFeel((javax.swing.LookAndFeel) o);
395                                } else {
396                                    // fallback to light
397                                    UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf());
398                                }
399                            } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException ex2) {
400                                LOG.error("Failed to set look and feel by class name {}", t, ex2);
401                                UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf());
402                            }
403                        }
404                    } else {
405                        // Try to find an installed LAF by name
406                        boolean applied = false;
407                        for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
408                            if (info.getName().equalsIgnoreCase(t) || info.getName().toLowerCase().contains(t.toLowerCase())) {
409                                UIManager.setLookAndFeel(info.getClassName());
410                                applied = true;
411                                break;
412                            }
413                        }
414                        if (!applied) {
415                            // default fallback
416                            UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf());
417                        }
418                    }
419                    break;
420            }
421            if (frame != null) {
422                javax.swing.SwingUtilities.updateComponentTreeUI(frame);
423                frame.pack();
424            }
425        } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException | IllegalArgumentException e) {
426            LOG.error("Failed to set theme {}", theme, e);
427        }
428    }
429
430    private static JPanel buildTopBar() {
431        JPanel bar = new JPanel(new FlowLayout(FlowLayout.LEFT));
432        List<String> students = Helpers.getStudents();
433        JComboBox<String> studentBox = new JComboBox<>(students.toArray(new String[0]));
434        studentBox.setEditable(false);
435
436        JLabel studentLabel = new JLabel("Student:");
437        studentLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24));
438        JLabel dateLabel = new JLabel("Date (YYYY-MM-DD):");
439        dateLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24));
440        JTextField dateField = new JTextField(LocalDate.now().toString(), 10);
441
442        JButton goBtn = new JButton("Apply");
443
444        bar.add(studentLabel);
445        bar.add(studentBox);
446        bar.add(dateLabel);
447        bar.add(dateField);
448        bar.add(goBtn);
449
450            goBtn.addActionListener(e -> {
451            String selected = (String) studentBox.getSelectedItem();
452            LocalDate date = LocalDate.now();
453            try {
454                date = LocalDate.parse(dateField.getText());
455            } catch (DateTimeParseException ex) {
456                // keep today
457            }
458            // Update the app's current date and selected student without recreating pages; show a confirmation dialog.
459            currentDate = date;
460            currentStudent = selected;
461            javax.swing.JOptionPane.showMessageDialog(frame,
462                    "The date has been updated to " + date.toString(),
463                    "Date Updated",
464                    javax.swing.JOptionPane.INFORMATION_MESSAGE);
465            // Notify registered pages so they can update any internal state
466            notifyDateChanged(date);
467            notifyStudentChanged(selected);
468        });
469
470    // Navigation buttons removed from top bar per UI request; pages can still be selected via menu
471
472        return bar;
473    }
474
475    /**
476     * Recreate per-page panels for the provided student and date. This replaces
477     * the CardLayout content so the shared graph and pages are reset.
478     */
479    /**
480     * Recreate per-page panels for the provided student and date. This
481     * replaces the CardLayout content so the shared graph and pages are reset.
482     *
483     * @param student selected student's display name
484     * @param date the session date for newly created pages
485     */
486    private static void recreatePages(final String student, final LocalDate date) {
487        // recreate the pages with a fresh sharedGraph so the graph is reset for the selected student/date
488        if (sharedGraph == null) {
489            sharedGraph = new JLineGraph();
490        } else {
491            sharedGraph = new JLineGraph();
492        }
493
494    // Clear any previous listeners to avoid stale references
495    clearDateChangeListeners();
496    clearStudentChangeListeners();
497
498        contentPanel.removeAll();
499        contentPanel.add(Homepage.create(), "homepage");
500
501        // Instantiate pages into locals so we can register listeners if they implement the interface
502        Braille braille = new Braille(student, date, sharedGraph);
503        contentPanel.add(braille, "braille");
504    if (braille instanceof DateChangeListener d) {
505        addDateChangeListener(d);
506    }
507    if (braille instanceof StudentChangeListener s) {
508        addStudentChangeListener(s);
509    }
510
511        Abacus abacus = new Abacus(student, date, sharedGraph);
512        contentPanel.add(abacus, "abacus");
513    if (abacus instanceof DateChangeListener d2) {
514        addDateChangeListener(d2);
515    }
516    if (abacus instanceof StudentChangeListener s2) {
517        addStudentChangeListener(s2);
518    }
519
520        BrailleNote brailleNote = new BrailleNote(student, date, sharedGraph);
521        contentPanel.add(brailleNote, "braillenote");
522    if (brailleNote instanceof DateChangeListener d3) {
523        addDateChangeListener(d3);
524    }
525    if (brailleNote instanceof StudentChangeListener s3) {
526        addStudentChangeListener(s3);
527    }
528
529        DigitalLiteracy dl = new DigitalLiteracy(student, date, sharedGraph);
530        contentPanel.add(dl, "digitalliteracy");
531    if (dl instanceof DateChangeListener d4) {
532        addDateChangeListener(d4);
533    }
534    if (dl instanceof StudentChangeListener s4) {
535        addStudentChangeListener(s4);
536    }
537
538        // pages that don't currently need date-driven updates remain created inline
539        contentPanel.add(new BrailleSense(student, date, sharedGraph), "braillesense");
540        contentPanel.add(new CVI(student, date, sharedGraph), "cvi");
541
542        IOS ios = new IOS(student, date, sharedGraph);
543        contentPanel.add(ios, "ios");
544    if (ios instanceof DateChangeListener d5) {
545        addDateChangeListener(d5);
546    }
547    if (ios instanceof StudentChangeListener s5) {
548        addStudentChangeListener(s5);
549    }
550
551        Keyboarding keyboarding = new Keyboarding(student, date, sharedGraph);
552        contentPanel.add(keyboarding, "keyboarding");
553    if (keyboarding instanceof DateChangeListener d6) {
554        addDateChangeListener(d6);
555    }
556    if (keyboarding instanceof StudentChangeListener s6) {
557        addStudentChangeListener(s6);
558    }
559
560        contentPanel.add(new Observations(student, date), "observations");
561
562        ScreenReader sr = new ScreenReader(student, date, sharedGraph);
563        contentPanel.add(sr, "screenreader");
564    if (sr instanceof DateChangeListener d7) {
565        addDateChangeListener(d7);
566    }
567    if (sr instanceof StudentChangeListener s7) {
568        addStudentChangeListener(s7);
569    }
570
571    contentPanel.add(new SessionNotes(student, date, sharedGraph), "sessionnotes");
572    contentPanel.add(new ContactLog(student, date, sharedGraph), "contactlog");
573        contentPanel.add(new InstructionalMaterials(), "instructionalmaterials");
574
575        contentPanel.revalidate();
576        contentPanel.repaint();
577        showPage("homepage", null);
578    }
579
580    /**
581     * Show a page previously registered with the CardLayout. If a component
582     * is provided and not yet added it will be registered under the given name.
583     *
584     * @param name registration name for the page
585     * @param comp optional component instance to add (may be null)
586     */
587    public static void showPage(final String name, final JComponent comp) {
588        CardLayout cl = (CardLayout) contentPanel.getLayout();
589        if (comp != null && comp.getParent() == null) {
590            contentPanel.add(comp, name);
591        }
592        cl.show(contentPanel, name);
593    }
594
595    /**
596     * Private constructor to prevent instantiation of this utility/entry class.
597     */
598    private Main() {
599        throw new AssertionError("Not instantiable");
600    }
601}