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 * Cortical Visual Impairment (CVI) assessment page.
028 *
029 * <p>Provides a structured scoring interface for evaluating the 10 characteristic behaviors
030 * associated with Cortical Visual Impairment as defined in the Roman-Lanzi CVI Range assessment
031 * framework. Skills are organized into two functional clusters:</p>
032 *
033 * <ul>
034 *   <li><b>Phase 1 (P1_1–P1_6): Primary CVI Characteristics</b>
035 *     <ul>
036 *       <li><b>Color Preference:</b> Preference for high-saturation colors (red, yellow)</li>
037 *       <li><b>Need for Movement:</b> Improved visual attention with motion</li>
038 *       <li><b>Latency:</b> Delayed visual response times</li>
039 *       <li><b>Field Preference:</b> Asymmetric visual field usage patterns</li>
040 *       <li><b>Visual Complexity:</b> Difficulty with cluttered/busy visual environments</li>
041 *       <li><b>Nonpurposeful Gaze:</b> Reduced sustained visual fixation</li>
042 *     </ul>
043 *   </li>
044 *   <li><b>Phase 2 (P2_1–P2_4): Secondary/Environmental Characteristics</b>
045 *     <ul>
046 *       <li><b>Distance Viewing:</b> Reduced effectiveness at distance</li>
047 *       <li><b>Atypical Reflexes:</b> Blink-to-threat, light reflex variations</li>
048 *       <li><b>Visual Novelty:</b> Preference for familiar objects/environments</li>
049 *       <li><b>Visual Reach:</b> Difficulty localizing and reaching toward objects</li>
050 *     </ul>
051 *   </li>
052 * </ul>
053 *
054 * <p><b>Scoring and Interpretation:</b> Each characteristic is typically scored on a 0–10 scale
055 * representing frequency/severity of the behavior. Higher scores may indicate greater impact
056 * depending on the specific assessment protocol in use. Consult the Roman-Lanzi CVI Range manual
057 * for standardized scoring guidelines.</p>
058 *
059 * <p><b>Data Management:</b></p>
060 * <ul>
061 *   <li>Scores captured via {@link PhaseScoreField} components with integer validation</li>
062 *   <li>Submit button persists to database via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
063 *   <li>Session JSON exported to {@code StudentDataFiles/<student>/Sessions/CVI/CVI-<sessionId>-<timestamp>.json}</li>
064 *   <li>Time-series plots generated per phase group and saved to {@code plots/} directory</li>
065 *   <li>Markdown and HTML reports generated with embedded plots and color-coded legends</li>
066 * </ul>
067 *
068 * <p>The shared {@link JLineGraph} component visualizes trends across multiple sessions,
069 * grouped by phase to separate primary and secondary characteristics. This page does not
070 * implement listener interfaces as it operates on static student/date parameters.</p>
071 *
072 * @see com.studentgui.apphelpers.Database
073 * @see JLineGraph
074 * @see PhaseScoreField
075 */
076public class CVI extends JPanel {
077    private static final Logger LOG = LoggerFactory.getLogger(CVI.class);
078    /** Mapping of assessment part codes to their input components. */
079    private final Map<String, PhaseScoreField> inputs = new LinkedHashMap<>();
080
081    /** Selected student display name (may be null) used when saving or plotting. */
082    private final String studentNameParam;
083
084    /** Session date to associate with saved CVI progress entries. */
085    private final LocalDate dateParam;
086
087    /** Shared graph component used to visualize recent CVI results. */
088    private final JLineGraph graph;
089
090    /**
091     * Construct the CVI page bound to the selected student and session date.
092     *
093     * @param studentName selected student name (may be null)
094     * @param date session date to use when creating progress sessions
095     * @param graph shared graph used to visualize recent results
096     */
097    public CVI(String studentName, LocalDate date, JLineGraph graph) {
098    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
099        this.dateParam = date;
100        this.graph = graph;
101    setLayout(new BorderLayout());
102    JPanel panel = new JPanel(new GridBagLayout());
103    JPanel view = new JPanel(new BorderLayout());
104    view.add(panel, BorderLayout.NORTH);
105    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
106    JScrollPane scroll = new JScrollPane(view);
107    GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST;
108
109        JLabel title = new JLabel("CVI Progression");
110        title.setFont(title.getFont().deriveFont(Font.BOLD,28f));
111    title.getAccessibleContext().setAccessibleName("CVI Progression Title");
112    title.setHorizontalAlignment(JLabel.LEFT);
113    gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; panel.add(title, gbc);
114
115    String[][] parts = new String[][]{{"P1_1","Color Preference"},{"P1_2","Need for Movement"},{"P1_3","Latency"},{"P1_4","Field Preference"},{"P1_5","Visual Complexity"},{"P1_6","Nonpurposeful Gaze"},{"P2_1","Distance Viewing"},{"P2_2","Atypical Reflexes"},{"P2_3","Visual Novelty"},{"P2_4","Visual Reach"}};
116        String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new);
117            int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(com.studentgui.uicomp.PhaseScoreField.getLabelFont(), labels);
118            com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50)));
119    int row = 1;
120    for (String[] pdef: parts) {
121        gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2;
122        PhaseScoreField tf = new PhaseScoreField(pdef[1], 0);
123        tf.setToolTipText("Enter whole number score for " + pdef[1]);
124        tf.getAccessibleContext().setAccessibleName(pdef[1]);
125        tf.setName("cvi_" + pdef[0]);
126        panel.add(tf, gbc);
127        inputs.put(pdef[0], tf);
128        row++;
129    }
130
131    // Two side-by-side buttons: Submit Data (save + save PNG) and Open Latest Plot
132    gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST;
133    JButton submit = new JButton("Submit Data");
134    submit.setPreferredSize(new java.awt.Dimension(0, 32));
135    submit.addActionListener((ActionEvent e) -> save());
136    submit.setToolTipText("Save CVI assessment for selected student (Alt+S)");
137    submit.setMnemonic(KeyEvent.VK_S);
138    submit.getAccessibleContext().setAccessibleName("Submit CVI Data");
139    submit.setName("cvi_submit");
140    panel.add(submit, gbc);
141
142    gbc.gridx = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST;
143    JButton openLatest = new JButton("Open Latest Plot");
144    openLatest.setPreferredSize(new java.awt.Dimension(0, 32));
145    openLatest.addActionListener((ActionEvent e) -> {
146        java.nio.file.Path plotPath = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "CVI");
147        if (plotPath == null) com.studentgui.apphelpers.UiNotifier.show("No CVI plot found for student");
148        else {
149                // Do not auto-open CVI plot on startup; only save it. Opening is handled
150                // by explicit user actions (Open Latest Plot).
151        }
152    });
153    panel.add(openLatest, gbc);
154    gbc.gridwidth = 1;
155
156        add(scroll, BorderLayout.CENTER); add(graph, BorderLayout.SOUTH);
157        SwingUtilities.invokeLater(()->{ panel.setPreferredSize(panel.getPreferredSize()); revalidate(); });
158        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
159        initParts();
160    }
161
162    /**
163     * Ensure the CVI progress-type and part rows exist in the database.
164     */
165    private void initParts() {
166        try {
167            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("CVI");
168            java.util.Set<String> keys = inputs.keySet();
169            String[] codes = new String[keys.size()];
170            int kidx = 0;
171            for (String k : keys) {
172                codes[kidx++] = k;
173            }
174            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
175        } catch (SQLException ex) {
176            LOG.error("Error ensuring CVI parts", ex);
177        }
178    }
179
180    /**
181     * Validate inputs and persist them as a new CVI progress session for the
182     * selected student.
183     */
184    private void save() {
185        if (studentNameParam == null || studentNameParam.trim().isEmpty()) {
186            com.studentgui.apphelpers.UiNotifier.show("Please select a student before saving CVI data.");
187            return;
188        }
189
190        try {
191            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam);
192            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("CVI");
193            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam);
194            java.util.Set<String> keys = inputs.keySet();
195            String[] codes = new String[keys.size()];
196            int kidx = 0;
197            for (String k : keys) codes[kidx++] = k;
198            int[] scores = new int[codes.length];
199            for (int i = 0; i < codes.length; i++) {
200                scores[i] = inputs.get(codes[i]).getValue();
201            }
202            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
203            LOG.info("CVI data saved for {}", studentNameParam);
204            com.studentgui.apphelpers.UiNotifier.show("CVI data saved.");
205            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
206            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "CVI", payload, sessionId);
207            if (jsonOut == null) LOG.warn("Unable to save CVI session JSON for sessionId={}", sessionId);
208            try {
209                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
210                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
211                java.nio.file.Files.createDirectories(plotsOut);
212                java.nio.file.Files.createDirectories(reportsOut);
213                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
214                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
215                String baseName = "CVI-" + sessionId + "-" + dateStr;
216
217                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "CVI", Integer.MAX_VALUE);
218                java.util.Map<String, java.nio.file.Path> groups = null;
219                String[] labels = new String[codes.length];
220                for (int i = 0; i < codes.length; i++) labels[i] = inputs.get(codes[i]).getLabel();
221                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
222                    graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
223                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
224                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
225                    dateStr = headerDate.format(df);
226                } else {
227                    java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
228                    java.util.List<Integer> latest = new java.util.ArrayList<>();
229                    for (String c : codes) {
230                        latest.add(inputs.get(c).getValue());
231                    }
232                    rowsList.add(latest);
233                    graph.updateWithGroupedData(rowsList, codes);
234                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
235                }
236
237                if (groups == null) groups = new java.util.LinkedHashMap<>();
238                StringBuilder md = new StringBuilder();
239                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
240                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
241                    md.append("## ").append(e.getKey()).append("\n\n");
242                    md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n");
243                }
244                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
245                java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8);
246
247                try {
248                    String[] palette = JLineGraph.PALETTE_HEX;
249                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
250                    for (int i = 0; i < codes.length; i++) {
251                        String code = codes[i];
252                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
253                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
254                    }
255                    StringBuilder html = new StringBuilder();
256                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
257                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
258                    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>");
259                    html.append("</head><body>");
260                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
261                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
262                        String grp = e2.getKey();
263                        String imgName = e2.getValue().getFileName().toString();
264                        html.append("<h2>").append(grp).append("</h2>");
265                        html.append("<div class=\"plot\"><img src=\"../plots/").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
266                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
267                        html.append("<div class=\"legend\">");
268                        for (int s = 0; s < idxs.size(); s++) {
269                            int idx = idxs.get(s);
270                            String code = codes[idx];
271                            String human = labels[idx];
272                            String seriesName = code + " - " + human;
273                            String color = palette[s % palette.length];
274                            html.append("<div class=\"legend-item\">");
275                            html.append("<span class=\"swatch\" style=\"background:");
276                            html.append(color);
277                            html.append(";\"></span>");
278                            html.append("<div>");
279                            html.append(seriesName);
280                            html.append("</div></div>");
281                        }
282                        html.append("</div>");
283                    }
284                    html.append("</body></html>");
285                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
286                    java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
287                    LOG.info("Wrote CVI HTML session report {}", htmlFile);
288                } catch (java.io.IOException ioex) {
289                    LOG.warn("Unable to write CVI HTML report: {}", ioex.toString());
290                }
291            } catch (java.io.IOException ioe) {
292                LOG.warn("Unable to save CVI per-phase plots or markdown report: {}", ioe.toString());
293            }
294        } catch (SQLException ex) {
295            LOG.error("Error saving CVI data", ex);
296            com.studentgui.apphelpers.UiNotifier.show("Database error saving CVI data: " + ex.getMessage());
297        }
298    }
299
300    // Plotting is handled as part of save(): the submit action updates the shared
301    // graph and writes a static PNG into the student's plots folder.
302}