001package jmri.jmrit.symbolicprog;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.awt.Component;
005import java.awt.event.ActionEvent;
006import java.util.ArrayList;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.Hashtable;
010import java.util.Iterator;
011import javax.swing.JComboBox;
012import javax.swing.JLabel;
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016/**
017 * Extends EnumVariableValue to represent a composition of variable values.
018 * <p>
019 * Internally, each "choice" is stored as a list of "setting" items. Numerical
020 * values for this type of variable (itself) are strictly sequential, because
021 * they are arbitrary.
022 * <p>
023 * This version of the class has certain limitations:
024 * <ol>
025 * <li>Variables referenced in the definition of one of these must have already
026 * been declared earlier in the decoder file. This prevents circular references,
027 * and makes it much easier to find the target variables.
028 * <li>
029 * This version of the variable never changes "State" (color), though it does
030 * track its value from changes to other variables.
031 * <li>There should be a final choice (entry) that doesn't define any settings.
032 * This will then form the default value when the target variables change.
033 * <li>Programming operations on a variable of this type doesn't do anything,
034 * because there doesn't seem to be a consistent model of what "read changes"
035 * and "write changes" should do. This has two implications:
036 * <ul>
037 * <li>Variables referenced as targets must appear on some programming pane, or
038 * they won't be updated by programming operations.
039 * <li>If this variable references variables that are not on this pane, the user
040 * needs to do a read/write all panes operation to record the changes made to
041 * this variable.
042 * </ul>
043 * It's therefore recommended that a CompositeVariableValue just make changes to
044 * target variables on the same programming page.
045 * <li>To apply a mask when setting a value, use an intermediary variable set
046 * from here, which in turn references the goal variable with a mask.
047 * </ol>
048 *
049 * @author Bob Jacobsen Copyright (C) 2001, 2005, 2013
050 */
051public class CompositeVariableValue extends EnumVariableValue {
052
053    public CompositeVariableValue(String name, String comment, String cvName,
054            boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly,
055            String cvNum, String mask, int minVal, int maxVal,
056            HashMap<String, CvValue> v, JLabel status, String stdname) {
057        super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, minVal, maxVal, v, status, stdname);
058        _maxVal = maxVal;
059        _minVal = minVal;
060
061        _value = new JComboBox<String>();
062        _value.getAccessibleContext().setAccessibleName(label());
063
064        log.debug("New Composite named {}", name);
065    }
066
067    /**
068     * Create a null object. Normally only used for tests and to pre-load
069     * classes.
070     */
071    public CompositeVariableValue() {
072        _value = new JComboBox<String>();
073        _value.getAccessibleContext().setAccessibleName(label());
074    }
075
076    @Override
077    public CvValue[] usesCVs() {
078        HashSet<CvValue> cvSet = new HashSet<CvValue>(20);  // 20 is arbitrary
079        for (VariableValue v : variables) {
080            CvValue[] cvs = v.usesCVs();
081            for (int k = 0; k < cvs.length; k++) {
082                cvSet.add(cvs[k]);
083            }
084        }
085        CvValue[] retval = new CvValue[cvSet.size()];
086        Iterator<CvValue> j = cvSet.iterator();
087        int index = 0;
088        while (j.hasNext()) {
089            retval[index++] = j.next();
090        }
091        return retval;
092    }
093
094    /**
095     * Define objects to save and manipulate a particular setting.
096     */
097    static class Setting {
098
099        String varName;
100        VariableValue variable;
101        int value;
102
103        Setting(String varName, VariableValue variable, String value) {
104            this.varName = varName;
105            this.variable = variable;
106            try {
107                this.value = Integer.parseInt(value);
108            } catch (NullPointerException | NumberFormatException e) {
109                log.error("Illegal value received for CompositeVariable {}. Should be int but was {}", varName, value);
110                return;
111            }
112            log.debug("    cTor Setting {} = {}", varName, value);
113        }
114
115        void setValue() {
116            log.debug("    Setting.setValue of {} to {}", varName, value);
117            if (variable == null) {
118                log.error("Variable {} not (yet) created. Verify correct compositeSetting", varName);
119                return;
120            }
121            variable.setIntValue(value);
122        }
123
124        boolean match() {
125            if (log.isDebugEnabled()) {
126                log.debug("         Match checks {} == {}", variable.getIntValue(), value);
127            }
128            if (variable == null) {
129                log.error("Variable {} not (yet) created. Verify correct compositeSetting", varName);
130                return false;
131            }
132            return (variable.getIntValue() == value);
133        }
134    }
135
136    /**
137     * Defines a list of Setting objects.
138     * <p>
139     * Serves as a home for various service methods
140     */
141    static class SettingList extends ArrayList<Setting> {
142
143        public SettingList() {
144            super();
145            log.debug("New setting list");
146        }
147
148        void addSetting(String varName, VariableValue variable, String value) {
149            Setting s = new Setting(varName, variable, value);
150            add(s);
151        }
152
153        void setValues() {
154            if (log.isDebugEnabled()) {
155                log.debug(" setValues in length {}", size());
156            }
157            for (int i = 0; i < this.size(); i++) {
158                Setting s = this.get(i);
159                s.setValue();
160            }
161        }
162
163        boolean match() {
164            for (int i = 0; i < size(); i++) {
165                if (!this.get(i).match()) {
166                    if (log.isDebugEnabled()) {
167                        log.debug("      No match in setting list of length {} at position {}", size(), i);
168                    }
169                    return false;
170                }
171            }
172            if (log.isDebugEnabled()) {
173                log.debug("      Match in setting list of length {}", size());
174            }
175            return true;
176        }
177    }
178
179    Hashtable<String, SettingList> choiceHash = new Hashtable<String, SettingList>();
180    HashSet<VariableValue> variables = new HashSet<VariableValue>(20);  // VariableValue; 20 is an arbitrary guess
181
182    /**
183     * Create a new possible selection.
184     *
185     * @param name Name of the choice being added
186     */
187    public void addChoice(String name) {
188        SettingList l = new SettingList();
189        choiceHash.put(name, l);
190        _value.addItem(name);
191    }
192
193    /**
194     * Add a setting to an existing choice.
195     * @param choice existing choice.
196     * @param varName variable name.
197     * @param variable variable value.
198     * @param value setting value.
199     */
200    public void addSetting(String choice, String varName, VariableValue variable, String value) {
201        SettingList s = choiceHash.get(choice);
202        s.addSetting(varName, variable, value);
203
204        if (variable != null) {
205            variables.add(variable);
206            if (!variable.label().equals(varName)) {
207                log.warn("Unexpected label /{}/ for varName /{}/ during addSetting", variable.label(), varName);
208            }
209        } else {
210            log.error("Variable pointer null when varName={} in choice {}; ignored", varName, choice);
211        }
212    }
213
214    /**
215     * Do end of initialization processing.
216     */
217    @SuppressWarnings("null")
218    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH",
219            justification = "we want to force an exception")
220    @Override
221    public void lastItem() {
222        // configure the representation object
223        _defaultColor = _value.getBackground();
224        super.setState(ValueState.READ);
225
226        // note that we don't set this to COLOR_UNKNOWN!  Rather,
227        // we check the current value
228        findValue();
229
230        // connect to all variables to hear changes
231        Iterator<VariableValue> i = variables.iterator();
232        while (i.hasNext()) {
233            VariableValue v = i.next();
234            if (v == null) {
235                log.error("Variable found as null in last item");
236            }
237            // connect, force an exception if v == null
238            v.addPropertyChangeListener(this);
239        }
240
241        // connect to the JComboBox model so we'll see changes.
242        _value.setActionCommand("");            // so we can tell where change comes from
243        _value.addActionListener(this);
244    }
245
246    @Override
247    public void setToolTipText(String t) {
248        super.setToolTipText(t);   // do default stuff
249        _value.setToolTipText(t);  // set our value
250    }
251
252    @Override
253    public Object rangeVal() {
254        return "composite: " + _minVal + " - " + _maxVal;
255    }
256
257    @Override
258    public void actionPerformed(ActionEvent e) {
259        // see if this is from _value itself, or from an alternate rep.
260        // if from an alternate rep, it will contain the value to select
261        if (!(e.getActionCommand().equals(""))) {
262            // is from alternate rep
263            _value.setSelectedItem(e.getActionCommand());
264        }
265        log.debug("action event: {}", e);
266
267        // notify
268        prop.firePropertyChange("Value", null, getIntValue());
269        // Here for new values; set as needed
270        selectValue(getIntValue());
271    }
272
273    /**
274     * This variable doesn't change state, hence doesn't change color.
275     */
276    @Override
277    public void setState(ValueState state) {
278        log.debug("Ignore setState({})", state);
279    }
280
281    /**
282     * Set to a specific value.
283     * <p>
284     * Does this by delegating to the SettingList
285     */
286    @Override
287    protected void selectValue(int value) {
288        log.debug("selectValue({})", value);
289        if (value > _value.getItemCount() - 1) {
290            log.error("Saw unreasonable internal value for pane combo box: {}", value);
291            return;
292        }
293
294        // locate SettingList for that number
295        String choice = _value.getItemAt(value);
296        SettingList sl = choiceHash.get(choice);
297        sl.setValues();
298
299    }
300
301    @Override
302    public int getIntValue() {
303        return _value.getSelectedIndex();
304    }
305
306    @Override
307    public Component getCommonRep() {
308        return _value;
309    }
310
311    @Override
312    public void setValue(int value) {
313        int oldVal = getIntValue();
314        selectValue(value);
315
316        if (oldVal != value || getState() == ValueState.UNKNOWN) {
317            prop.firePropertyChange("Value", null, value);
318        }
319    }
320
321    /**
322     * Notify the connected CVs of a state change from above by way of the
323     * variables (e.g. not direct to CVs).
324     */
325    @Override
326    public void setCvState(ValueState state) {
327        Iterator<VariableValue> i = variables.iterator();
328        while (i.hasNext()) {
329            VariableValue v = i.next();
330            v.setCvState(state);
331        }
332    }
333
334    @Override
335    public boolean isChanged() {
336        Iterator<VariableValue> i = variables.iterator();
337        while (i.hasNext()) {
338            VariableValue v = i.next();
339            if (v.isChanged()) {
340                return true;
341            }
342        }
343        return false;
344    }
345
346    @Override
347    public void setToRead(boolean state) {
348
349        Iterator<VariableValue> i = variables.iterator();
350        while (i.hasNext()) {
351            VariableValue v = i.next();
352            v.setToRead(state);
353        }
354    }
355
356    /**
357     * This variable needs to be read if any of its subsidiary variables needs
358     * to be read.
359     */
360    @Override
361    public boolean isToRead() {
362        Iterator<VariableValue> i = variables.iterator();
363        while (i.hasNext()) {
364            VariableValue v = i.next();
365            if (v.isToRead()) {
366                return true;
367            }
368        }
369        return false;
370    }
371
372    @Override
373    public void setToWrite(boolean state) {
374        log.debug("Start setToWrite with {}", state);
375
376        Iterator<VariableValue> i = variables.iterator();
377        while (i.hasNext()) {
378            VariableValue v = i.next();
379            v.setToWrite(state);
380        }
381        log.debug("End setToWrite");
382    }
383
384    /**
385     * This variable needs to be written if any of its subsidiary variables
386     * needs to be written.
387     */
388    @Override
389    public boolean isToWrite() {
390        Iterator<VariableValue> i = variables.iterator();
391        while (i.hasNext()) {
392            VariableValue v = i.next();
393            if (v.isToWrite()) {
394                return true;
395            }
396        }
397        return false;
398    }
399
400    @Override
401    public void readChanges() {
402        if (isChanged()) {
403            readingChanges = true;
404            amReading = true;
405            continueRead();
406        }
407    }
408
409    @Override
410    public void writeChanges() {
411        if (isChanged()) {
412            writingChanges = true;
413            amWriting = true;
414            continueWrite();
415        }
416    }
417
418    @Override
419    public void readAll() {
420        readingChanges = false;
421        amReading = true;
422        continueRead();
423    }
424    boolean amReading = false;
425    boolean readingChanges = false;
426
427    /**
428     * See if there's anything to read, and if so do it.
429     */
430    protected void continueRead() {
431        // search for something to do
432        log.debug("Start continueRead");
433
434        Iterator<VariableValue> i = variables.iterator();
435        while (i.hasNext()) {
436            VariableValue v = i.next();
437            if (v.isToRead() && (!readingChanges || v.isChanged())) {
438                // something to do!
439                amReading = true; // should be set already
440                setBusy(true);
441                if (readingChanges) {
442                    v.readChanges();
443                } else {
444                    v.readAll();
445                }
446                return;  // wait for busy change event to continue
447            }
448        }
449        // found nothing, ensure cleaned up
450        amReading = false;
451        super.setState(ValueState.READ);
452        setBusy(false);
453        log.debug("End continueRead, nothing to do");
454    }
455
456    @Override
457    public void writeAll() {
458        if (getReadOnly()) {
459            log.error("unexpected write operation when readOnly is set");
460        }
461        writingChanges = false;
462        amWriting = true;
463        continueWrite();
464    }
465    boolean amWriting = false;
466    boolean writingChanges = false;
467
468    /**
469     * See if there's anything to write, and if so do it.
470     */
471    protected void continueWrite() {
472        // search for something to do
473        log.debug("Start continueWrite");
474
475        Iterator<VariableValue> i = variables.iterator();
476        while (i.hasNext()) {
477            VariableValue v = i.next();
478            if (v.isToWrite() && (!writingChanges || v.isChanged())) {
479                // something to do!
480                amWriting = true; // should be set already
481                setBusy(true);
482                log.debug("request write of {} writing changes {}", v.label(), writingChanges);
483                if (writingChanges) {
484                    v.writeChanges();
485                } else {
486                    v.writeAll();
487                }
488                log.debug("return from starting write request");
489                return;  // wait for busy change event to continue
490            }
491        }
492        // found nothing, ensure cleaned up
493        amWriting = false;
494        super.setState(ValueState.STORED);
495        setBusy(false);
496        log.debug("End continueWrite, nothing to do");
497    }
498
499    // handle incoming parameter notification
500    @Override
501    public void propertyChange(java.beans.PropertyChangeEvent e) {
502        // notification from CV; check for Value being changed
503        if (log.isDebugEnabled()) {
504            log.debug("propertyChange in {} type {} new value {}", label(), e.getPropertyName(), e.getNewValue());
505        }
506        if (e.getPropertyName().equals("Busy")) {
507            if (((Boolean) e.getNewValue()).equals(Boolean.FALSE)) {
508                log.debug("busy change continues programming");
509                // some programming operation just finished
510                if (amReading) {
511                    continueRead();
512                } else if (amWriting) {
513                    continueWrite();
514                }
515                // if we're not reading or writing, no problem, that's just something else happening
516            }
517        } else if (e.getPropertyName().equals("Value")) {
518            findValue();
519        }
520    }
521
522    /**
523     * Suspect underlying variables have changed value; check. First match will
524     * succeed, so there should not be multiple matches possible. ("First match"
525     * is defined in choice-sequence).
526     */
527    void findValue() {
528        if (log.isDebugEnabled()) {
529            log.debug("findValue invoked on {}", label());
530        }
531        for (int i = 0; i < _value.getItemCount(); i++) {
532            String choice = _value.getItemAt(i);
533            SettingList sl = choiceHash.get(choice);
534            if (sl.match()) {
535                log.debug("  match in {}", i);
536                _value.setSelectedItem(choice);
537                return;
538            }
539        }
540        log.debug("   no match");
541    }
542
543    // clean up connections when done
544    @Override
545    public void dispose() {
546        log.debug("dispose");
547
548        Iterator<VariableValue> i = variables.iterator();
549        while (i.hasNext()) {
550            VariableValue v = i.next();
551            v.removePropertyChangeListener(this);
552        }
553
554        // remove the graphical representation
555        disposeReps();
556    }
557
558    // initialize logging
559    private final static Logger log = LoggerFactory.getLogger(CompositeVariableValue.class);
560
561}