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