001package jmri.jmrit.symbolicprog;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.Color;
006import java.awt.Component;
007import java.awt.event.ActionEvent;
008import java.awt.event.ActionListener;
009import java.awt.event.FocusEvent;
010import java.awt.event.FocusListener;
011import java.util.*;
012
013import javax.swing.tree.DefaultMutableTreeNode;
014import javax.swing.tree.TreePath;
015
016import javax.swing.ComboBoxModel;
017import javax.swing.JComboBox;
018import javax.swing.JLabel;
019import javax.swing.JScrollPane;
020import javax.swing.JTree;
021import javax.swing.event.TreeSelectionEvent;
022import javax.swing.event.TreeSelectionListener;
023import javax.swing.tree.DefaultTreeModel;
024import javax.swing.tree.DefaultTreeSelectionModel;
025
026import jmri.util.CvUtil;
027
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031/**
032 * Extends VariableValue to represent a variable split across multiple CVs with
033 * values from a pre-selected range each of which is associated with a text name
034 * (aka, a drop down)
035 * <br>
036 * The {@code mask} attribute represents the part of the value that's present in
037 * each CV; higher-order bits are loaded to subsequent CVs.<br>
038 * It is possible to assign a specific mask for each CV by providing a space
039 * separated list of masks, starting with the lowest, and matching the order of
040 * CVs
041 * <br><br>
042 * The original use was for addresses of stationary (accessory) decoders.
043 * <br>
044 * The original version only allowed two CVs, with the second CV specified by
045 * the attributes {@code highCV} and {@code upperMask}.
046 * <br><br>
047 * The preferred technique is now to specify all CVs in the {@code CV} attribute
048 * alone, as documented at {@link CvUtil#expandCvList expandCvList(String)}.
049 * <br><br>
050 * Optional attributes {@code factor} and {@code offset} are applied when going
051 * <i>from</i> the variable value <i>to</i> the CV values, or vice-versa:
052 * <pre>
053 * Value to put in CVs = ((value in text field) -{@code offset})/{@code factor}
054 * Value to put in text field = ((value in CVs) *{@code factor}) +{@code offset}
055 * </pre>
056 *
057 * @author Bob Jacobsen Copyright (C) 2002, 2003, 2004, 2013
058 * @author Dave Heap Copyright (C) 2016, 2019
059 * @author Egbert Broerse Copyright (C) 2020
060 * @author Jordan McBride Copyright (C) 2021
061 */
062public class SplitEnumVariableValue extends VariableValue
063        implements ActionListener, FocusListener {
064
065    private static final int RETRY_COUNT = 2;
066
067    int atest = 1;
068    private final List<JTree> trees = new ArrayList<>();
069
070    private final List<ComboCheckBox> comboCBs = new ArrayList<>();
071    private final List<SplitEnumVariableValue.VarComboBox> comboVars = new ArrayList<>();
072    private final List<ComboRadioButtons> comboRBs = new ArrayList<>();
073
074
075    public SplitEnumVariableValue(String name, String comment, String cvName,
076            boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly,
077            String cvNum, String mask, int minVal, int maxVal,
078            HashMap<String, CvValue> v, JLabel status, String stdname,
079            String pSecondCV, int pFactor, int pOffset, String uppermask, String extra1, String extra2, String extra3, String extra4) {
080        super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, v, status, stdname);
081        _minVal = 0;
082        _maxVal = ~0;
083        stepOneActions(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, minVal, maxVal, v, status, stdname, pSecondCV, pFactor, pOffset, uppermask, extra1, extra2, extra3, extra4);
084        _name = name;
085        _mask = mask; // will be converted to MaskArray to apply separate mask for each CV
086        if (mask != null && mask.contains(" ")) {
087            _maskArray = mask.split(" "); // type accepts multiple masks for SplitVariableValue
088        } else {
089            _maskArray = new String[1];
090            _maskArray[0] = mask;
091        }
092        _cvNum = cvNum;
093        mFactor = pFactor;
094        mOffset = pOffset;
095        // legacy format variables
096        mSecondCV = pSecondCV;
097        _uppermask = uppermask;
098
099
100        log.debug("Variable={};comment={};cvName={};cvNum={};stdname={}", _name, comment, cvName, _cvNum, stdname);
101
102        // upper bit offset includes lower bit offset, and MSB bits missing from upper part
103        log.debug("Variable={}; upper mask {} had offsetVal={} so upperbitoffset={}", _name, _uppermask, offsetVal(_uppermask), offsetVal(_uppermask));
104
105        // set up array of used CVs
106        cvList = new ArrayList<>();
107
108        List<String> nameList = CvUtil.expandCvList(_cvNum); // see if cvName needs expanding
109        if (nameList.isEmpty()) {
110            // primary CV
111            String tMask;
112            if (_maskArray != null && _maskArray.length == 1) {
113                log.debug("PrimaryCV mask={}", _maskArray[0]);
114                tMask = _maskArray[0];
115            } else {
116                tMask = _mask; // mask supplied could be an empty string
117            }
118            cvList.add(new CvItem(_cvNum, tMask));
119
120            if (pSecondCV != null && !pSecondCV.equals("")) {
121                cvList.add(new CvItem(pSecondCV, _uppermask));
122            }
123        } else {
124            for (int i = 0; i < nameList.size(); i++) {
125                cvList.add(new CvItem(nameList.get(i), _maskArray[Math.min(i, _maskArray.length - 1)]));
126                // use last mask for all following CVs if fewer masks than the number of CVs listed were provided
127                log.debug("Added mask #{}: {}", i, _maskArray[Math.min(i, _maskArray.length - 1)]);
128            }
129        }
130
131        cvCount = cvList.size();
132
133        for (int i = 0; i < cvCount; i++) {
134            cvList.get(i).startOffset = currentOffset;
135            String t = cvList.get(i).cvMask;
136            if (t.contains("V")) {
137                currentOffset = currentOffset + t.lastIndexOf("V") - t.indexOf("V") + 1;
138            } else {
139                log.error("Variable={};cvName={};cvMask={} is an invalid bitmask", _name, cvList.get(i).cvName, cvList.get(i).cvMask);
140            }
141            log.debug("Variable={};cvName={};cvMask={};startOffset={};currentOffset={}", _name, cvList.get(i).cvName, cvList.get(i).cvMask, cvList.get(i).startOffset, currentOffset);
142
143            // connect CV for notification
144            CvValue cv = _cvMap.get(cvList.get(i).cvName);
145            cvList.get(i).thisCV = cv;
146        }
147
148        stepTwoActions();
149
150
151        // have to do when list is complete
152        for (int i = 0; i < cvCount; i++) {
153            cvList.get(i).thisCV.addPropertyChangeListener(this);
154            cvList.get(i).thisCV.setState(ValueState.FROMFILE);
155        }
156        treeNodes.addLast(new DefaultMutableTreeNode(""));
157    }
158
159    /**
160     * Subclasses can override this to pick up constructor-specific attributes
161     * and perform other actions before cvList has been built.
162     *
163     * @param name      name.
164     * @param comment   comment.
165     * @param cvName    cv name.
166     * @param readOnly  true for read only, else false.
167     * @param infoOnly  true for info only, else false.
168     * @param writeOnly true for write only, else false.
169     * @param opsOnly   true for ops only, else false.
170     * @param cvNum     cv number.
171     * @param mask      cv mask.
172     * @param minVal    minimum value.
173     * @param maxVal    maximum value.
174     * @param v         hashmap of string and cv value.
175     * @param status    status.
176     * @param stdname   std name.
177     * @param pSecondCV second cv (no longer preferred, specify in cv)
178     * @param pFactor   factor.
179     * @param pOffset   offset.
180     * @param uppermask upper mask (no longer preferred, specify in mask)
181     * @param extra1    extra 1.
182     * @param extra2    extra 2.
183     * @param extra3    extra 3.
184     * @param extra4    extra 4.
185     */
186    public void stepOneActions(String name, String comment, String cvName,
187            boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly,
188            String cvNum, String mask, int minVal, int maxVal,
189            HashMap<String, CvValue> v, JLabel status, String stdname,
190            String pSecondCV, int pFactor, int pOffset, String uppermask, String extra1, String extra2, String extra3, String extra4) {
191        if (extra3 != null) {
192            _minVal = getValueFromText(extra3);
193        }
194        if (extra4 != null) {
195            _maxVal = getValueFromText(extra4);
196        }
197    }
198
199    public void nItems(int n) {
200        _itemArray = new String[n];
201        _pathArray = new TreePath[n];
202        _valueArray = new int[n];
203        _nstored = 0;
204        log.debug("enumeration arrays size={}", n);
205    }
206
207        /**
208     * Create a new item in the enumeration, with an associated value one more
209     * than the last item (or zero if this is the first one added)
210     *
211     * @param s Name of the enumeration item
212     */
213    public void addItem(String s) {
214        if (_nstored == 0) {
215            addItem(s, 0);
216        } else {
217            addItem(s, _valueArray[_nstored - 1] + 1);
218        }
219    }
220
221    public void addItem(String s, int value) {
222        _valueArray[_nstored] = value;
223        SplitEnumVariableValue.TreeLeafNode node = new SplitEnumVariableValue.TreeLeafNode(s, _nstored);
224        treeNodes.getLast().add(node);
225        _pathArray[_nstored] = new TreePath(node.getPath());
226        _itemArray[_nstored++] = s;
227        log.debug("_itemArray.length={},_nstored={},s='{}',value={}", _itemArray.length, _nstored, s, value);
228    }
229
230    public void startGroup(String name) {
231        DefaultMutableTreeNode next = new DefaultMutableTreeNode(name);
232        treeNodes.getLast().add(next);
233        treeNodes.addLast(next);
234    }
235
236    public void endGroup() {
237        treeNodes.removeLast();
238    }
239
240    public void lastItem() {
241        _value = new JComboBox<>(java.util.Arrays.copyOf(_itemArray, _nstored));
242        _value.getAccessibleContext().setAccessibleName(label());
243
244        // finish initialization
245        _value.setActionCommand("");
246        _defaultColor = _value.getBackground();
247        _value.setBackground(ValueState.UNKNOWN.getColor());
248        _value.setOpaque(true);
249        // connect to the JComboBox model and the CV so we'll see changes.
250        _value.addActionListener(this);
251        CvValue cv1 = cvList.get(0).thisCV;
252        CvValue cv2 = cvList.get(1).thisCV;
253        if (cv1 == null || cv2 == null) {
254            log.error("no CV defined in enumVal {}, skipping setState", getCvName());
255            return;
256        }
257        cv1.addPropertyChangeListener(this);
258        cv1.setState(ValueState.FROMFILE);
259        cv2.addPropertyChangeListener(this);
260        cv2.setState(ValueState.FROMFILE);
261    }
262
263
264
265    @Override
266    public void setToolTipText(String t) {
267        super.setToolTipText(t);   // do default stuff
268        _value.setToolTipText(t);  // set our value
269    }
270        // stored value
271    JComboBox<String> _value = null;
272
273    // place to keep the items & associated numbers
274    private String[] _itemArray = null;
275    private TreePath[] _pathArray = null;
276    private int[] _valueArray = null;
277    private int _nstored;
278
279    Deque<DefaultMutableTreeNode> treeNodes = new ArrayDeque<>();
280
281    /**
282     * Subclasses can override this to invoke further actions after cvList has
283     * been built.
284     */
285    public void stepTwoActions() {
286        if (currentOffset > bitCount) {
287            String eol = System.getProperty("line.separator");
288            throw new Error(
289                    "Decoder File parsing error:"
290                    + eol + "The Decoder Definition File specified \"" + _cvNum
291                    + "\" for variable \"" + _name + "\". This expands to:"
292                    + eol + "\"" + getCvDescription() + "\""
293                    + eol + "This requires " + currentOffset + " bits, which exceeds the " + bitCount
294                    + " bit capacity of the long integer used to store the variable."
295                    + eol + "The Decoder Definition File needs correction.");
296        }
297        _columns = cvCount * 2; //update column width now we have a better idea
298    }
299
300    @Override
301    public void setAvailable(boolean a) {
302        _value.setVisible(a);
303        for (ComboCheckBox c : comboCBs) {
304            c.setVisible(a);
305        }
306        for (SplitEnumVariableValue.VarComboBox c : comboVars) {
307            c.setVisible(a);
308        }
309        for (ComboRadioButtons c : comboRBs) {
310            c.setVisible(a);
311        }
312        super.setAvailable(a);
313    }
314
315    /**
316     * Simple request getter for the CVs composing this variable
317     * <br>
318     * @return Array of CvValue for all of associated CVs
319     */
320    @Override
321    public CvValue[] usesCVs() {
322        CvValue[] theseCvs = new CvValue[cvCount];
323        for (int i = 0; i < cvCount; i++) {
324            theseCvs[i] = cvList.get(i).thisCV;
325        }
326        return theseCvs;
327    }
328
329    /**
330     * Multiple masks can be defined for the CVs accessed by this variable.
331     * <br>
332     * Actual individual masks are returned in
333     * {@link #getCvDescription getCvDescription()}.
334     *
335     * @return The legacy two-CV mask if {@code highCV} is specified.
336     * <br>
337     * The {@code mask} if {@code highCV} is not specified.
338     */
339    @Override
340    public String getMask() {
341        if (mSecondCV != null && !mSecondCV.equals("")) {
342            return _uppermask + _mask;
343        } else {
344            return _mask; // a list of 1-n masks, separated by spaces
345        }
346    }
347
348    /**
349     * Access a specific mask, used in tests
350     *
351     * @param i index of CV in variable
352     * @return a single mask as string in the form XXXXVVVV, or empty string if
353     *         index out of bounds
354     */
355    protected String getMask(int i) {
356        if (i < cvCount) {
357            return cvList.get(i).cvMask;
358        }
359        return "";
360    }
361
362    /**
363     * Provide a user-readable description of the CVs accessed by this variable.
364     * <br>
365     * Actual individual masks are added to CVs if more are present.
366     *
367     * @return A user-friendly CV(s) and bitmask(s) description
368     */
369    @Override
370    public String getCvDescription() {
371        StringBuilder buf = new StringBuilder();
372        for (int i = 0; i < cvCount; i++) {
373            if (buf.length() > 0) {
374                buf.append(" & ");
375            }
376            buf.append("CV");
377            buf.append(cvList.get(i).cvName);
378            String temp = CvUtil.getMaskDescription(cvList.get(i).cvMask);
379            if (temp.length() > 0) {
380                buf.append(" ");
381                buf.append(temp);
382            }
383        }
384        buf.append("."); // mark that mask descriptions are already inserted for CvUtil.addCvDescription
385        return buf.toString();
386    }
387
388    String mSecondCV;
389    String _uppermask;
390    int mFactor;
391    int mOffset;
392    String _name;
393    String _mask; // full string as provided, use _maskArray to access one of multiple masks
394    String[] _maskArray = new String[0];
395    String _cvNum;
396
397    List<CvItem> cvList;
398
399    int cvCount = 0;
400    int currentOffset = 0;
401
402    /**
403     * Get the first CV from the set used to define this variable
404     * <br>
405     * @return The legacy two-CV mask if {@code highCV} is specified.
406     */
407    @Override
408    public String getCvNum() {
409        String retString = "";
410        if (cvCount > 0) {
411            retString = cvList.get(0).cvName;
412        }
413        return retString;
414    }
415
416    long _minVal;
417    long _maxVal;
418
419    @Override
420    public Object rangeVal() {
421        return "Split value";
422    }
423
424    String oldContents = "0";
425
426    long getValueFromText(String s) {
427        return (Long.parseUnsignedLong(s));
428    }
429
430    String getTextFromValue(long v) {
431        return (Long.toUnsignedString(v));
432    }
433
434    /**
435     * Contains numeric-value specific code.
436     * <br><br>
437     * Calculates new value for _enumField and invokes
438     * {@link #setLongValue(long) setLongValue(newVal)} to make and notify the
439     * change
440     *
441     * @param intVals array of new CV values
442     */
443    void updateVariableValue(int[] intVals) {
444        if (intVals.length > 0){
445            long newVal = 0;
446            for (int i = 0; i < intVals.length; i++) {
447                newVal = newVal | (((long) intVals[i]) << cvList.get(i).startOffset);
448                log.debug("Variable={}; i={}; intVals={}; startOffset={}; newVal={}",
449                    _name, i, intVals[i], cvList.get(i).startOffset, getTextFromValue(newVal));
450            }
451            log.debug("Variable={}; set value to {}", _name, newVal);
452            setLongValue(newVal);  // check for duplicate is done inside setLongValue
453            log.debug("Variable={}; in property change after setValue call", _name);
454        }
455    }
456
457    /**
458     * Saves selected item from _value (enumField) to oldContents.
459     */
460    void enterField() {
461        oldContents =  String.valueOf(_value.getSelectedItem());
462        log.debug("enterField sets oldContents to {}", oldContents);
463    }
464
465    /**
466     * Contains numeric-value specific code.
467     * <br>
468     * firePropertyChange for "Value" with new and old contents of _enumField
469     */
470    void exitField(){
471        // there may be a lost focus event left in the queue when disposed so protect
472        log.trace("exitField starts");
473        if (_value != null && !oldContents.equals(_value.getSelectedItem())) {
474            long newFieldVal = 0;
475            try {
476                newFieldVal = Long.parseLong((String) Objects.requireNonNull(_value.getSelectedItem()));
477            } catch (NumberFormatException e) {
478                //_value.setText(oldContents);
479            }
480            log.debug("_minVal={};_maxVal={};newFieldVal={}",
481                    Long.toUnsignedString(_minVal), Long.toUnsignedString(_maxVal), Long.toUnsignedString(newFieldVal));
482            if (Long.compareUnsigned(newFieldVal, _minVal) < 0 || Long.compareUnsigned(newFieldVal, _maxVal) > 0) {
483
484            } else {
485                long newVal = (newFieldVal - mOffset) / mFactor;
486                long oldVal = (getValueFromText(oldContents) - mOffset) / mFactor;
487                prop.firePropertyChange("Value", oldVal, newVal);
488            }
489        }
490        log.trace("exitField ends");
491    }
492
493    boolean _fieldShrink = false;
494
495    void updatedDropDown() {
496        log.debug("Variable='{}'; enter updatedDropDown in {} with DropDownValue='{}'", _name, (this.getClass().getSimpleName()), _value.getSelectedIndex());
497        // called for new values in text field - set the CVs as needed
498
499        int[] retVals = getCvValsFromSingleInt(getIntValue());
500
501        // combine with existing values via mask
502        for (int j = 0; j < cvCount; j++) {
503            int i = j;
504            log.debug("retVals[{}]={};cvList.get({}).cvMask{};offsetVal={}", i, retVals[i], i, cvList.get(i).cvMask, offsetVal(cvList.get(i).cvMask));
505            int cvMask = maskValAsInt(cvList.get(i).cvMask);
506            CvValue thisCV = cvList.get(i).thisCV;
507            int oldCvVal = thisCV.getValue();
508            int newCvVal = (oldCvVal & ~cvMask)
509                    | ((retVals[i] << offsetVal(cvList.get(i).cvMask)) & cvMask);
510            log.debug("{};cvMask={};oldCvVal={};retVals[{}]={};newCvVal={}", cvList.get(i).cvName, cvMask, oldCvVal, i, retVals[i], newCvVal);
511
512            // cv updates here trigger updated property changes, which means
513            // we're going to get notified sooner or later.
514            if (newCvVal != oldCvVal) {
515                thisCV.setValue(newCvVal);
516            }
517        }
518        log.debug("Variable={}; exit updatedDropDown", _name);
519    }
520
521    int[] getCvValsFromSingleInt(long newEntry) {
522        // calculate resulting number
523        long newVal = (newEntry - mOffset) / mFactor;
524        log.debug("getCvValsFromSingleInt Variable={};newEntry={};newVal={} with Offset={} + Factor={} applied", _name, newEntry, newVal, mOffset, mFactor);
525
526        int[] retVals = new int[cvCount];
527
528        // extract individual values via masks
529        for (int i = 0; i < cvCount; i++) {
530            log.trace("      Starting with newVal={} startOffset={} mask={} offsetVal={}",
531                        newVal, cvList.get(i).startOffset, maskValAsInt(cvList.get(i).cvMask), offsetVal(cvList.get(i).cvMask));
532            retVals[i] = (((int) (newVal >>> cvList.get(i).startOffset))
533                    & (maskValAsInt(cvList.get(i).cvMask) >>> offsetVal(cvList.get(i).cvMask)));
534            log.trace("      Calculated {} entry is {}", i, retVals[i]);
535        }
536        return retVals;
537    }
538
539    /**
540     * ActionListener implementation. Called by new selection in the JComboBox representation.
541     * <p>
542     * Invokes {@link #exitField exitField()}
543     *
544     * @param e the action event
545     */
546    @Override
547    public void actionPerformed(ActionEvent e) {
548        // see if this is from _value itself, or from an alternate rep.
549        // if from an alternate rep, it will contain the value to select
550        if (e != null){
551            if (log.isDebugEnabled()) {
552                log.debug("Variable = {} start action event cmd={}", label(), e.getActionCommand());
553            }
554            if (!(e.getActionCommand().equals(""))) {
555                // is from alternate rep
556                log.debug("{} action event {} was from alternate rep", label(), e.getActionCommand());
557                _value.setSelectedItem(e.getActionCommand());
558
559                // match and select in tree
560                if (_nstored > 0) {
561                    for (int i = 0; i < _nstored; i++) {
562                        if (e.getActionCommand().equals(_itemArray[i])) {
563                            // now select in the tree
564                            TreePath path = _pathArray[i];
565                            for (JTree tree : trees) {
566                                tree.setSelectionPath(path);
567                                // ensure selection is in visible portion of JScrollPane
568                                tree.scrollPathToVisible(path);
569                            }
570                            break; // first one is enough
571                        }
572                    }
573                }
574            }
575
576            // called for new values - set the CV as needed
577            CvValue cv = _cvMap.get(getCvNum());
578            if (cv == null) {
579                log.error("no CV defined in enumVal {}, skipping setValue", _cvMap.get(getCvName()));
580                return;
581            }
582
583            updatedDropDown();
584
585        }
586        exitField();
587    }
588
589    /**
590     * FocusListener implementations.
591     */
592    @Override
593    public void focusGained(FocusEvent e) {
594        log.debug("Variable={}; focusGained", _name);
595        enterField();
596    }
597
598    @Override
599    public void focusLost(FocusEvent e) {
600        log.debug("Variable={}; focusLost", _name);
601        exitField();
602    }
603
604    // to complete this class, fill in the routines to handle "Value" parameter
605    // and to read/write/hear parameter changes.
606    @Override
607    public String getValueString() {
608        return Integer.toString(getIntValue());
609    }
610
611    /**
612     * Set value from a String value.
613     *
614     * @param value a string representing the Long value to be set
615     */
616    public void setValue(int value) {
617        if(value > 0){
618            try {
619                long longVal = value;
620                long val = longVal;
621                setLongValue(val);
622            } catch (NumberFormatException e) {
623                log.warn("skipping set of non-long value \"{}\"", value);
624            }
625            selectValue(value);
626        }
627    }
628
629    @Override
630    public void setIntValue(int i) {
631        setLongValue(i);
632    }
633
634    @Override
635    public int getIntValue() {
636        if (_value.getSelectedIndex() >= _valueArray.length || _value.getSelectedIndex() < 0) {
637            log.error("trying to get value {} too large for array length {} in var {}", _value.getSelectedIndex(), _valueArray.length, label());
638        }
639        log.debug("SelectedIndex={} value={}", _value.getSelectedIndex(), _valueArray[_value.getSelectedIndex()]);
640        return _valueArray[_value.getSelectedIndex()];
641    }
642
643    /**
644     * Get the value as an unsigned long.
645     *
646     * @return the value as a long
647     */
648    @Override
649    public long getLongValue() {
650        return _valueArray[_value.getSelectedIndex()];
651    }
652
653    @Override
654    public String getTextValue() {
655        if (_value.getSelectedItem() != null) {
656            return _value.getSelectedItem().toString();
657        } else {
658            return "";
659        }
660    }
661
662    @Override
663    public Object getValueObject() {
664        return getLongValue();
665    }
666
667    @Override
668    public Component getCommonRep() {
669        if (getReadOnly()) {
670            JLabel r = new JLabel((String)_value.getSelectedItem());
671            updateRepresentation(r);
672            return r;
673        } else {
674            return _value;
675        }
676    }
677
678    private void addReservedEntry(long value) {
679        log.warn("Variable \"{}\" had to add reserved entry for {}", _name, value);
680        // We can be commanded to a number that hasn't been defined.
681        // But that's OK for certain applications.
682        // When this happens, we add enum values as needed
683        log.debug("Create new item with value {} count was {} in {}", value, _value.getItemCount(), label());
684
685        // lengthen arrays
686        _valueArray = java.util.Arrays.copyOf(_valueArray, _valueArray.length + 1);
687
688        _itemArray = java.util.Arrays.copyOf(_itemArray, _itemArray.length + 1);
689
690        _pathArray = java.util.Arrays.copyOf(_pathArray, _pathArray.length + 1);
691
692        addItem("Reserved value " + value, (int)value);
693
694        // update the JComboBox
695        _value.addItem(_itemArray[_nstored - 1]);
696        _value.setSelectedItem(_itemArray[_nstored - 1]);
697
698        // tell trees to redisplay & select
699        for (JTree tree : trees) {
700            ((DefaultTreeModel) tree.getModel()).reload();
701            tree.setSelectionPath(_pathArray[_nstored - 1]);
702            // ensure selection is in visible portion of JScrollPane
703            tree.scrollPathToVisible(_pathArray[_nstored - 1]);
704        }
705    }
706
707    public void setLongValue(long value) {
708        log.debug("Variable={}; enter setLongValue {}", _name, value);
709        long oldVal;
710        try {
711            oldVal = (Long.parseLong((String)_value.getSelectedItem()) - mOffset) / mFactor;
712        } catch (java.lang.NumberFormatException ex) {
713            oldVal = -999;
714        }
715        log.debug("Variable={}; setValue with new value {} old value {}", _name, value, oldVal);
716
717        int lengthOfArray = this._valueArray.length;
718
719        boolean foundIt = false; // did we find entry? If not, have to add one
720        for (int i = 0; i < lengthOfArray; i++) {
721          if (this._valueArray[i] == value){
722              log.trace("{} setLongValue setSelectedIndex to {}", _name, i);
723              _value.setSelectedIndex(i);
724              foundIt = true;
725          }
726        }
727        if (!foundIt) {
728            addReservedEntry(value);
729        }
730
731        if (oldVal != value || getState() == ValueState.UNKNOWN) {
732            actionPerformed(null);
733        }
734        // TODO PENDING: the code used to fire value * mFactor + mOffset, which is a text representation;
735        // but 'oldValue' was converted back using mOffset / mFactor making those two (new / old)
736        // using different scales. Probably a bug, but it has been there from well before
737        // the extended splitVal. Because of the risk of breaking existing
738        // behaviour somewhere, deferring correction until at least the next test release.
739        prop.firePropertyChange("Value", oldVal, value * mFactor + mOffset);
740        log.debug("Variable={}; exit setLongValue old={} new={}", _name, oldVal, value);
741    }
742
743    Color _defaultColor;
744
745    // implement an abstract member to set colors
746    @Override
747    void setColor(Color c) {
748        if (c != null && _value != null) {
749            _value.setBackground(c);
750            log.debug("Variable={}; Set Color to {}", _name, c.toString());
751        } else if (_value != null) {
752            log.debug("Variable={}; Set Color to defaultColor {}", _name, _defaultColor.toString());
753            _value.setBackground(_defaultColor);
754        }
755
756        // prop.firePropertyChange("Value", null, null);
757    }
758
759    int _columns = 1;
760
761
762       @Override
763    public Component getNewRep(String format) {
764        // sort on format type
765        switch (format) {
766            case "tree":
767                DefaultTreeModel dModel = new DefaultTreeModel(treeNodes.getFirst());
768                JTree dTree = new JTree(dModel);
769                trees.add(dTree);
770                JScrollPane dScroll = new JScrollPane(dTree);
771                dTree.setRootVisible(false);
772                dTree.setShowsRootHandles(true);
773                dTree.setScrollsOnExpand(true);
774                dTree.setExpandsSelectedPaths(true);
775                dTree.getSelectionModel().setSelectionMode(DefaultTreeSelectionModel.SINGLE_TREE_SELECTION);
776                // arrange for only leaf nodes can be selected
777                dTree.addTreeSelectionListener(new TreeSelectionListener() {
778                    @Override
779                    public void valueChanged(TreeSelectionEvent e) {
780                        TreePath[] paths = e.getPaths();
781                        for (TreePath path : paths) {
782                            DefaultMutableTreeNode o = (DefaultMutableTreeNode) path.getLastPathComponent();
783                            if (o.getChildCount() > 0) {
784                                ((JTree) e.getSource()).removeSelectionPath(path);
785                            }
786                        }
787                        // now record selection
788                        if (paths.length >= 1) {
789                            if (paths[0].getLastPathComponent() instanceof SplitEnumVariableValue.TreeLeafNode) {
790                                // update value of Variable
791                                setValue(_valueArray[((SplitEnumVariableValue.TreeLeafNode) paths[0].getLastPathComponent()).index]);
792                            }
793                        }
794                    }
795                });
796                // select initial value
797                TreePath path = _pathArray[_value.getSelectedIndex()];
798                dTree.setSelectionPath(path);
799                // ensure selection is in visible portion of JScrollPane
800                dTree.scrollPathToVisible(path);
801
802                if (getReadOnly() || getInfoOnly()) {
803                    log.error("read only variables cannot use tree format: {}", item());
804                }
805                updateRepresentation(dScroll);
806                return dScroll;
807            default: {
808                // return a new JComboBox representing the same model
809                SplitEnumVariableValue.VarComboBox b = new SplitEnumVariableValue.VarComboBox(_value.getModel(), this);
810                comboVars.add(b);
811                if (getReadOnly() || getInfoOnly()) {
812                    b.setEnabled(false);
813                }
814                updateRepresentation(b);
815                return b;
816            }
817        }
818    }
819
820    /**
821     * Select a specific value in the JComboBox display
822     * or, if need be, create another one
823     * @param value The new numerical value for the complete enum variable.
824     */
825    protected void selectValue(int value) {
826        if (_nstored > 0 && value != 0) {
827            for (int i = 0; i < _nstored; i++) {
828                if (_valueArray[i] == value) {
829                    //found it, select it
830                    log.debug("{}: selectValue sets to {}", _name, i);
831                    _value.setSelectedIndex(i);
832
833                    // now select in the tree
834                    TreePath path = _pathArray[i];
835                    for (JTree tree : trees) {
836                        tree.setSelectionPath(path);
837                        // ensure selection is in visible portion of JScrollPane
838                        tree.scrollPathToVisible(path);
839                    }
840                    return;
841                }
842            }
843        }
844
845        // if we got to here, we need to add a new reserved value entry
846        addReservedEntry(value);
847    }
848
849    java.util.List<Component> reps = new java.util.ArrayList<>();
850
851    public int retry = 0; // counts retrys of a single CV
852
853    int _progState = 0; // coded by the following
854    static final int IDLE = 0;
855    static final int READING_FIRST = 1; // positive values are reading, i.e. 2 is read 2nd CV
856    static final int WRITING_FIRST = -1; // negative values are writing, i.e. -2 is write 2nd CV
857
858    static final int bitCount = Long.bitCount(~0);
859    static final long intMask = Integer.toUnsignedLong(~0);
860
861    /**
862     * Notify the connected CVs of a state change from above
863     *
864     * @param state The new state
865     */
866    @Override
867    public void setCvState(ValueState state) {
868        for (int i = 0; i < cvCount; i++) {
869            cvList.get(i).thisCV.setState(state);
870        }
871    }
872
873    @Override
874    public boolean isChanged() {
875        boolean changed = false;
876        for (int i = 0; i < cvCount; i++) {
877            changed = (changed || considerChanged(cvList.get(i).thisCV));
878        }
879        return changed;
880    }
881
882    @Override
883    public boolean isToRead() {
884        boolean toRead = false;
885        for (int i = 0; i < cvCount; i++) {
886            toRead = (toRead || (cvList.get(i).thisCV).isToRead());
887        }
888        return toRead;
889    }
890
891    @Override
892    public boolean isToWrite() {
893        boolean toWrite = false;
894        for (int i = 0; i < cvCount; i++) {
895            toWrite = (toWrite || (cvList.get(i).thisCV).isToWrite());
896        }
897        return toWrite;
898    }
899
900    @Override
901    public void readChanges() {
902        if (isToRead() && !isChanged()) {
903            log.debug("!!!!!!! unacceptable combination in readChanges: {}", label());
904        }
905        if (isChanged() || isToRead()) {
906            readAll();
907        }
908    }
909
910    @Override
911    public void writeChanges() {
912        if (isToWrite() && !isChanged()) {
913            log.debug("!!!!!! unacceptable combination in writeChanges: {}", label());
914        }
915        if (isChanged() || isToWrite()) {
916            writeAll();
917        }
918    }
919
920    @Override
921    public void readAll() {
922        log.debug("Variable={}; splitVal read() invoked", _name);
923        setToRead(false);
924        setBusy(true);  // will be reset when value changes
925        //super.setState(READ);
926        //_value.setSelectedIndex(0); // start with a clean slate
927        for (int i = 0; i < cvCount; i++) { // mark all Cvs as to be read
928            cvList.get(i).thisCV.setState(ValueState.READ);
929        }
930        //super.setState(READING_FIRST);
931        _progState = READING_FIRST;
932        retry = 0;
933        log.debug("Variable={}; Start CV read", _name);
934        log.debug("    Reading CV={}", cvList.get(0).cvName);
935        (cvList.get(0).thisCV).read(_status); // kick off the read sequence
936    }
937
938    @Override
939    public void writeAll() {
940        log.debug("Variable={}; write() invoked", _name);
941        if (getReadOnly()) {
942            log.error("Variable={}; unexpected write operation when readOnly is set", _name);
943        }
944        setToWrite(false);
945        setBusy(true);  // will be reset when value changes
946        if (_progState != IDLE) {
947            log.warn("Variable={}; Programming state {}, not IDLE, in write()", _name, _progState);
948        }
949
950         for (int i = 0; i < cvCount; i++) { // mark all Cvs as to be written
951            cvList.get(i).thisCV.setState(ValueState.STORED);
952        }
953
954       _progState = WRITING_FIRST;
955        log.debug("Variable={}; Start CV write", _name);
956        log.debug("     Writing CV={}", cvList.get(0).cvName);
957        (cvList.get(0).thisCV).write(_status); // kick off the write sequence
958    }
959
960    /**
961     * Assigns a priority value to a given state.
962     *
963     * @param state State to be converted to a priority value
964     * @return Priority value from state, with UNKNOWN numerically highest
965     */
966    @SuppressFBWarnings(value = {"SF_SWITCH_NO_DEFAULT", "SF_SWITCH_FALLTHROUGH"}, justification = "Intentional fallthrough to produce correct value")
967    int priorityValue(ValueState state) {
968        int value = 0;
969        switch (state) {
970            case UNKNOWN:
971                value++;
972            //$FALL-THROUGH$
973            case DIFFERENT:
974                value++;
975            //$FALL-THROUGH$
976            case EDITED:
977                value++;
978            //$FALL-THROUGH$
979            case FROMFILE:
980                value++;
981            //$FALL-THROUGH$
982            default:
983                //$FALL-THROUGH$
984                return value;
985        }
986    }
987
988       // handle incoming parameter notification
989    @Override
990    public void propertyChange(java.beans.PropertyChangeEvent e) {
991        // notification from CV; check for Value being changed
992        log.trace("propertyChange for {} {} _progState = {} from {}", e.getPropertyName(), e.getNewValue(), _progState, e.getSource());
993        switch (e.getPropertyName()) {
994            case "Busy":
995
996                if (((Boolean) e.getNewValue()).equals(Boolean.FALSE)) {
997
998                    // check for expected cv
999                    if ( (_progState >= READING_FIRST || _progState <= WRITING_FIRST ) && e.getSource() != cvList.get(Math.abs(_progState) - 1).thisCV ) {
1000                        log.trace("From \"{}\" but expected \"{}\", ignoring",
1001                            e.getSource(), cvList.get(Math.abs(_progState) - 1).thisCV );
1002                        break;
1003                    }
1004
1005                    if (_progState >= READING_FIRST){
1006                        ValueState curState = (cvList.get(Math.abs(_progState) - 1).thisCV).getState();
1007                        log.trace("propertyChange Busy _progState={} curState={}", _progState, curState);
1008                        if (curState == ValueState.READ) {   // was the last read successful?
1009                            retry = 0;
1010                            log.debug("   Variable={}; Busy finds ValueState.READ cvCount={}", _name, cvCount);
1011                            if (Math.abs(_progState) < cvCount) {   // read next CV
1012                                _progState++;
1013                                log.debug("Increment _progState to {}, reading CV={}", _progState, cvList.get(Math.abs(_progState) - 1).cvName);
1014                                (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status);
1015                            } else {  // finally done, set not busy
1016                                log.debug("Variable={}; Busy goes false with success READING _progState {}", _name, _progState);
1017                                _progState = IDLE;
1018                                setToRead(false);
1019                                setBusy(false);
1020                            }
1021                        } else {   // read failed
1022                            log.debug("   Variable={}; Busy finds other than ValueState.READ _progState {}", _name, _progState);
1023                            if (retry < RETRY_COUNT) { //have we exhausted retry count?
1024                                retry++;
1025                                // stay on same sequence number for retry, don't update _progState
1026                                (cvList.get(Math.abs(_progState) - 1).thisCV).read(_status);
1027                            } else {
1028                                log.warn("Retry failed for CV{}" ,(cvList.get(Math.abs(_progState) - 1).thisCV).toString());
1029                                _progState = IDLE;
1030                                setToRead(false);
1031                                setBusy(false);
1032                                if (RETRY_COUNT > 0) {
1033                                    for (int i = 0; i < cvCount; i++) { // mark all CVs as unknown otherwise problems may occur
1034                                        cvList.get(i).thisCV.setState(ValueState.UNKNOWN);
1035                                    }
1036                                }
1037                            }
1038                        }
1039                    } else  if (_progState <= WRITING_FIRST) {  // writing CVs
1040                        if ((cvList.get(Math.abs(_progState) - 1).thisCV).getState() == ValueState.STORED) {   // was the last read successful?
1041                            if (Math.abs(_progState) < cvCount) {   // write next CV
1042                                _progState--;
1043                                log.debug("Writing CV={}", cvList.get(Math.abs(_progState) - 1).cvName);
1044                                (cvList.get(Math.abs(_progState) - 1).thisCV).write(_status);
1045                            } else {  // finally done, set not busy
1046                                log.debug("Variable={}; Busy goes false with success WRITING _progState {}", _name, _progState);
1047                                _progState = IDLE;
1048                                setBusy(false);
1049                                setToWrite(false);
1050                            }
1051                        } else {   // write failed we're done!
1052                            log.debug("Variable={}; Busy goes false with failure WRITING _progState {}", _name, _progState);
1053                            _progState = IDLE;
1054                            setToWrite(false);
1055                            setBusy(false);
1056                        }
1057                    }
1058                }
1059                break;
1060            case "State": {
1061                log.debug("Possible {} variable state change due to CV state change, so propagate that", _name);
1062                ValueState varState = getState(); // AbstractValue.SAME;
1063                log.debug("{} variable state was {}", _name, varState.getName());
1064                for (int i = 0; i < cvCount; i++) {
1065                    ValueState state = cvList.get(i).thisCV.getState();
1066                    if (i == 0) {
1067                        varState = state;
1068                    } else if (priorityValue(state) > priorityValue(varState)) {
1069                        varState = ValueState.UNKNOWN; // or should it be = state ?
1070//                        varState = state; // or should it be = state ?
1071                    }
1072                }
1073                setState(varState);
1074                for (JTree tree : trees) {
1075                    tree.setBackground(_value.getBackground());
1076                    //tree.setOpaque(true);
1077                }
1078                log.debug("{} variable state set to {}", _name, varState.getName());
1079                break;
1080            }
1081            case "Value": {
1082                // update value of Variable
1083
1084                //setLongValue(Long.parseLong((String)_value.getSelectedItem()));  // check for duplicate done inside setValue
1085                log.debug("update value of Variable {} cvCount={}", _name, cvCount);
1086
1087                int[] intVals = new int[cvCount];
1088
1089                for (int i = 0; i < cvCount; i++) {
1090                    intVals[i] = (cvList.get(i).thisCV.getValue() & maskValAsInt(cvList.get(i).cvMask)) >>> offsetVal(cvList.get(i).cvMask);
1091                    log.trace("   with intVal[{}] = {}", i, intVals[i]);
1092                }
1093
1094                updateVariableValue(intVals);
1095
1096                log.debug("state change due to CV value change, so propagate that");
1097                ValueState varState = ValueState.SAME;
1098                for (int i = 0; i < cvCount; i++) {
1099                    ValueState state = cvList.get(i).thisCV.getState();
1100                    if (priorityValue(state) > priorityValue(varState)) {
1101                        varState = state;
1102                    }
1103                }
1104                setState(varState);
1105
1106                updatedDropDown();
1107
1108                break;
1109            }
1110            default:
1111                break;
1112        }
1113    }
1114
1115    /* Internal class extends a JComboBox so that its color is consistent with
1116     * an underlying variable
1117     *
1118     * @author Bob Jacobsen   Copyright (C) 2001
1119     * @author tweaked by Jordan McBride Copyright (C) 2021
1120     *
1121     */
1122    public static class VarComboBox extends JComboBox<String> {
1123
1124        VarComboBox(ComboBoxModel<String> m, SplitEnumVariableValue var) {
1125            super(m);
1126            _var = var;
1127            _l = new java.beans.PropertyChangeListener() {
1128                @Override
1129                public void propertyChange(java.beans.PropertyChangeEvent e) {
1130                        log.debug("VarComboBox saw property change: {}", e);
1131                    originalPropertyChanged(e);
1132                }
1133            };
1134            // get the original color right
1135            setBackground(_var._value.getBackground());
1136            setOpaque(true);
1137            // listen for changes to original state
1138            _var.addPropertyChangeListener(_l);
1139        }
1140
1141        SplitEnumVariableValue _var;
1142        transient java.beans.PropertyChangeListener _l = null;
1143
1144        void originalPropertyChanged(java.beans.PropertyChangeEvent e) {
1145            // update this color from original state
1146            if (e.getPropertyName().equals("State")) {
1147                setBackground(_var._value.getBackground());
1148                setOpaque(true);
1149            }
1150        }
1151
1152        public void dispose() {
1153            if (_var != null && _l != null) {
1154                _var.removePropertyChangeListener(_l);
1155            }
1156            _l = null;
1157            _var = null;
1158        }
1159    }
1160
1161    /**
1162     * Class to hold CV parameters for CVs used.
1163     */
1164    static class CvItem {
1165
1166        // class fields
1167        String cvName;
1168        String cvMask;
1169        int startOffset;
1170        CvValue thisCV;
1171
1172        CvItem(String cvNameVal, String cvMaskVal) {
1173            cvName = cvNameVal;
1174            cvMask = cvMaskVal;
1175        }
1176    }
1177
1178// clean up connections when done
1179    @Override
1180    public void dispose() {
1181        log.debug("dispose");
1182
1183        // remove connection to CV
1184        if (_cvMap.get(getCvNum()) == null) {
1185            log.error("no CV defined for variable {}, no listeners to remove", getCvNum());
1186        } else {
1187            _cvMap.get(getCvNum()).removePropertyChangeListener(this);
1188        }
1189        // remove connection to graphical representation
1190        disposeReps();
1191    }
1192
1193        void disposeReps() {
1194        if (_value != null) {
1195            _value.removeActionListener(this);
1196        }
1197        for (int i = 0; i < comboCBs.size(); i++) {
1198            comboCBs.get(i).dispose();
1199        }
1200        for (int i = 0; i < comboVars.size(); i++) {
1201            comboVars.get(i).dispose();
1202        }
1203        for (int i = 0; i < comboRBs.size(); i++) {
1204            comboRBs.get(i).dispose();
1205        }
1206    }
1207
1208    static class TreeLeafNode extends DefaultMutableTreeNode {
1209
1210        TreeLeafNode(String name, int index) {
1211            super(name);
1212            this.index = index;
1213        }
1214
1215        int index;
1216    }
1217
1218
1219
1220    // initialize logging
1221    private final static Logger log = LoggerFactory.getLogger(SplitEnumVariableValue.class
1222            .getName());
1223
1224}