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