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