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