001package jmri.jmrit.logixng.util;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.beans.PropertyVetoException;
006import java.beans.VetoableChangeListener;
007import java.util.HashMap;
008import java.util.Locale;
009import java.util.Map;
010
011import javax.annotation.CheckForNull;
012import javax.annotation.Nonnull;
013
014import jmri.*;
015import jmri.jmrit.logixng.*;
016import jmri.jmrit.logixng.implementation.AbstractBase;
017import jmri.jmrit.logixng.util.parser.*;
018import jmri.jmrit.logixng.util.parser.RecursiveDescentParser;
019import jmri.util.TypeConversionUtil;
020
021/**
022 * Select an combo box value for LogixNG actions and expressions.
023 *
024 * @author Daniel Bergqvist (C) 2024
025 */
026public class LogixNG_SelectComboBox implements VetoableChangeListener {
027
028    /**
029     * An item in the combo box.
030     */
031    public interface Item {
032
033        /**
034         * Return the key that's used to store the item in the tables and
035         * panels file, as well as when indirect addessing is used.
036         * @return the key
037         */
038        String getKey();
039
040        /**
041         * Return the value that's used to show the item in the ComboBox.
042         * @return the value
043         */
044        @Override
045        String toString();
046    }
047
048    private final AbstractBase _base;
049    private final InUse _inUse;
050    private Item[] _valuesArray;
051    private final LogixNG_SelectTable _selectTable;
052    private final PropertyChangeListener _listener;
053    private boolean _listenToMemory;
054    private boolean _listenersAreRegistered;
055
056    private NamedBeanAddressing _addressing = NamedBeanAddressing.Direct;
057    private Item _value;
058    private String _reference = "";
059    private NamedBeanHandle<Memory> _memoryHandle;
060    private String _localVariable = "";
061    private String _formula = "";
062    private ExpressionNode _expressionNode;
063
064
065    public LogixNG_SelectComboBox(AbstractBase base, Item[] valuesArray,
066            Item initialValue, PropertyChangeListener listener) {
067        _base = base;
068        _inUse = () -> true;
069        _valuesArray = valuesArray;
070        _value = initialValue;
071        _selectTable = new LogixNG_SelectTable(_base, _inUse);
072        _listener = listener;
073    }
074
075
076    public void copy(LogixNG_SelectComboBox copy) throws ParserException {
077        copy.setAddressing(_addressing);
078        copy.setValue(_value);
079        copy.setLocalVariable(_localVariable);
080        copy.setReference(_reference);
081        copy.setMemory(_memoryHandle);
082        copy.setListenToMemory(_listenToMemory);
083        copy.setFormula(_formula);
084        _selectTable.copy(copy._selectTable);
085    }
086
087    public void setValues(Item[] valuesArray) {
088        String key = _value != null ? _value.getKey() : null;
089        _valuesArray = valuesArray;
090
091        // Check if the selected value is in the array
092        boolean found = false;
093        for (Item value : _valuesArray) {
094            if (value.getKey().equals(key)) {
095                found = true;
096            }
097        }
098        if (!found) {
099            if (_valuesArray.length > 0) {
100                _value = _valuesArray[0];
101            } else {
102                _value = null;
103            }
104        }
105    }
106
107    public Item[] getValues() {
108        return _valuesArray;
109    }
110
111    public void setAddressing(@Nonnull NamedBeanAddressing addressing) throws ParserException {
112        this._addressing = addressing;
113        parseFormula();
114    }
115
116    public boolean isDirectAddressing() {
117        return _addressing == NamedBeanAddressing.Direct;
118    }
119
120    public NamedBeanAddressing getAddressing() {
121        return _addressing;
122    }
123
124    public void setValue(Item value) {
125        _base.assertListenersAreNotRegistered(log, "setString");
126        _value = value;
127    }
128
129    public void setValue(String key) {
130        _base.assertListenersAreNotRegistered(log, "setString");
131        if (key == null) {
132            _value = null;
133            return;
134        }
135        for (Item v : _valuesArray) {
136            if (v.getKey().equals(key)) {
137                _value = v;
138                return;
139            }
140        }
141        throw new IllegalArgumentException("Key " + key + " is not in list");
142    }
143
144    @CheckForNull
145    public Item getValue() {
146        return _value;
147    }
148
149    public Item getValueByKey(String key) {
150        for (Item value : _valuesArray) {
151            if (value.getKey().equals(key)) return value;
152        }
153        return null;
154    }
155
156    public void setReference(@Nonnull String reference) {
157        if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) {
158            throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference");
159        }
160        _reference = reference;
161    }
162
163    public String getReference() {
164        return _reference;
165    }
166
167    public void setMemory(@Nonnull String memoryName) {
168        Memory memory = InstanceManager.getDefault(MemoryManager.class).getMemory(memoryName);
169        if (memory != null) {
170            setMemory(memory);
171        } else {
172            removeMemory();
173            log.warn("memory \"{}\" is not found", memoryName);
174        }
175    }
176
177    public void setMemory(@Nonnull NamedBeanHandle<Memory> handle) {
178        _memoryHandle = handle;
179        InstanceManager.memoryManagerInstance().addVetoableChangeListener(this);
180        addRemoveVetoListener();
181    }
182
183    public void setMemory(@Nonnull Memory memory) {
184        setMemory(InstanceManager.getDefault(NamedBeanHandleManager.class)
185                .getNamedBeanHandle(memory.getDisplayName(), memory));
186    }
187
188    public void removeMemory() {
189        if (_memoryHandle != null) {
190            _memoryHandle = null;
191            addRemoveVetoListener();
192        }
193    }
194
195    public NamedBeanHandle<Memory> getMemory() {
196        return _memoryHandle;
197    }
198
199    public void setListenToMemory(boolean listenToMemory) {
200        _listenToMemory = listenToMemory;
201    }
202
203    public boolean getListenToMemory() {
204        return _listenToMemory;
205    }
206
207    public void setLocalVariable(@Nonnull String localVariable) {
208        _localVariable = localVariable;
209    }
210
211    public String getLocalVariable() {
212        return _localVariable;
213    }
214
215    public void setFormula(@Nonnull String formula) throws ParserException {
216        _formula = formula;
217        parseFormula();
218    }
219
220    public String getFormula() {
221        return _formula;
222    }
223
224    private void parseFormula() throws ParserException {
225        if (_addressing == NamedBeanAddressing.Formula) {
226            Map<String, Variable> variables = new HashMap<>();
227
228            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
229            _expressionNode = parser.parseExpression(_formula);
230        } else {
231            _expressionNode = null;
232        }
233    }
234
235    public LogixNG_SelectTable getSelectTable() {
236        return _selectTable;
237    }
238
239    private void addRemoveVetoListener() {
240        if (_memoryHandle != null) {
241            InstanceManager.getDefault(MemoryManager.class).addVetoableChangeListener(this);
242        } else {
243            InstanceManager.getDefault(MemoryManager.class).removeVetoableChangeListener(this);
244        }
245    }
246
247    public Item evaluateValue(ConditionalNG conditionalNG) throws JmriException {
248
249        if (_addressing == NamedBeanAddressing.Direct) {
250            return _value;
251        } else {
252            String key;
253
254            switch (_addressing) {
255                case Reference:
256                    key = ReferenceUtil.getReference(
257                            conditionalNG.getSymbolTable(), _reference);
258                    break;
259
260                case Memory:
261                    key = TypeConversionUtil
262                            .convertToString(_memoryHandle.getBean().getValue(), false);
263                    break;
264
265                case LocalVariable:
266                    SymbolTable symbolNamedBean = conditionalNG.getSymbolTable();
267                    key = TypeConversionUtil
268                            .convertToString(symbolNamedBean.getValue(_localVariable), false);
269                    break;
270
271                case Formula:
272                    key = _expressionNode  != null
273                            ? TypeConversionUtil.convertToString(
274                                    _expressionNode.calculate(
275                                            conditionalNG.getSymbolTable()), false)
276                            : null;
277                    break;
278
279                case Table:
280                    key = TypeConversionUtil.convertToString(
281                            _selectTable.evaluateTableData(conditionalNG), false);
282                    break;
283
284                default:
285                    throw new IllegalArgumentException("invalid _addressing state: " + _addressing.name());
286            }
287
288            return LogixNG_SelectComboBox.this.getValueByKey(key);
289        }
290    }
291
292    public String getDescription(Locale locale) {
293        String description;
294
295        String memoryName;
296        if (_memoryHandle != null) {
297            memoryName = _memoryHandle.getName();
298        } else {
299            memoryName = Bundle.getMessage(locale, "BeanNotSelected");
300        }
301
302        switch (_addressing) {
303            case Direct:
304                description = Bundle.getMessage(locale, "AddressByDirect", _value);
305                break;
306
307            case Reference:
308                description = Bundle.getMessage(locale, "AddressByReference", _reference);
309                break;
310
311            case Memory:
312                description = Bundle.getMessage(locale, "AddressByMemory_Listen", memoryName, Base.getListenString(_listenToMemory));
313                break;
314
315            case LocalVariable:
316                description = Bundle.getMessage(locale, "AddressByLocalVariable", _localVariable);
317                break;
318
319            case Formula:
320                description = Bundle.getMessage(locale, "AddressByFormula", _formula);
321                break;
322
323            case Table:
324                description = Bundle.getMessage(
325                        locale,
326                        "AddressByTable",
327                        _selectTable.getTableNameDescription(locale),
328                        _selectTable.getTableRowDescription(locale),
329                        _selectTable.getTableColumnDescription(locale));
330                break;
331
332            default:
333                throw new IllegalArgumentException("invalid _addressing: " + _addressing.name());
334        }
335        return description;
336    }
337
338    /**
339     * Register listeners if this object needs that.
340     */
341    public void registerListeners() {
342        if (!_listenersAreRegistered
343                && (_addressing == NamedBeanAddressing.Memory)
344                && (_memoryHandle != null)
345                && _listenToMemory) {
346            _memoryHandle.getBean().addPropertyChangeListener("value", _listener);
347            _listenersAreRegistered = true;
348        }
349    }
350
351    /**
352     * Unregister listeners if this object needs that.
353     */
354    public void unregisterListeners() {
355        if (_listenersAreRegistered
356                && (_addressing == NamedBeanAddressing.Memory)
357                && (_memoryHandle != null)
358                && _listenToMemory) {
359            _memoryHandle.getBean().removePropertyChangeListener("value", _listener);
360            _listenersAreRegistered = false;
361        }
362    }
363
364    @Override
365    public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException {
366        if ("CanDelete".equals(evt.getPropertyName()) && _inUse.isInUse()) { // No I18N
367            if (evt.getOldValue() instanceof Memory) {
368                boolean doVeto = false;
369                if ((_addressing == NamedBeanAddressing.Memory) && (_memoryHandle != null) && evt.getOldValue().equals(_memoryHandle.getBean())) {
370                    doVeto = true;
371                }
372                if (doVeto) {
373                    PropertyChangeEvent e = new PropertyChangeEvent(this, "DoNotDelete", null, null);
374                    throw new PropertyVetoException(Bundle.getMessage("MemoryInUseMemoryExpressionVeto", _base.getDisplayName()), e); // NOI18N
375                }
376            }
377        } else if ("DoDelete".equals(evt.getPropertyName())) { // No I18N
378            if (evt.getOldValue() instanceof Memory) {
379                if (evt.getOldValue().equals(_memoryHandle.getBean())) {
380                    removeMemory();
381                }
382            }
383        }
384    }
385
386    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogixNG_SelectComboBox.class);
387}