001package com.studentgui.apppages;
002
003import java.awt.BorderLayout;
004import java.awt.Font;
005import java.awt.GridBagConstraints;
006import java.awt.GridBagLayout;
007import java.awt.Insets;
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.sql.SQLException;
011import java.time.LocalDate;
012import java.util.LinkedHashMap;
013import java.util.Map;
014
015import javax.swing.JButton;
016import javax.swing.JLabel;
017import javax.swing.JPanel;
018import javax.swing.JScrollPane;
019import javax.swing.SwingUtilities;
020
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024import com.studentgui.uicomp.PhaseScoreField;
025
026/**
027 * HIMS BrailleSense productivity device proficiency assessment page.
028 *
029 * <p>Evaluates student competency with the HIMS BrailleSense family of refreshable braille
030 * notetakers (BrailleSense Polaris, BrailleSense 6, etc.) across 52 skills organized into
031 * 12 functional domains. The BrailleSense assessment structure mirrors {@link BrailleNote}
032 * to allow cross-device skill comparison.</p>
033 *
034 * <p><b>Device Family Context:</b> The BrailleSense is a portable braille notetaker with
035 * refreshable braille display, perkins-style keyboard, and integrated productivity software.
036 * It runs proprietary HIMS firmware and includes word processing, email, web browsing,
037 * media playback, and educational applications.</p>
038 *
039 * <p><b>Assessment Phases (12 domains, 52 skills):</b></p>
040 * <ul>
041 *   <li><b>Phase 1:</b> Device fundamentals (layout, setup, navigation, file management, core apps)</li>
042 *   <li><b>Phase 2:</b> Productivity suite (calendar, email, web, calculator, word processor)</li>
043 *   <li><b>Phase 3:</b> Advanced apps (presentations, code editor, third-party integration, braille I/O)</li>
044 *   <li><b>Phase 4:</b> Cloud integration and advanced file management</li>
045 *   <li><b>Phase 5:</b> Collaboration, export/import, printing, backup workflows</li>
046 *   <li><b>Phase 6:</b> App installation, updates, troubleshooting</li>
047 *   <li><b>Phase 7:</b> Automation (custom shortcuts, macros, scripting)</li>
048 *   <li><b>Phase 8:</b> Peripheral connectivity (Bluetooth, USB, displays, audio/video)</li>
049 *   <li><b>Phase 9:</b> Security, user accounts, parental controls, network settings</li>
050 *   <li><b>Phase 10:</b> Speech customization (TTS settings, voice profiles, languages)</li>
051 *   <li><b>Phase 11:</b> Device maintenance (firmware, diagnostics, logs, support, warranty)</li>
052 *   <li><b>Phase 12:</b> Community resources (online help, forums, feedback channels)</li>
053 * </ul>
054 *
055 * <p><b>Data Management and Report Generation:</b></p>
056 * <ul>
057 *   <li>Scores captured via {@link PhaseScoreField} components (integer 0–4 typical)</li>
058 *   <li>Persisted to normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
059 *   <li>JSON export: {@code StudentDataFiles/<student>/Sessions/BrailleSense/BrailleSense-<sessionId>-<timestamp>.json}</li>
060 *   <li>Phase-grouped time-series plots: {@code plots/BrailleSense-<sessionId>-<date>-P<N>.png} (12 phase groups)</li>
061 *   <li>Markdown and HTML reports with embedded plots and color-coded legends</li>
062 * </ul>
063 *
064 * <p>The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix.
065 * This page operates on static student/date parameters and does not implement listener interfaces.</p>
066 *
067 * @see com.studentgui.apphelpers.Database
068 * @see JLineGraph
069 * @see PhaseScoreField
070 * @see BrailleNote
071 */
072public class BrailleSense extends JPanel {
073    private static final Logger LOG = LoggerFactory.getLogger(BrailleSense.class);
074    /** Map of assessment part codes to their input components. */
075    private final Map<String, PhaseScoreField> inputs = new LinkedHashMap<>();
076    /** Canonical assessment parts for BrailleSense. */
077    private final String[][] parts;
078    /** Selected student display name (may be null). */
079    private final String studentNameParam;
080    /** Date associated with the current session. */
081    private final LocalDate dateParam;
082    /** Shared graph component used to visualize recent results. */
083    private final JLineGraph graph;
084
085    /**
086     * Create a BrailleSense page bound to the provided student and date.
087     *
088     * @param studentName selected student name (may be null until selection)
089     * @param date session date to associate with persisted progress rows
090     * @param graph shared graph component used to plot recent results
091     */
092    public BrailleSense(String studentName, LocalDate date, JLineGraph graph) {
093    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
094        this.dateParam = date;
095        this.graph = graph;
096        setLayout(new BorderLayout());
097
098        // create a data entry panel that mirrors BrailleNote's layout so alignment is identical
099        JPanel dataEntryPanel = new JPanel(new GridBagLayout());
100        JPanel view = new JPanel(new BorderLayout());
101        view.add(dataEntryPanel, BorderLayout.NORTH);
102        view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20, 20, 20, 20));
103        JScrollPane dataEntryScrollPane = new JScrollPane(view);
104        dataEntryScrollPane.setVerticalScrollBarPolicy(javax.swing.JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
105        dataEntryScrollPane.setHorizontalScrollBarPolicy(javax.swing.JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
106        dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleSense data entry scroll pane");
107
108        GridBagConstraints gbc = new GridBagConstraints();
109        gbc.insets = new Insets(2, 2, 2, 2);
110        gbc.fill = GridBagConstraints.HORIZONTAL;
111        gbc.weightx = 1.0;
112        gbc.weighty = 0.0;
113
114        JLabel titleLabel = new JLabel("BrailleSense Skills");
115        // Use an explicit font so theme changes don't alter the title appearance
116        titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 28));
117        gbc.gridx = 0;
118        gbc.gridy = 0;
119        gbc.gridwidth = GridBagConstraints.REMAINDER;
120        dataEntryPanel.add(titleLabel, gbc);
121
122        gbc.gridy = 1;
123        gbc.gridwidth = GridBagConstraints.REMAINDER;
124        gbc.ipady = 20;
125        dataEntryPanel.add(new JPanel(), gbc);
126
127        this.parts = new String[][]{
128                {"P1_1", "1.1 Physical Layout"}, {"P1_2", "1.2 Setup/Universal Commands"}, {"P1_3", "1.3 BNT+ Navigation"}, {"P1_4", "1.4 File Management"}, {"P1_5", "1.5 Word Processor"}, {"P1_6", "1.6 Email"}, {"P1_7", "1.7 Internet"}, {"P1_8", "1.8 Calculator"}, {"P1_9", "1.9 KeyMath"},
129                {"P2_1", "2.1 Calendar"}, {"P2_2", "2.2 KeyBRF"}, {"P2_3", "2.3 KeyFiles"}, {"P2_4", "2.4 KeyMail"}, {"P2_5", "2.5 KeyWeb"}, {"P2_6", "2.6 KeyCalc"}, {"P2_7", "2.7 KeyWord"},
130                {"P3_1", "3.1 KeySlides"}, {"P3_2", "3.2 KeyCode"}, {"P3_3", "3.3 Third Party Apps"}, {"P3_4", "3.4 Braille Input"}, {"P3_5", "3.5 Braille Output"}, {"P3_6", "3.6 Settings"}, {"P3_7", "3.7 Accessibility"},
131                {"P4_1", "4.1 Advanced File Management"}, {"P4_2", "4.2 Cloud Integration"}, {"P4_3", "4.3 Device Maintenance"},
132                {"P5_1", "5.1 Collaboration"}, {"P5_2", "5.2 Export/Import"}, {"P5_3", "5.3 Printing"}, {"P5_4", "5.4 Backup"},
133                {"P6_1", "6.1 App Installation"}, {"P6_2", "6.2 App Updates"}, {"P6_3", "6.3 Troubleshooting"},
134                {"P7_1", "7.1 Custom Shortcuts"}, {"P7_2", "7.2 Macros"}, {"P7_3", "7.3 Scripting"}, {"P7_4", "7.4 Automation"},
135                {"P8_1", "8.1 Bluetooth Devices"}, {"P8_2", "8.2 USB Devices"}, {"P8_3", "8.3 External Displays"}, {"P8_4", "8.4 Audio Output"}, {"P8_5", "8.5 Video Output"},
136                {"P9_1", "9.1 Security"}, {"P9_2", "9.2 User Accounts"}, {"P9_3", "9.3 Parental Controls"}, {"P9_4", "9.4 Network Settings"},
137                {"P10_1", "10.1 Speech Settings"}, {"P10_2", "10.2 Voice Profiles"}, {"P10_3", "10.3 Language Support"},
138                {"P11_1", "11.1 Firmware Updates"}, {"P11_2", "11.2 Diagnostics"}, {"P11_3", "11.3 Logs"}, {"P11_4", "11.4 Support"}, {"P11_5", "11.5 Warranty"},
139                {"P12_1", "12.1 Community Resources"}, {"P12_2", "12.2 Online Help"}, {"P12_3", "12.3 User Forums"}, {"P12_4", "12.4 Feedback"}
140        };
141
142        // compute pixel width using font metrics so labels align precisely
143        String[] labels = java.util.Arrays.stream(this.parts).map(x -> x[1]).toArray(String[]::new);
144        int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(com.studentgui.uicomp.PhaseScoreField.getLabelFont(), labels);
145        com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50)));
146        int row = 1;
147        for (String[] def : this.parts) {
148            gbc.gridx = 0;
149            gbc.gridy = row;
150            gbc.gridwidth = 2;
151            PhaseScoreField tf = new PhaseScoreField(def[1], 0);
152            tf.setName("braillesense_" + def[0]);
153            tf.getAccessibleContext().setAccessibleName(def[1]);
154            tf.setToolTipText("Enter score for " + def[1]);
155            dataEntryPanel.add(tf, gbc);
156            inputs.put(def[0], tf);
157            row++;
158        }
159
160        // Place Submit and Open Latest side-by-side to match IOS/ScreenReader styling
161        gbc.gridx = 0;
162        gbc.gridy = row;
163        gbc.gridwidth = 1;
164        gbc.anchor = GridBagConstraints.WEST;
165        JButton submit = new JButton("Submit Data");
166        submit.setPreferredSize(new java.awt.Dimension(0, 32));
167        submit.addActionListener((ActionEvent e) -> save());
168        submit.setMnemonic(KeyEvent.VK_S);
169        submit.setToolTipText("Save BrailleSense scores (Alt+S)");
170        submit.getAccessibleContext().setAccessibleName("Submit BrailleSense Data");
171        submit.setName("braillesense_submit");
172        dataEntryPanel.add(submit, gbc);
173
174        gbc.gridx = 1;
175        gbc.gridwidth = 1;
176        gbc.anchor = GridBagConstraints.WEST;
177        JButton openLatest = new JButton("Open Latest Plot");
178        openLatest.setPreferredSize(new java.awt.Dimension(0, 32));
179        openLatest.addActionListener((ActionEvent e) -> {
180            java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "BrailleSense");
181            if (p == null) {
182                com.studentgui.apphelpers.UiNotifier.show("No BrailleSense plot found for student");
183            } else {
184                try {
185                    java.awt.Desktop.getDesktop().open(p.toFile());
186                } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) {
187                    com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString());
188                }
189            }
190        });
191        dataEntryPanel.add(openLatest, gbc);
192
193        // Filler to consume remaining horizontal space
194        gbc.gridx = 2;
195        gbc.gridwidth = GridBagConstraints.REMAINDER;
196        gbc.anchor = GridBagConstraints.WEST;
197        dataEntryPanel.add(new JPanel(), gbc);
198
199        dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleSense data entry scroll pane");
200        add(dataEntryScrollPane, BorderLayout.CENTER);
201        add(graph, BorderLayout.SOUTH);
202        SwingUtilities.invokeLater(() -> {
203            dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize());
204            revalidate();
205        });
206        SwingUtilities.invokeLater(() -> {
207            for (var e : inputs.values()) {
208                LOG.debug("BrailleSense field {} labelWidth={} spinnerX={} gap={}", e.getLabel(), e.getLabelWrapWidth(), e.getSpinnerX(), e.getActualGap());
209            }
210        });
211        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
212        initParts();
213    }
214
215    /**
216     * Ensure the database contains the progress-type and assessment part rows
217     * for BrailleSense. Safe to call repeatedly.
218     */
219    private void initParts() {
220        try {
221            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleSense");
222            String[] codes = inputs.keySet().toArray(String[]::new);
223            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
224        } catch (SQLException ex) {
225            LOG.error("Error ensuring braillesense parts", ex);
226        }
227    }
228
229    /**
230     * Persist the current inputs as a new progress session for the selected
231     * student. Non-integer input is treated as zero.
232     */
233    private void save() {
234        try {
235            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam);
236            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleSense");
237            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam);
238            String[] codes = inputs.keySet().toArray(String[]::new);
239            int[] scores = new int[codes.length];
240            for (int i = 0; i < codes.length; i++) {
241                scores[i] = inputs.get(codes[i]).getValue();
242            }
243            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
244            LOG.info("BrailleSense data saved for {}", studentNameParam);
245            com.studentgui.apphelpers.UiNotifier.show("BrailleSense data saved.");
246            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
247            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "BrailleSense", payload, sessionId);
248            if (jsonOut == null) {
249                LOG.warn("Unable to save BrailleSense session JSON for sessionId={}", sessionId);
250            }
251            try {
252                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
253                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
254                java.nio.file.Files.createDirectories(plotsOut);
255                java.nio.file.Files.createDirectories(reportsOut);
256                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
257                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
258                String baseName = "BrailleSense-" + sessionId + "-" + dateStr;
259
260                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "BrailleSense", Integer.MAX_VALUE);
261                java.util.Map<String, java.nio.file.Path> groups = null;
262                String[] labels = java.util.Arrays.stream(this.parts).map(x -> x[1]).toArray(String[]::new);
263                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
264                    graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
265                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
266                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
267                    dateStr = headerDate.format(df);
268                } else {
269                    java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
270                    java.util.List<Integer> latest = new java.util.ArrayList<>();
271                    for (int i = 0; i < codes.length; i++) {
272                        latest.add(inputs.get(codes[i]).getValue());
273                    }
274                    rowsList.add(latest);
275                    graph.updateWithGroupedData(rowsList, codes);
276                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
277                }
278
279                if (groups == null) {
280                    groups = new java.util.LinkedHashMap<>();
281                }
282                StringBuilder md = new StringBuilder();
283                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
284                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
285                    md.append("## ").append(e.getKey()).append("\n\n");
286                    md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n");
287                }
288                // adjust markdown image links to point to the plots folder relative to reports
289                java.lang.String mdText = md.toString().replace("![](./", "![](../plots/");
290                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
291                java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8);
292
293                try {
294                    String[] palette = JLineGraph.PALETTE_HEX;
295                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
296                    for (int i = 0; i < codes.length; i++) {
297                        String code = codes[i];
298                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
299                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
300                    }
301                    StringBuilder html = new StringBuilder();
302                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
303                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
304                    html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
305                    html.append("</head><body>");
306                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
307                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
308                        String grp = e2.getKey();
309                        String imgName = e2.getValue().getFileName().toString();
310                        html.append("<h2>").append(grp).append("</h2>");
311                        html.append("<div class=\"plot\"><img src=\"../plots/").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
312                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
313                        html.append("<div class=\"legend\">");
314                        for (int s = 0; s < idxs.size(); s++) {
315                            int idx = idxs.get(s);
316                            String code = codes[idx];
317                            String human = this.parts[idx][1];
318                            String seriesName = code + " - " + human;
319                            String color = palette[s % palette.length];
320                            html.append("<div class=\"legend-item\">");
321                            html.append("<span class=\"swatch\" style=\"background:");
322                            html.append(color);
323                            html.append(";\"></span>");
324                            html.append("<div>");
325                            html.append(seriesName);
326                            html.append("</div></div>");
327                        }
328                        html.append("</div>");
329                    }
330                    html.append("</body></html>");
331                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
332                    java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
333                    LOG.info("Wrote BrailleSense HTML session report {}", htmlFile);
334                } catch (java.io.IOException ioex) {
335                    LOG.warn("Unable to write BrailleSense HTML report: {}", ioex.toString());
336                }
337            } catch (java.io.IOException ioe) {
338                LOG.warn("Unable to save BrailleSense per-phase plots or markdown report: {}", ioe.toString());
339            }
340        } catch (SQLException ex) {
341            LOG.error("Error saving braillesense data", ex);
342        }
343    }
344
345    // plotting is handled via submit/save which updates the shared graph and saves a static PNG
346}