001package jmri.jmrit.logixng.actions;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.*;
006import java.util.concurrent.atomic.AtomicReference;
007
008import jmri.*;
009import jmri.jmrit.logixng.*;
010import jmri.jmrit.logixng.implementation.DefaultSymbolTable;
011import jmri.jmrit.logixng.util.*;
012import jmri.jmrit.logixng.util.parser.*;
013import jmri.util.*;
014
015/**
016 * Executes an action when the expression is True.
017 *
018 * @author Daniel Bergqvist Copyright 2025
019 */
020public class ForEachWithDelay extends AbstractDigitalAction
021        implements FemaleSocketListener, PropertyChangeListener {
022
023    private final LogixNG_SelectString _selectVariable =
024            new LogixNG_SelectString(this, this);
025
026    private final LogixNG_SelectNamedBean<Memory> _selectMemoryNamedBean =
027            new LogixNG_SelectNamedBean<>(
028                    this, Memory.class, InstanceManager.getDefault(MemoryManager.class), this);
029
030    private boolean _useCommonSource = true;
031    private CommonManager _commonManager = CommonManager.Sensors;
032    private UserSpecifiedSource _userSpecifiedSource = UserSpecifiedSource.Variable;
033    private String _formula = "";
034    private ExpressionNode _expressionNode;
035    private int _delay;
036    private TimerUnit _unit = TimerUnit.MilliSeconds;
037    private String _variableName = "";
038    private boolean _resetIfAlreadyStarted;
039    private boolean _useIndividualTimers;
040    private String _socketSystemName;
041    private final FemaleDigitalActionSocket _socket;
042    private ProtectedTimerTask _defaultTimerTask;
043
044    private final InternalFemaleSocket _defaultInternalSocket = new InternalFemaleSocket();
045
046
047    public ForEachWithDelay(String sys, String user) {
048        super(sys, user);
049        _socket = InstanceManager.getDefault(DigitalActionManager.class)
050                .createFemaleSocket(this, this, "A");
051    }
052
053    @Override
054    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
055        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
056        String sysName = systemNames.get(getSystemName());
057        String userName = userNames.get(getSystemName());
058        if (sysName == null) sysName = manager.getAutoSystemName();
059        ForEachWithDelay copy = new ForEachWithDelay(sysName, userName);
060        copy.setComment(getComment());
061        copy.setUseCommonSource(_useCommonSource);
062        copy.setCommonManager(_commonManager);
063        copy.setUserSpecifiedSource(_userSpecifiedSource);
064        copy.setDelay(_delay);
065        copy.setUnit(_unit);
066        _selectVariable.copy(copy._selectVariable);
067        _selectMemoryNamedBean.copy(copy._selectMemoryNamedBean);
068        copy.setFormula(_formula);
069        copy.setLocalVariableName(_variableName);
070        copy.setResetIfAlreadyStarted(_resetIfAlreadyStarted);
071        copy.setUseIndividualTimers(_useIndividualTimers);
072        return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames);
073    }
074
075    public LogixNG_SelectString getSelectVariable() {
076        return _selectVariable;
077    }
078
079    public LogixNG_SelectNamedBean<Memory> getSelectMemoryNamedBean() {
080        return _selectMemoryNamedBean;
081    }
082
083    public void setUseCommonSource(boolean commonSource) {
084        this._useCommonSource = commonSource;
085    }
086
087    public boolean isUseCommonSource() {
088        return _useCommonSource;
089    }
090
091    public void setCommonManager(CommonManager commonManager) throws ParserException {
092        _commonManager = commonManager;
093        parseFormula();
094    }
095
096    public CommonManager getCommonManager() {
097        return _commonManager;
098    }
099
100    public void setUserSpecifiedSource(UserSpecifiedSource userSpecifiedSource) throws ParserException {
101        _userSpecifiedSource = userSpecifiedSource;
102        parseFormula();
103    }
104
105    public UserSpecifiedSource getUserSpecifiedSource() {
106        return _userSpecifiedSource;
107    }
108
109    public void setFormula(String formula) throws ParserException {
110        _formula = formula;
111        parseFormula();
112    }
113
114    public String getFormula() {
115        return _formula;
116    }
117
118    private void parseFormula() throws ParserException {
119        if (_userSpecifiedSource == UserSpecifiedSource.Formula) {
120            Map<String, Variable> variables = new HashMap<>();
121
122            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
123            _expressionNode = parser.parseExpression(_formula);
124        } else {
125            _expressionNode = null;
126        }
127    }
128
129    /**
130     * Get the delay.
131     * @return the delay
132     */
133    public int getDelay() {
134        return _delay;
135    }
136
137    /**
138     * Set the delay.
139     * @param delay the delay
140     */
141    public void setDelay(int delay) {
142        _delay = delay;
143    }
144
145    /**
146     * Get the unit
147     * @return the unit
148     */
149    public TimerUnit getUnit() {
150        return _unit;
151    }
152
153    /**
154     * Set the unit
155     * @param unit the unit
156     */
157    public void setUnit(TimerUnit unit) {
158        _unit = unit;
159    }
160
161    /**
162     * Get name of local variable
163     * @return name of local variable
164     */
165    public String getLocalVariableName() {
166        return _variableName;
167    }
168
169    /**
170     * Set name of local variable
171     * @param localVariableName name of local variable
172     */
173    public void setLocalVariableName(String localVariableName) {
174        _variableName = localVariableName;
175    }
176
177    /**
178     * Get reset if timer is already started.
179     * @return true if the timer should be reset if this action is executed
180     *         while timer is ticking, false othervise
181     */
182    public boolean getResetIfAlreadyStarted() {
183        return _resetIfAlreadyStarted;
184    }
185
186    /**
187     * Set reset if timer is already started.
188     * @param resetIfAlreadyStarted true if the timer should be reset if this
189     *                              action is executed while timer is ticking,
190     *                              false othervise
191     */
192    public void setResetIfAlreadyStarted(boolean resetIfAlreadyStarted) {
193        _resetIfAlreadyStarted = resetIfAlreadyStarted;
194    }
195
196    /**
197     * Get use individual timers.
198     * @return true if the timer should use individual timers, false othervise
199     */
200    public boolean getUseIndividualTimers() {
201        return _useIndividualTimers;
202    }
203
204    /**
205     * Set reset if timer is already started.
206     * @param useIndividualTimers true if the timer should use individual timers,
207     *                              false othervise
208     */
209    public void setUseIndividualTimers(boolean useIndividualTimers) {
210        _useIndividualTimers = useIndividualTimers;
211    }
212
213    /** {@inheritDoc} */
214    @Override
215    public LogixNG_Category getCategory() {
216        return LogixNG_Category.FLOW_CONTROL;
217    }
218
219    /** {@inheritDoc} */
220    @Override
221    @SuppressWarnings("unchecked")
222    public void execute() throws JmriException {
223        final AtomicReference<Collection<? extends Object>> collectionRef = new AtomicReference<>();
224        final AtomicReference<JmriException> ref = new AtomicReference<>();
225
226        final ConditionalNG conditionalNG = getConditionalNG();
227        final SymbolTable symbolTable = getConditionalNG().getSymbolTable();
228
229        if (_useCommonSource) {
230            collectionRef.set(_commonManager.getManager().getNamedBeanSet());
231        } else {
232            ThreadingUtil.runOnLayoutWithJmriException(() -> {
233
234                Object value = null;
235
236                switch (_userSpecifiedSource) {
237                    case Variable:
238                        String otherLocalVariable = _selectVariable.evaluateValue(getConditionalNG());
239                        Object variableValue = symbolTable.getValue(otherLocalVariable);
240
241                        value = variableValue;
242                        break;
243
244                    case Memory:
245                        Memory memory = _selectMemoryNamedBean.evaluateNamedBean(getConditionalNG());
246                        if (memory != null) {
247                            value = memory.getValue();
248                        } else {
249                            log.warn("ForEachWithDelay memory is null");
250                        }
251                        break;
252
253                    case Formula:
254                        if (!_formula.isEmpty() && _expressionNode != null) {
255                            value = _expressionNode.calculate(conditionalNG.getSymbolTable());
256                        }
257                        break;
258
259                    default:
260                        // Throw exception
261                        throw new IllegalArgumentException("_userSpecifiedSource has invalid value: {}" + _userSpecifiedSource.name());
262                }
263
264                if (value instanceof Manager) {
265                    collectionRef.set(((Manager<? extends NamedBean>) value).getNamedBeanSet());
266                } else if (value != null && value.getClass().isArray()) {
267                    // Note: (Object[]) is needed to tell that the parameter is an array and not a vararg argument
268                    // See: https://stackoverflow.com/questions/2607289/converting-array-to-list-in-java/2607327#2607327
269                    collectionRef.set(Arrays.asList((Object[])value));
270                } else if (value instanceof Collection) {
271                    collectionRef.set((Collection<? extends Object>) value);
272                } else if (value instanceof Map) {
273                    collectionRef.set(((Map<?,?>) value).entrySet());
274                } else {
275                    throw new JmriException(Bundle.getMessage("ForEachWithDelay_InvalidValue",
276                                    value != null ? value.getClass().getName() : null));
277                }
278            });
279        }
280
281        if (ref.get() != null) throw ref.get();
282
283        List<Object> list = new ArrayList<>(collectionRef.get());
284
285        synchronized(this) {
286            if (!_useIndividualTimers && (_defaultTimerTask != null)) {
287                if (_resetIfAlreadyStarted) _defaultTimerTask.stopTimer();
288                else return;
289            }
290            long timerDelay = _delay * _unit.getMultiply();
291            long timerStart = System.currentTimeMillis();
292            ConditionalNG conditonalNG = getConditionalNG();
293            scheduleTimer(conditonalNG, conditonalNG.getSymbolTable(), timerDelay, timerStart, list, 0);
294        }
295    }
296
297    /**
298     * Get a new timer task.
299     * @param conditionalNG  the ConditionalNG
300     * @param symbolTable    the symbol table
301     * @param timerDelay     the time the timer should wait
302     * @param timerStart     the time when the timer was started
303     */
304    private ProtectedTimerTask getNewTimerTask(
305            ConditionalNG conditionalNG,
306            SymbolTable symbolTable,
307            long timerDelay,
308            long timerStart,
309            List<? extends Object> list,
310            int nextIndex)
311            throws JmriException {
312
313        DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(symbolTable);
314
315        return new ProtectedTimerTask() {
316            @Override
317            public void execute() {
318                try {
319                    synchronized(ForEachWithDelay.this) {
320                        if (!_useIndividualTimers) _defaultTimerTask = null;
321                        long currentTime = System.currentTimeMillis();
322                        long currentTimerTime = currentTime - timerStart;
323                        if (currentTimerTime < timerDelay) {
324                            scheduleTimer(conditionalNG, newSymbolTable, timerDelay - currentTimerTime, currentTime, list, nextIndex);
325                        } else {
326                            InternalFemaleSocket internalSocket;
327                            if (_useIndividualTimers) {
328                                internalSocket = new InternalFemaleSocket();
329                            } else {
330                                internalSocket = _defaultInternalSocket;
331                            }
332                            internalSocket.conditionalNG = conditionalNG;
333                            internalSocket.newSymbolTable = newSymbolTable;
334                            internalSocket.newSymbolTable.setValue(_variableName, list.get(nextIndex));
335                            conditionalNG.execute(internalSocket);
336
337                            if (nextIndex+1 < list.size()) {
338                                scheduleTimer(conditionalNG, newSymbolTable, timerDelay, currentTime, list, nextIndex+1);
339                            }
340                        }
341                    }
342                } catch (RuntimeException | JmriException e) {
343                    log.error("Exception thrown", e);
344                }
345            }
346        };
347    }
348
349    private void scheduleTimer(
350            ConditionalNG conditionalNG,
351            SymbolTable symbolTable,
352            long timerDelay,
353            long timerStart,
354            List<? extends Object> list,
355            int nextIndex)
356            throws JmriException {
357
358        synchronized(ForEachWithDelay.this) {
359            if (!_useIndividualTimers && (_defaultTimerTask != null)) {
360                _defaultTimerTask.stopTimer();
361            }
362            ProtectedTimerTask timerTask =
363                    getNewTimerTask(conditionalNG, symbolTable, timerDelay, timerStart, list, nextIndex);
364            if (!_useIndividualTimers) {
365                _defaultTimerTask = timerTask;
366            }
367            TimerUtil.schedule(timerTask, timerDelay);
368        }
369    }
370
371    @Override
372    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
373        switch (index) {
374            case 0:
375                return _socket;
376
377            default:
378                throw new IllegalArgumentException(
379                        String.format("index has invalid value: %d", index));
380        }
381    }
382
383    @Override
384    public int getChildCount() {
385        return 1;
386    }
387
388    @Override
389    public void connected(FemaleSocket socket) {
390        if (socket == _socket) {
391            _socketSystemName = socket.getConnectedSocket().getSystemName();
392        } else {
393            throw new IllegalArgumentException("unkown socket");
394        }
395    }
396
397    @Override
398    public void disconnected(FemaleSocket socket) {
399        if (socket == _socket) {
400            _socketSystemName = null;
401        } else {
402            throw new IllegalArgumentException("unkown socket");
403        }
404    }
405
406    @Override
407    public String getShortDescription(Locale locale) {
408        return Bundle.getMessage(locale, "ForEachWithDelay_Short");
409    }
410
411    @Override
412    public String getLongDescription(Locale locale) {
413        if (_useCommonSource) {
414            return Bundle.getMessage(locale, "ForEachWithDelay_Long_Common",
415                    _commonManager.toString(), _variableName, _socket.getName(), _unit.getTimeWithUnit(_delay),
416                    _resetIfAlreadyStarted
417                            ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_ResetRepeat"))
418                            : Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_IgnoreRepeat")),
419                    _useIndividualTimers
420                            ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_UseIndividualTimers"))
421                            : "");
422        } else {
423            switch (_userSpecifiedSource) {
424                case Variable:
425                    return Bundle.getMessage(locale, "ForEachWithDelay_Long_LocalVariable",
426                            _selectVariable.getDescription(locale), _variableName, _socket.getName(), _unit.getTimeWithUnit(_delay),
427                            _resetIfAlreadyStarted
428                                    ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_ResetRepeat"))
429                                    : Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_IgnoreRepeat")),
430                            _useIndividualTimers
431                                    ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_UseIndividualTimers"))
432                                    : "");
433
434                case Memory:
435                    return Bundle.getMessage(locale, "ForEachWithDelay_Long_Memory",
436                            _selectMemoryNamedBean.getDescription(locale), _variableName, _socket.getName(), _unit.getTimeWithUnit(_delay),
437                            _resetIfAlreadyStarted
438                                    ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_ResetRepeat"))
439                                    : Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_IgnoreRepeat")),
440                            _useIndividualTimers
441                                    ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_UseIndividualTimers"))
442                                    : "");
443
444                case Formula:
445                    return Bundle.getMessage(locale, "ForEachWithDelay_Long_Formula",
446                            _formula, _variableName, _socket.getName(), _unit.getTimeWithUnit(_delay),
447                            _resetIfAlreadyStarted
448                                    ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_ResetRepeat"))
449                                    : Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_IgnoreRepeat")),
450                            _useIndividualTimers
451                                    ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_UseIndividualTimers"))
452                                    : "");
453
454                default:
455                    throw new IllegalArgumentException("_variableOperation has invalid value: " + _userSpecifiedSource.name());
456            }
457        }
458    }
459
460    public FemaleDigitalActionSocket getSocket() {
461        return _socket;
462    }
463
464    public String getSocketSystemName() {
465        return _socketSystemName;
466    }
467
468    public void setSocketSystemName(String systemName) {
469        _socketSystemName = systemName;
470    }
471
472    /** {@inheritDoc} */
473    @Override
474    public void setup() {
475        try {
476            if ( !_socket.isConnected()
477                    || !_socket.getConnectedSocket().getSystemName()
478                            .equals(_socketSystemName)) {
479
480                String socketSystemName = _socketSystemName;
481                _socket.disconnect();
482                if (socketSystemName != null) {
483                    MaleSocket maleSocket =
484                            InstanceManager.getDefault(DigitalActionManager.class)
485                                    .getBySystemName(socketSystemName);
486                    _socket.disconnect();
487                    if (maleSocket != null) {
488                        _socket.connect(maleSocket);
489                        maleSocket.setup();
490                    } else {
491                        log.error("cannot load digital action {}", socketSystemName);
492                    }
493                }
494            } else {
495                _socket.getConnectedSocket().setup();
496            }
497        } catch (SocketAlreadyConnectedException ex) {
498            // This shouldn't happen and is a runtime error if it does.
499            throw new RuntimeException("socket is already connected");
500        }
501    }
502
503    /** {@inheritDoc} */
504    @Override
505    public void registerListenersForThisClass() {
506        if (!_listenersAreRegistered) {
507            if (_userSpecifiedSource == UserSpecifiedSource.Memory) {
508                _selectMemoryNamedBean.registerListeners();
509            }
510            _listenersAreRegistered = true;
511        }
512    }
513
514    /** {@inheritDoc} */
515    @Override
516    public void unregisterListenersForThisClass() {
517        if (_listenersAreRegistered) {
518            if (_userSpecifiedSource == UserSpecifiedSource.Memory) {
519                _selectMemoryNamedBean.unregisterListeners();
520            }
521            _listenersAreRegistered = false;
522        }
523    }
524
525    /** {@inheritDoc} */
526    @Override
527    public void disposeMe() {
528    }
529
530    /** {@inheritDoc} */
531    @Override
532    public void propertyChange(PropertyChangeEvent evt) {
533        getConditionalNG().execute();
534    }
535
536
537    public enum UserSpecifiedSource {
538        Variable(Bundle.getMessage("ForEachWithDelay_UserSpecifiedSource_Variable")),
539        Memory(Bundle.getMessage("ForEachWithDelay_UserSpecifiedSource_Memory")),
540        Formula(Bundle.getMessage("ForEachWithDelay_UserSpecifiedSource_Formula"));
541
542        private final String _text;
543
544        private UserSpecifiedSource(String text) {
545            this._text = text;
546        }
547
548        @Override
549        public String toString() {
550            return _text;
551        }
552
553    }
554
555
556    private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket {
557
558        private ConditionalNG conditionalNG;
559        private SymbolTable newSymbolTable;
560
561        public InternalFemaleSocket() {
562            super(null, new FemaleSocketListener(){
563                @Override
564                public void connected(FemaleSocket socket) {
565                    // Do nothing
566                }
567
568                @Override
569                public void disconnected(FemaleSocket socket) {
570                    // Do nothing
571                }
572            }, "A");
573        }
574
575        @Override
576        public void execute() throws JmriException {
577            if (conditionalNG == null) { throw new NullPointerException("conditionalNG is null"); }
578            if (_socket != null) {
579                SymbolTable oldSymbolTable = conditionalNG.getSymbolTable();
580                conditionalNG.setSymbolTable(newSymbolTable);
581                _socket.execute();
582                conditionalNG.setSymbolTable(oldSymbolTable);
583            }
584        }
585
586    }
587
588
589    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ForEachWithDelay.class);
590
591}