001package jmri.jmrit.logixng.actions;
002
003import jmri.jmrit.logixng.util.TimerUnit;
004
005import java.util.*;
006
007import jmri.InstanceManager;
008import jmri.JmriException;
009import jmri.jmrit.logixng.*;
010import jmri.jmrit.logixng.util.ProtectedTimerTask;
011import jmri.util.TimerUtil;
012
013/**
014 * Executes an action after some time.
015 *
016 * @author Daniel Bergqvist Copyright 2019
017 */
018public class ActionTimer extends AbstractDigitalAction
019        implements FemaleSocketListener {
020
021    private static class State{
022        private ProtectedTimerTask _timerTask;
023        private int _currentTimer = -1;
024        private TimerState _timerState = TimerState.Off;
025        private long _currentTimerDelay = 0;
026        private long _currentTimerStart = 0;
027        private boolean _startIsActive = false;
028    }
029
030    public static final int EXPRESSION_START = 0;
031    public static final int EXPRESSION_STOP = 1;
032    public static final int NUM_STATIC_EXPRESSIONS = 2;
033
034    private String _startExpressionSocketSystemName;
035    private String _stopExpressionSocketSystemName;
036    private final FemaleDigitalExpressionSocket _startExpressionSocket;
037    private final FemaleDigitalExpressionSocket _stopExpressionSocket;
038    private final List<ActionEntry> _actionEntries = new ArrayList<>();
039    private boolean _startImmediately = false;
040    private boolean _runContinuously = false;
041    private boolean _startAndStopByStartExpression = false;
042    private TimerUnit _unit = TimerUnit.MilliSeconds;
043    private boolean _delayByLocalVariables = false;
044    private String _delayLocalVariablePrefix = "";  // An index is appended, for example Delay1, Delay2, ... Delay15.
045    private final Map<ConditionalNG, State> _stateMap = new HashMap<>();
046
047
048    public ActionTimer(String sys, String user) {
049        super(sys, user);
050        _startExpressionSocket = InstanceManager.getDefault(DigitalExpressionManager.class)
051                .createFemaleSocket(this, this, Bundle.getMessage("ActionTimerSocketStart"));
052        _stopExpressionSocket = InstanceManager.getDefault(DigitalExpressionManager.class)
053                .createFemaleSocket(this, this, Bundle.getMessage("ActionTimerSocketStop"));
054        _actionEntries
055                .add(new ActionEntry(InstanceManager.getDefault(DigitalActionManager.class)
056                        .createFemaleSocket(this, this, getNewSocketName())));
057    }
058
059    public ActionTimer(String sys, String user,
060            List<Map.Entry<String, String>> expressionSystemNames,
061            List<ActionData> actionDataList)
062            throws BadUserNameException, BadSystemNameException {
063        super(sys, user);
064        _startExpressionSocket = InstanceManager.getDefault(DigitalExpressionManager.class)
065                .createFemaleSocket(this, this, Bundle.getMessage("ActionTimerSocketStart"));
066        _stopExpressionSocket = InstanceManager.getDefault(DigitalExpressionManager.class)
067                .createFemaleSocket(this, this, Bundle.getMessage("ActionTimerSocketStop"));
068        setActionData(actionDataList);
069    }
070
071    @Override
072    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
073        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
074        String sysName = systemNames.get(getSystemName());
075        String userName = userNames.get(getSystemName());
076        if (sysName == null) sysName = manager.getAutoSystemName();
077        ActionTimer copy = new ActionTimer(sysName, userName);
078        copy.setComment(getComment());
079        copy.setNumActions(getNumActions());
080        for (int i=0; i < getNumActions(); i++) {
081            copy.setDelay(i, getDelay(i));
082        }
083        copy.setStartImmediately(_startImmediately);
084        copy.setRunContinuously(_runContinuously);
085        copy.setStartAndStopByStartExpression(_startAndStopByStartExpression);
086        copy.setUnit(_unit);
087        copy.setDelayByLocalVariables(_delayByLocalVariables);
088        copy.setDelayLocalVariablePrefix(_delayLocalVariablePrefix);
089        return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames);
090    }
091
092    private void setActionData(List<ActionData> actionDataList) {
093        if (!_actionEntries.isEmpty()) {
094            throw new RuntimeException("action system names cannot be set more than once");
095        }
096
097        for (ActionData data : actionDataList) {
098            FemaleDigitalActionSocket socket =
099                    InstanceManager.getDefault(DigitalActionManager.class)
100                            .createFemaleSocket(this, this, data._socketName);
101
102            _actionEntries.add(new ActionEntry(data._delay, socket, data._socketSystemName));
103        }
104    }
105
106    /** {@inheritDoc} */
107    @Override
108    public Category getCategory() {
109        return Category.COMMON;
110    }
111
112    /**
113     * Get a new timer task.
114     */
115    private ProtectedTimerTask getNewTimerTask(ConditionalNG conditionalNG, State state) {
116        return new ProtectedTimerTask() {
117            @Override
118            public void execute() {
119                try {
120                    long currentTimerTime = System.currentTimeMillis() - state._currentTimerStart;
121                    if (currentTimerTime < state._currentTimerDelay) {
122                        scheduleTimer(conditionalNG, state, state._currentTimerDelay - currentTimerTime);
123                    } else {
124                        state._timerState = TimerState.Completed;
125                        conditionalNG.execute();
126                    }
127                } catch (Exception e) {
128                    log.error("Exception thrown", e);
129                }
130            }
131        };
132    }
133
134    private void scheduleTimer(ConditionalNG conditionalNG, State state, long delay) {
135        synchronized(this) {
136            if (state._timerTask != null) {
137                state._timerTask.stopTimer();
138                state._timerTask = null;
139            }
140        }
141        state._timerTask = getNewTimerTask(conditionalNG, state);
142        TimerUtil.schedule(state._timerTask, delay);
143    }
144
145    private void schedule(ConditionalNG conditionalNG, SymbolTable symbolTable, State state) {
146        synchronized(this) {
147            long delay;
148
149            if (_delayByLocalVariables) {
150                delay = jmri.util.TypeConversionUtil
151                        .convertToLong(symbolTable.getValue(
152                                _delayLocalVariablePrefix + Integer.toString(state._currentTimer+1)));
153            } else {
154                delay = _actionEntries.get(state._currentTimer)._delay;
155            }
156
157            state._currentTimerDelay = delay * _unit.getMultiply();
158            state._currentTimerStart = System.currentTimeMillis();
159            state._timerState = TimerState.WaitToRun;
160            scheduleTimer(conditionalNG, state, delay * _unit.getMultiply());
161        }
162    }
163
164    private boolean start(State state) throws JmriException {
165        boolean lastStartIsActive = state._startIsActive;
166        state._startIsActive = _startExpressionSocket.isConnected() && _startExpressionSocket.evaluate();
167        return state._startIsActive && !lastStartIsActive;
168    }
169
170    private boolean checkStart(ConditionalNG conditionalNG, SymbolTable symbolTable, State state) throws JmriException {
171        if (start(state)) state._timerState = TimerState.RunNow;
172
173        if (state._timerState == TimerState.RunNow) {
174            synchronized(this) {
175                if (state._timerTask != null) {
176                    state._timerTask.stopTimer();
177                    state._timerTask = null;
178                }
179            }
180            state._currentTimer = 0;
181            while (state._currentTimer < _actionEntries.size()) {
182                ActionEntry ae = _actionEntries.get(state._currentTimer);
183                if (ae._delay > 0) {
184                    schedule(conditionalNG, symbolTable, state);
185                    return true;
186                }
187                else {
188                    state._currentTimer++;
189                }
190            }
191            // If we get here, all timers has a delay of 0 ms
192            state._timerState = TimerState.Off;
193            return true;
194        }
195
196        return false;
197    }
198
199    private boolean stop(State state) throws JmriException {
200        boolean stop;
201
202        if (_startAndStopByStartExpression) {
203            stop = _startExpressionSocket.isConnected() && !_startExpressionSocket.evaluate();
204        } else {
205            stop = _stopExpressionSocket.isConnected() && _stopExpressionSocket.evaluate();
206        }
207
208        if (stop) {
209            synchronized(this) {
210                if (state._timerTask != null) state._timerTask.stopTimer();
211                state._timerTask = null;
212            }
213            state._timerState = TimerState.Off;
214            return true;
215        }
216        return false;
217    }
218
219    /** {@inheritDoc} */
220    @Override
221    public void execute() throws JmriException {
222        ConditionalNG conditionalNG = getConditionalNG();
223        State state = _stateMap.computeIfAbsent(conditionalNG, o -> new State());
224
225        if (stop(state)) {
226            state._startIsActive = false;
227            return;
228        }
229
230        if (checkStart(conditionalNG, conditionalNG.getSymbolTable(), state)) return;
231
232        if (state._timerState == TimerState.Off) return;
233        if (state._timerState == TimerState.Running) return;
234
235        int startTimer = state._currentTimer;
236        while (state._timerState == TimerState.Completed) {
237            // If the timer has passed full time, execute the action
238            if ((state._timerState == TimerState.Completed) && _actionEntries.get(state._currentTimer)._socket.isConnected()) {
239                _actionEntries.get(state._currentTimer)._socket.execute();
240            }
241
242            // Move to them next timer
243            state._currentTimer++;
244            if (state._currentTimer >= _actionEntries.size()) {
245                state._currentTimer = 0;
246                if (!_runContinuously) {
247                    state._timerState = TimerState.Off;
248                    return;
249                }
250            }
251
252            ActionEntry ae = _actionEntries.get(state._currentTimer);
253            if (ae._delay > 0) {
254                schedule(conditionalNG, conditionalNG.getSymbolTable(), state);
255                return;
256            }
257
258            if (startTimer == state._currentTimer) {
259                // If we get here, all timers has a delay of 0 ms
260                state._timerState = TimerState.Off;
261            }
262        }
263    }
264
265    /**
266     * Get the delay.
267     * @param actionSocket the socket
268     * @return the delay
269     */
270    public int getDelay(int actionSocket) {
271        return _actionEntries.get(actionSocket)._delay;
272    }
273
274    /**
275     * Set the delay.
276     * @param actionSocket the socket
277     * @param delay the delay
278     */
279    public void setDelay(int actionSocket, int delay) {
280        _actionEntries.get(actionSocket)._delay = delay;
281    }
282
283    /**
284     * Get if to start immediately
285     * @return true if to start immediately
286     */
287    public boolean isStartImmediately() {
288        return _startImmediately;
289    }
290
291    /**
292     * Set if to start immediately
293     * @param startImmediately true if to start immediately
294     */
295    public void setStartImmediately(boolean startImmediately) {
296        _startImmediately = startImmediately;
297    }
298
299    /**
300     * Get if run continuously
301     * @return true if run continuously
302     */
303    public boolean isRunContinuously() {
304        return _runContinuously;
305    }
306
307    /**
308     * Set if run continuously
309     * @param runContinuously true if run continuously
310     */
311    public void setRunContinuously(boolean runContinuously) {
312        _runContinuously = runContinuously;
313    }
314
315    /**
316     * Is both start and stop is controlled by the start expression.
317     * @return true if to start immediately
318     */
319    public boolean isStartAndStopByStartExpression() {
320        return _startAndStopByStartExpression;
321    }
322
323    /**
324     * Set if both start and stop is controlled by the start expression.
325     * @param startAndStopByStartExpression true if to start immediately
326     */
327    public void setStartAndStopByStartExpression(boolean startAndStopByStartExpression) {
328        _startAndStopByStartExpression = startAndStopByStartExpression;
329    }
330
331    /**
332     * Get the unit
333     * @return the unit
334     */
335    public TimerUnit getUnit() {
336        return _unit;
337    }
338
339    /**
340     * Set the unit
341     * @param unit the unit
342     */
343    public void setUnit(TimerUnit unit) {
344        _unit = unit;
345    }
346
347    /**
348     * Is delays given by local variables?
349     * @return value true if delay is given by local variables
350     */
351    public boolean isDelayByLocalVariables() {
352        return _delayByLocalVariables;
353    }
354
355    /**
356     * Set if delays should be given by local variables.
357     * @param value true if delay is given by local variables
358     */
359    public void setDelayByLocalVariables(boolean value) {
360        _delayByLocalVariables = value;
361    }
362
363    /**
364     * Is both start and stop is controlled by the start expression.
365     * @return true if to start immediately
366     */
367    public String getDelayLocalVariablePrefix() {
368        return _delayLocalVariablePrefix;
369    }
370
371    /**
372     * Set if both start and stop is controlled by the start expression.
373     * @param value true if to start immediately
374     */
375    public void setDelayLocalVariablePrefix(String value) {
376        _delayLocalVariablePrefix = value;
377    }
378
379    @Override
380    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
381        if (index == EXPRESSION_START) return _startExpressionSocket;
382        if (index == EXPRESSION_STOP) return _stopExpressionSocket;
383        if ((index < 0) || (index >= (NUM_STATIC_EXPRESSIONS + _actionEntries.size()))) {
384            throw new IllegalArgumentException(
385                    String.format("index has invalid value: %d", index));
386        }
387        return _actionEntries.get(index - NUM_STATIC_EXPRESSIONS)._socket;
388    }
389
390    @Override
391    public int getChildCount() {
392        return NUM_STATIC_EXPRESSIONS + _actionEntries.size();
393    }
394
395    @Override
396    public void connected(FemaleSocket socket) {
397        if (socket == _startExpressionSocket) {
398            _startExpressionSocketSystemName = socket.getConnectedSocket().getSystemName();
399        } else if (socket == _stopExpressionSocket) {
400            _stopExpressionSocketSystemName = socket.getConnectedSocket().getSystemName();
401        } else {
402            for (ActionEntry entry : _actionEntries) {
403                if (socket == entry._socket) {
404                    entry._socketSystemName =
405                            socket.getConnectedSocket().getSystemName();
406                }
407            }
408        }
409    }
410
411    @Override
412    public void disconnected(FemaleSocket socket) {
413        if (socket == _startExpressionSocket) {
414            _startExpressionSocketSystemName = null;
415        } else if (socket == _stopExpressionSocket) {
416            _stopExpressionSocketSystemName = null;
417        } else {
418            for (ActionEntry entry : _actionEntries) {
419                if (socket == entry._socket) {
420                    entry._socketSystemName = null;
421                }
422            }
423        }
424    }
425
426    @Override
427    public String getShortDescription(Locale locale) {
428        return Bundle.getMessage(locale, "ActionTimer_Short");
429    }
430
431    @Override
432    public String getLongDescription(Locale locale) {
433        String options = "";
434        if (_delayByLocalVariables) {
435            options = Bundle.getMessage("ActionTimer_Options_DelayByLocalVariable", _delayLocalVariablePrefix);
436        }
437        if (_startAndStopByStartExpression) {
438            return Bundle.getMessage(locale, "ActionTimer_Long2",
439                    Bundle.getMessage("ActionTimer_StartAndStopByStartExpression"), options);
440        } else {
441            return Bundle.getMessage(locale, "ActionTimer_Long", options);
442        }
443    }
444
445    public FemaleDigitalExpressionSocket getStartExpressionSocket() {
446        return _startExpressionSocket;
447    }
448
449    public String getStartExpressionSocketSystemName() {
450        return _startExpressionSocketSystemName;
451    }
452
453    public void setStartExpressionSocketSystemName(String systemName) {
454        _startExpressionSocketSystemName = systemName;
455    }
456
457    public FemaleDigitalExpressionSocket getStopExpressionSocket() {
458        return _stopExpressionSocket;
459    }
460
461    public String getStopExpressionSocketSystemName() {
462        return _stopExpressionSocketSystemName;
463    }
464
465    public void setStopExpressionSocketSystemName(String systemName) {
466        _stopExpressionSocketSystemName = systemName;
467    }
468
469    public int getNumActions() {
470        return _actionEntries.size();
471    }
472
473    public void setNumActions(int num) {
474        List<FemaleSocket> addList = new ArrayList<>();
475        List<FemaleSocket> removeList = new ArrayList<>();
476
477        // Is there too many children?
478        while (_actionEntries.size() > num) {
479            ActionEntry ae = _actionEntries.get(num);
480            if (ae._socket.isConnected()) {
481                throw new IllegalArgumentException("Cannot remove sockets that are connected");
482            }
483            removeList.add(_actionEntries.get(_actionEntries.size()-1)._socket);
484            _actionEntries.remove(_actionEntries.size()-1);
485        }
486
487        // Is there not enough children?
488        while (_actionEntries.size() < num) {
489            FemaleDigitalActionSocket socket =
490                    InstanceManager.getDefault(DigitalActionManager.class)
491                            .createFemaleSocket(this, this, getNewSocketName());
492            _actionEntries.add(new ActionEntry(socket));
493            addList.add(socket);
494        }
495        firePropertyChange(Base.PROPERTY_CHILD_COUNT, removeList, addList);
496    }
497
498    public FemaleDigitalActionSocket getActionSocket(int socket) {
499        return _actionEntries.get(socket)._socket;
500    }
501
502    public String getActionSocketSystemName(int socket) {
503        return _actionEntries.get(socket)._socketSystemName;
504    }
505
506    public void setActionSocketSystemName(int socket, String systemName) {
507        _actionEntries.get(socket)._socketSystemName = systemName;
508    }
509
510    /** {@inheritDoc} */
511    @Override
512    public void setup() {
513        try {
514            if ( !_startExpressionSocket.isConnected()
515                    || !_startExpressionSocket.getConnectedSocket().getSystemName()
516                            .equals(_startExpressionSocketSystemName)) {
517
518                String socketSystemName = _startExpressionSocketSystemName;
519                _startExpressionSocket.disconnect();
520                if (socketSystemName != null) {
521                    MaleSocket maleSocket =
522                            InstanceManager.getDefault(DigitalExpressionManager.class)
523                                    .getBySystemName(socketSystemName);
524                    if (maleSocket != null) {
525                        _startExpressionSocket.connect(maleSocket);
526                        maleSocket.setup();
527                    } else {
528                        log.error("cannot load digital expression {}", socketSystemName);
529                    }
530                }
531            } else {
532                _startExpressionSocket.getConnectedSocket().setup();
533            }
534
535            if ( !_stopExpressionSocket.isConnected()
536                    || !_stopExpressionSocket.getConnectedSocket().getSystemName()
537                            .equals(_stopExpressionSocketSystemName)) {
538
539                String socketSystemName = _stopExpressionSocketSystemName;
540                _stopExpressionSocket.disconnect();
541                if (socketSystemName != null) {
542                    MaleSocket maleSocket =
543                            InstanceManager.getDefault(DigitalExpressionManager.class)
544                                    .getBySystemName(socketSystemName);
545                    _stopExpressionSocket.disconnect();
546                    if (maleSocket != null) {
547                        _stopExpressionSocket.connect(maleSocket);
548                        maleSocket.setup();
549                    } else {
550                        log.error("cannot load digital expression {}", socketSystemName);
551                    }
552                }
553            } else {
554                _stopExpressionSocket.getConnectedSocket().setup();
555            }
556
557            for (ActionEntry ae : _actionEntries) {
558                if ( !ae._socket.isConnected()
559                        || !ae._socket.getConnectedSocket().getSystemName()
560                                .equals(ae._socketSystemName)) {
561
562                    String socketSystemName = ae._socketSystemName;
563                    ae._socket.disconnect();
564                    if (socketSystemName != null) {
565                        MaleSocket maleSocket =
566                                InstanceManager.getDefault(DigitalActionManager.class)
567                                        .getBySystemName(socketSystemName);
568                        ae._socket.disconnect();
569                        if (maleSocket != null) {
570                            ae._socket.connect(maleSocket);
571                            maleSocket.setup();
572                        } else {
573                            log.error("cannot load digital action {}", socketSystemName);
574                        }
575                    }
576                } else {
577                    ae._socket.getConnectedSocket().setup();
578                }
579            }
580        } catch (SocketAlreadyConnectedException ex) {
581            // This shouldn't happen and is a runtime error if it does.
582            throw new RuntimeException("socket is already connected");
583        }
584    }
585
586    /** {@inheritDoc} */
587    @Override
588    public void registerListenersForThisClass() {
589        if (!_listenersAreRegistered) {
590            _stateMap.forEach((conditionalNG, state) -> {
591                // If _timerState is not TimerState.Off, the timer was running when listeners wss unregistered
592                if ((_startImmediately) || (state._timerState != TimerState.Off)) {
593                    if (state._timerState == TimerState.Off) {
594                        state._timerState = TimerState.RunNow;
595                    }
596                    conditionalNG.execute();
597                }
598            });
599            _listenersAreRegistered = true;
600        }
601    }
602
603    /** {@inheritDoc} */
604    @Override
605    public void unregisterListenersForThisClass() {
606        synchronized(this) {
607            _stateMap.forEach((conditionalNG, state) -> {
608                // stopTimer() will not return until the timer task
609                // is cancelled and stopped.
610                if (state._timerTask != null) state._timerTask.stopTimer();
611                state._timerTask = null;
612                state._timerState = TimerState.Off;
613            });
614        }
615        _listenersAreRegistered = false;
616    }
617
618    /** {@inheritDoc} */
619    @Override
620    public void disposeMe() {
621        synchronized(this) {
622            _stateMap.forEach((conditionalNG, state) -> {
623                if (state._timerTask != null) state._timerTask.stopTimer();
624                state._timerTask = null;
625            });
626        }
627    }
628
629
630    private static class ActionEntry {
631        private int _delay;
632        private String _socketSystemName;
633        private final FemaleDigitalActionSocket _socket;
634
635        private ActionEntry(int delay, FemaleDigitalActionSocket socket, String socketSystemName) {
636            _delay = delay;
637            _socketSystemName = socketSystemName;
638            _socket = socket;
639        }
640
641        private ActionEntry(FemaleDigitalActionSocket socket) {
642            this._socket = socket;
643        }
644
645    }
646
647
648    public static class ActionData {
649        private int _delay;
650        private String _socketName;
651        private String _socketSystemName;
652
653        public ActionData(int delay, String socketName, String socketSystemName) {
654            _delay = delay;
655            _socketName = socketName;
656            _socketSystemName = socketSystemName;
657        }
658    }
659
660
661    private enum TimerState {
662        Off,
663        RunNow,
664        WaitToRun,
665        Running,
666        Completed,
667    }
668
669
670    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ActionTimer.class);
671
672}