001package jmri.jmrix.loconet.swing.lncvprog;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.List;
006
007import javax.annotation.Nonnull;
008import javax.swing.table.AbstractTableModel;
009import javax.swing.table.TableColumn;
010import javax.swing.table.TableColumnModel;
011
012import jmri.InstanceManager;
013import jmri.Programmer;
014import jmri.jmrit.decoderdefn.DecoderFile;
015import jmri.jmrit.decoderdefn.DecoderIndexFile;
016import jmri.jmrit.roster.Roster;
017import jmri.jmrit.roster.RosterEntry;
018import jmri.jmrit.symbolicprog.tabbedframe.PaneOpsProgFrame;
019import jmri.jmrix.ProgrammingTool;
020import jmri.jmrix.loconet.LncvDevicesManager;
021import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
022import jmri.jmrix.loconet.uhlenbrock.LncvDevice;
023import jmri.util.swing.JmriJOptionPane;
024
025/**
026 * Table model for the programmed LNCV Modules table.
027 * See Sv2f Programing tool
028 *
029 * @author Egbert Broerse Copyright (C) 2020, 2025
030 */
031public class LncvProgTableModel extends AbstractTableModel implements PropertyChangeListener, ProgrammingTool {
032
033    public static final int COUNT_COLUMN = 0;
034    public static final int ARTICLE_COLUMN = 1;
035    public static final int MODADDR_COLUMN = 2;
036    public static final int CV_COLUMN = 3;
037    public static final int VALUE_COLUMN = 4;
038    public static final int DEVICENAME_COLUMN = 5;
039    public static final int ROSTERENTRY_COLUMN = 6;
040    public static final int OPENPRGMRBUTTON_COLUMN = 7;
041    static public final int NUMCOLUMNS = 8;
042    private final LncvProgPane parent;
043    private final transient LocoNetSystemConnectionMemo memo;
044    protected Roster _roster;
045    protected LncvDevicesManager lncvdm;
046
047    LncvProgTableModel(LncvProgPane parent, @Nonnull LocoNetSystemConnectionMemo memo) {
048        this.parent = parent;
049        this.memo = memo;
050        lncvdm = memo.getLncvDevicesManager();
051        _roster = Roster.getDefault();
052        lncvdm.addPropertyChangeListener(this);
053    }
054
055    public void initTable(javax.swing.JTable lncvModulesTable) {
056       TableColumnModel assignmentColumnModel = lncvModulesTable.getColumnModel();
057       TableColumn idColumn = assignmentColumnModel.getColumn(0);
058       idColumn.setMaxWidth(3);
059    }
060
061   @Override
062   public String getColumnName(int c) {
063       switch (c) {
064           case ARTICLE_COLUMN:
065               return Bundle.getMessage("HeadingArticle");
066           case MODADDR_COLUMN:
067               return Bundle.getMessage("HeadingAddress");
068           case CV_COLUMN:
069               return Bundle.getMessage("HeadingCvLastRead");
070           case VALUE_COLUMN:
071               return Bundle.getMessage("HeadingValue");
072           case DEVICENAME_COLUMN:
073               return Bundle.getMessage("HeadingDeviceModel");
074           case ROSTERENTRY_COLUMN:
075               return Bundle.getMessage("HeadingDeviceId");
076           case OPENPRGMRBUTTON_COLUMN:
077               return Bundle.getMessage("ButtonProgram");
078           case COUNT_COLUMN:
079           default:
080               return "#";
081       }
082   }
083
084   @Override
085   public Class<?> getColumnClass(int c) {
086       switch (c) {
087           case COUNT_COLUMN:
088           case ARTICLE_COLUMN:
089           case MODADDR_COLUMN:
090           case CV_COLUMN:
091           case VALUE_COLUMN:
092               return Integer.class;
093           case OPENPRGMRBUTTON_COLUMN:
094               return javax.swing.JButton.class;
095           case DEVICENAME_COLUMN:
096           case ROSTERENTRY_COLUMN:
097           default:
098               return String.class;
099       }
100   }
101
102   @Override
103   public boolean isCellEditable(int r, int c) {
104       return (c == OPENPRGMRBUTTON_COLUMN);
105   }
106
107   @Override
108   public int getColumnCount() {
109      return NUMCOLUMNS;
110   }
111
112   @Override
113   public int getRowCount() {
114        if (lncvdm == null) {
115            return 0;
116        } else {
117            return lncvdm.getDeviceCount();
118        }
119   }
120
121   @Override
122   public Object getValueAt(int r, int c) {
123       LncvDevice dev = memo.getLncvDevicesManager().getDeviceList().getDevice(r);
124       try {
125          switch (c) {
126              case ARTICLE_COLUMN:
127                  assert dev != null;
128                  return dev.getProductID();
129              case MODADDR_COLUMN:
130                  assert dev != null;
131                  return dev.getDestAddr();
132              case CV_COLUMN:
133                  assert dev != null;
134                  return dev.getCvNum();
135              case VALUE_COLUMN:
136                  assert dev != null;
137                  return dev.getCvValue();
138              case DEVICENAME_COLUMN:
139                  assert dev != null;
140                  if (dev.getDeviceName().isEmpty()) { // not yet filled in, look for a candidate
141                      List<DecoderFile> l =
142                          InstanceManager.getDefault(
143                              DecoderIndexFile.class).
144                              matchingDecoderList(
145                                      null,
146                                      null,
147                                      null,
148                                      null,
149                                      String.valueOf(dev.getProductID()), // a bit risky to check just 1 value
150                                      null,
151                                      null,
152                                      null,
153                                      null
154                              );
155                      //log.debug("found {} possible decoder matches for LNCV device", l.size());
156                      String lastModelName = "";
157                      if (!l.isEmpty()) {
158                          for (DecoderFile d : l) {
159                              // we do not check for LNCV programmingMode support
160                              // we do not expect replies from non-LNCV devices
161                              // TODO check using new access to getProgrammingModes() in the DecoderIndexFile
162                              if (d.getModel().isEmpty()) {
163                                  log.warn("Empty model(name) in decoderfile {}", d.getFileName());
164                                  continue;
165                              }
166                              lastModelName = d.getModel();
167                          }
168                          dev.setDevName(lastModelName);
169                          dev.setDecoderFile(l.get(l.size() - 1));
170                      }
171                      return lastModelName;
172                  }
173                  return dev.getDeviceName();
174              case ROSTERENTRY_COLUMN:
175                  assert dev != null;
176                  return dev.getRosterName();
177              case OPENPRGMRBUTTON_COLUMN:
178                  if (dev != null && !dev.getDeviceName().isEmpty()) {
179                      if ((dev.getRosterName() != null) && (dev.getRosterName().isEmpty())) {
180                          return Bundle.getMessage("ButtonCreateEntry");
181                      }
182                      if (dev.getDecoderFile().isProgrammingMode("LOCONETLNCVMODE")) {
183                          return Bundle.getMessage("ButtonProgram");
184                      } else {
185                          return Bundle.getMessage("ButtonWrongMode");
186                      }
187                  }
188                  return Bundle.getMessage("ButtonNoMatchInRoster");
189              default: // column 1
190                 return r + 1;
191          }
192      } catch (NullPointerException npe) {
193        log.warn("Caught NPE reading Module {}", r);
194        return "";
195      }
196   }
197
198    @Override
199    public void setValueAt(Object value, int r, int c) {
200        if (getRowCount() < r + 1) {
201            // prevent update of a row that does not (yet) exist
202            return;
203        }
204        LncvDevice dev = memo.getLncvDevicesManager().getDeviceList().getDevice(r);
205        if (c == OPENPRGMRBUTTON_COLUMN) {
206            if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonCreateEntry")) == 0) {
207                createRosterEntry(dev);
208                if (dev.getRosterEntry() != null) {
209                    setValueAt(dev.getRosterName(), r, c);
210                } else {
211                    log.warn("Failed to connect RosterEntry to device {}", dev.getRosterName());
212                }
213            } else if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonProgram")) == 0) {
214                openProgrammer(r);
215            } else if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonWrongMode")) == 0) {
216                infoNotForLncv(getValueAt(r, 1).toString()); // TODO once we check for LNCV progMode this can be removed
217            } else if (((String) getValueAt(r, c)).compareTo(Bundle.getMessage("ButtonNoMatchInRoster")) == 0){
218                // suggest to rebuild decoderIndex
219                warnRecreate(getValueAt(r, 1).toString());
220            }
221        } else {
222            // no change, so do not fire a property change event
223            return;
224        }
225        if (getRowCount() >= 1) {
226            this.fireTableRowsUpdated(r, r);
227        }
228    }
229
230    private void openProgrammer(int r) {
231        LncvDevice dev = memo.getLncvDevicesManager().getDeviceList().getDevice(r);
232
233        LncvDevicesManager.ProgrammingResult result = lncvdm.prepareForSymbolicProgrammer(dev, this);
234        switch (result) {
235            case SUCCESS_PROGRAMMER_OPENED:
236                return;
237            case FAIL_NO_SUCH_DEVICE:
238                JmriJOptionPane.showMessageDialog(parent,
239                        Bundle.getMessage("FAIL_NO_SUCH_DEVICE"),
240                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
241                return;
242            case FAIL_NO_APPROPRIATE_PROGRAMMER:
243                JmriJOptionPane.showMessageDialog(parent,
244                        Bundle.getMessage("FAIL_NO_APPROPRIATE_PROGRAMMER"),
245                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
246                return;
247            case FAIL_NO_MATCHING_ROSTER_ENTRY:
248                JmriJOptionPane.showMessageDialog(parent,
249                        Bundle.getMessage("FAIL_NO_MATCHING_ROSTER_ENTRY"),
250                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
251                return;
252            case FAIL_DESTINATION_ADDRESS_IS_ZERO:
253                JmriJOptionPane.showMessageDialog(parent,
254                        Bundle.getMessage("FAIL_DESTINATION_ADDRESS_IS_ZERO"),
255                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
256                return;
257            case FAIL_MULTIPLE_DEVICES_SAME_DESTINATION_ADDRESS:
258                JmriJOptionPane.showMessageDialog(parent,
259                        Bundle.getMessage("FAIL_MULTIPLE_DEVICES_SAME_DESTINATION_ADDRESS", dev.getDestAddr()),
260                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
261                return;
262            case FAIL_NO_ADDRESSED_PROGRAMMER:
263                JmriJOptionPane.showMessageDialog(parent,
264                        Bundle.getMessage("FAIL_NO_ADDRESSED_PROGRAMMER"),
265                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
266                return;
267            case FAIL_NO_LNCV_PROGRAMMER:
268                JmriJOptionPane.showMessageDialog(parent,
269                        Bundle.getMessage("FAIL_NO_LNCV_PROGRAMMER"),
270                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
271                return;
272            default:
273                JmriJOptionPane.showMessageDialog(parent,
274                        Bundle.getMessage("FAIL_UNKNOWN"),
275                        Bundle.getMessage("TitleOpenRosterEntry"), JmriJOptionPane.ERROR_MESSAGE);
276        }
277    }
278
279    /**
280     * {@inheritDoc}
281     */
282    @Override
283    public void openPaneOpsProgFrame(RosterEntry re, String name,
284                                     String programmerFile, Programmer p) {
285        // would be better if this was a new task on the GUI thread...
286        log.debug("attempting to open programmer, re={}, name={}, programmerFile={}, programmer={}",
287                re, name, programmerFile, p);
288
289        DecoderFile decoderFile = InstanceManager.getDefault(DecoderIndexFile.class).fileFromTitle(re.getDecoderModel());
290
291        PaneOpsProgFrame progFrame =
292                new PaneOpsProgFrame(decoderFile, re, name, programmerFile, p);
293
294        progFrame.pack();
295        progFrame.setVisible(true);
296    }
297
298    private void createRosterEntry(LncvDevice dev) {
299        if (dev.getDestAddr() == 0) {
300            JmriJOptionPane.showMessageDialog(parent,
301                    Bundle.getMessage("FAIL_ADD_ENTRY_0"),
302                    Bundle.getMessage("ButtonCreateEntry"), JmriJOptionPane.ERROR_MESSAGE);
303        } else {
304            String s = null;
305            while (s == null) {
306                s = JmriJOptionPane.showInputDialog(parent,
307                        Bundle.getMessage("DialogEnterEntryName"),
308                        Bundle.getMessage("EnterEntryNameTitle"),JmriJOptionPane.QUESTION_MESSAGE);
309                if (s == null) {
310                    // Cancel button hit
311                    return;
312                }
313            }
314
315            RosterEntry re = getRosterEntry(dev, s);
316            _roster.addEntry(re);
317            dev.setRosterEntry(re);
318        }
319    }
320
321    @Nonnull
322    private static RosterEntry getRosterEntry(LncvDevice dev, String s) {
323        RosterEntry re = new RosterEntry(dev.getDecoderFile().getFileName());
324        re.setDccAddress(Integer.toString(dev.getDestAddr()));
325        re.setDecoderModel(dev.getDecoderFile().getModel());
326        re.setProductID(Integer.toString(dev.getProductID()));
327        // add some details that won't be picked up otherwise from definition
328        re.setDecoderFamily(dev.getDecoderFile().getFileName());
329        re.setProgrammingModes(dev.getDecoderFile().getProgrammingModes());
330        re.setId(s);
331        return re;
332    }
333
334    private void warnRecreate(String address) {
335        // show dialog to inform and allow rebuilding index
336        Object[] dialogBoxButtonOptions = {
337                Bundle.getMessage("ButtonRecreateIndex"),
338                Bundle.getMessage("ButtonCancel")};
339        int userReply = JmriJOptionPane.showOptionDialog(parent,
340                Bundle.getMessage("DialogWarnRecreate", address),
341                Bundle.getMessage("TitleOpenRosterEntry"),
342                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE,
343                null, dialogBoxButtonOptions, dialogBoxButtonOptions[0]);
344        if (userReply == 0) { // array position 0
345            try {
346                DecoderIndexFile.forceCreationOfNewIndex(false); // faster
347            } catch (Exception exq) {
348                log.error("exception updating decoderIndexFile", exq);
349            }
350        }
351    }
352
353    /**
354     * Show dialog to inform that address matched decoder doesn't support LNSV1 mode.
355     */
356    private void infoNotForLncv(String address) {
357        Object[] dialogBoxButtonOptions = {
358                Bundle.getMessage("ButtonOK")};
359        JmriJOptionPane.showOptionDialog(parent,
360                Bundle.getMessage("DialogInfoMatchNotX", address, "LNCV"),
361                Bundle.getMessage("TitleOpenRosterEntry"),
362                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.INFORMATION_MESSAGE,
363                null, dialogBoxButtonOptions, dialogBoxButtonOptions[0]);
364    }
365
366    /*
367     * Process the "property change" events from LncvDevicesManager.
368     *
369     * @param evt event
370     */
371    @Override
372    public void propertyChange(PropertyChangeEvent evt) {
373        // these messages can arrive without a complete
374        // GUI, in which case we just ignore them
375        //String eventName = evt.getPropertyName();
376        /* always use fireTableDataChanged() because it does not always
377            resize columns to "preferred" widths!
378            This may slow things down, but that is a small price to pay!
379        */
380        fireTableDataChanged();
381    }
382
383    public void dispose() {
384        if ((memo != null) && (memo.getLncvDevicesManager() != null)) {
385            memo.getLncvDevicesManager().removePropertyChangeListener(this);
386        }
387    }
388
389    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LncvProgTableModel.class);
390
391}