001package jmri.jmrit.logixng.actions;
002
003import jmri.util.TimerUnit;
004
005import java.beans.PropertyChangeEvent;
006import java.beans.PropertyChangeListener;
007import java.util.*;
008
009import jmri.InstanceManager;
010import jmri.JmriException;
011import jmri.jmrit.logixng.*;
012import jmri.jmrit.logixng.implementation.DefaultSymbolTable;
013import jmri.jmrit.logixng.util.*;
014import jmri.util.TimerUtil;
015
016/**
017 * Executes an action when the expression is True.
018 *
019 * @author Daniel Bergqvist Copyright 2021
020 */
021public class Timeout extends AbstractDigitalAction
022        implements FemaleSocketListener, PropertyChangeListener {
023
024    private ProtectedTimerTask _timerTask;
025    private final LogixNG_SelectInteger _selectDelay = new LogixNG_SelectInteger(this, this);
026    private final LogixNG_SelectEnum<TimerUnit> _selectTimerUnit =
027            new LogixNG_SelectEnum<>(this, TimerUnit.values(), TimerUnit.MilliSeconds, this);
028    private String _expressionSocketSystemName;
029    private String _actionSocketSystemName;
030    private final FemaleDigitalExpressionSocket _expressionSocket;
031    private final FemaleDigitalActionSocket _actionSocket;
032    private final InternalFemaleSocket _internalSocket = new InternalFemaleSocket();
033
034    // These variables are used internally in this action
035    private long _timerDelay = 0;   // Timer delay in milliseconds
036    private long _timerStart = 0;   // Timer start in milliseconds
037
038    public Timeout(String sys, String user) {
039        super(sys, user);
040        _expressionSocket = InstanceManager.getDefault(DigitalExpressionManager.class)
041                .createFemaleSocket(this, this, "E");
042        _actionSocket = InstanceManager.getDefault(DigitalActionManager.class)
043                .createFemaleSocket(this, this, "A");
044    }
045
046    @Override
047    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
048        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
049        String sysName = systemNames.get(getSystemName());
050        String userName = userNames.get(getSystemName());
051        if (sysName == null) sysName = manager.getAutoSystemName();
052        Timeout copy = new Timeout(sysName, userName);
053        copy.setComment(getComment());
054        _selectDelay.copy(copy._selectDelay);
055        _selectTimerUnit.copy(copy._selectTimerUnit);
056        return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames);
057    }
058
059    /** {@inheritDoc} */
060    @Override
061    public LogixNG_Category getCategory() {
062        return LogixNG_Category.OTHER;
063    }
064
065    /**
066     * Get a new timer task.
067     */
068    private ProtectedTimerTask getNewTimerTask(ConditionalNG conditionalNG, SymbolTable symbolTable) throws JmriException {
069
070        DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(symbolTable);
071
072        return new ProtectedTimerTask() {
073            @Override
074            public void execute() {
075                try {
076                    synchronized(Timeout.this) {
077                        _timerTask = null;
078                        long currentTimerTime = System.currentTimeMillis() - _timerStart;
079                        if (currentTimerTime < _timerDelay) {
080                            scheduleTimer(conditionalNG, symbolTable, _timerDelay - currentTimerTime);
081                        } else {
082                            _internalSocket.conditionalNG = conditionalNG;
083                            _internalSocket.newSymbolTable = newSymbolTable;
084                            conditionalNG.execute(_internalSocket);
085                        }
086                    }
087                } catch (RuntimeException | JmriException e) {
088                    log.error("Exception thrown", e);
089                }
090            }
091        };
092    }
093
094    private void scheduleTimer(ConditionalNG conditionalNG, SymbolTable symbolTable, long delay) throws JmriException {
095        if (_timerTask != null) _timerTask.stopTimer();
096        _timerTask = getNewTimerTask(conditionalNG, symbolTable);
097        TimerUtil.schedule(_timerTask, delay);
098    }
099
100    public LogixNG_SelectInteger getSelectDelay() {
101        return _selectDelay;
102    }
103
104    public LogixNG_SelectEnum<TimerUnit> getSelectTimerUnit() {
105        return _selectTimerUnit;
106    }
107
108    /** {@inheritDoc} */
109    @Override
110    public void execute() throws JmriException {
111        boolean result = _expressionSocket.evaluate();
112
113        synchronized(this) {
114            if (result) {
115                if (_timerTask != null) {
116                    _timerTask.stopTimer();
117                    _timerTask = null;
118                }
119                return;
120            }
121
122            // Don't restart timer if it's still running
123            if (_timerTask != null) return;
124
125            _timerDelay = _selectDelay.evaluateValue(getConditionalNG())
126                    * _selectTimerUnit.evaluateEnum(getConditionalNG()).getMultiply();
127            _timerStart = System.currentTimeMillis();
128            ConditionalNG conditonalNG = getConditionalNG();
129            scheduleTimer(conditonalNG, conditonalNG.getSymbolTable(), _timerDelay);
130        }
131    }
132
133    @Override
134    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
135        switch (index) {
136            case 0:
137                return _expressionSocket;
138
139            case 1:
140                return _actionSocket;
141
142            default:
143                throw new IllegalArgumentException(
144                        String.format("index has invalid value: %d", index));
145        }
146    }
147
148    @Override
149    public int getChildCount() {
150        return 2;
151    }
152
153    @Override
154    public void connected(FemaleSocket socket) {
155        if (socket == _expressionSocket) {
156            _expressionSocketSystemName = socket.getConnectedSocket().getSystemName();
157        } else if (socket == _actionSocket) {
158            _actionSocketSystemName = socket.getConnectedSocket().getSystemName();
159        } else {
160            throw new IllegalArgumentException("unkown socket");
161        }
162    }
163
164    @Override
165    public void disconnected(FemaleSocket socket) {
166        if (socket == _expressionSocket) {
167            _expressionSocketSystemName = null;
168        } else if (socket == _actionSocket) {
169            _actionSocketSystemName = null;
170        } else {
171            throw new IllegalArgumentException("unkown socket");
172        }
173    }
174
175    @Override
176    public String getShortDescription(Locale locale) {
177        return Bundle.getMessage(locale, "Timeout_Short");
178    }
179
180    @Override
181    public String getLongDescription(Locale locale) {
182        String delay = _selectDelay.getDescription(locale);
183
184        if ((_selectDelay.isDirectAddressing())
185                && (_selectTimerUnit.isDirectAddressing())) {
186
187            return Bundle.getMessage(locale,
188                    "Timeout_Long",
189                    _expressionSocket.getName(),
190                    _actionSocket.getName(),
191                    _selectTimerUnit.getEnum().getTimeWithUnit(_selectDelay.getValue()));
192        }
193
194        String timeUnit = _selectTimerUnit.getDescription(locale);
195
196        return Bundle.getMessage(locale,
197                "Timeout_Long_Indirect",
198                _expressionSocket.getName(),
199                _actionSocket.getName(),
200                delay,
201                timeUnit);
202    }
203
204    public FemaleDigitalExpressionSocket getExpressionSocket() {
205        return _expressionSocket;
206    }
207
208    public String getExpressionSocketSystemName() {
209        return _expressionSocketSystemName;
210    }
211
212    public void setExpressionSocketSystemName(String systemName) {
213        _expressionSocketSystemName = systemName;
214    }
215
216    public FemaleDigitalActionSocket getActionSocket() {
217        return _actionSocket;
218    }
219
220    public String getActionSocketSystemName() {
221        return _actionSocketSystemName;
222    }
223
224    public void setActionSocketSystemName(String systemName) {
225        _actionSocketSystemName = systemName;
226    }
227
228    /** {@inheritDoc} */
229    @Override
230    public void setup() {
231        try {
232            if ( !_expressionSocket.isConnected()
233                    || !_expressionSocket.getConnectedSocket().getSystemName()
234                            .equals(_expressionSocketSystemName)) {
235
236                String socketSystemName = _expressionSocketSystemName;
237                _expressionSocket.disconnect();
238                if (socketSystemName != null) {
239                    MaleSocket maleSocket =
240                            InstanceManager.getDefault(DigitalExpressionManager.class)
241                                    .getBySystemName(socketSystemName);
242                    if (maleSocket != null) {
243                        _expressionSocket.connect(maleSocket);
244                        maleSocket.setup();
245                    } else {
246                        log.error("cannot load digital expression {}", socketSystemName);
247                    }
248                }
249            } else {
250                _expressionSocket.getConnectedSocket().setup();
251            }
252
253            if ( !_actionSocket.isConnected()
254                    || !_actionSocket.getConnectedSocket().getSystemName()
255                            .equals(_actionSocketSystemName)) {
256
257                String socketSystemName = _actionSocketSystemName;
258                _actionSocket.disconnect();
259                if (socketSystemName != null) {
260                    MaleSocket maleSocket =
261                            InstanceManager.getDefault(DigitalActionManager.class)
262                                    .getBySystemName(socketSystemName);
263                    _actionSocket.disconnect();
264                    if (maleSocket != null) {
265                        _actionSocket.connect(maleSocket);
266                        maleSocket.setup();
267                    } else {
268                        log.error("cannot load digital action {}", socketSystemName);
269                    }
270                }
271            } else {
272                _actionSocket.getConnectedSocket().setup();
273            }
274        } catch (SocketAlreadyConnectedException ex) {
275            // This shouldn't happen and is a runtime error if it does.
276            throw new RuntimeException("socket is already connected");
277        }
278    }
279
280    /** {@inheritDoc} */
281    @Override
282    public void registerListenersForThisClass() {
283        _selectDelay.registerListeners();
284        _selectTimerUnit.registerListeners();
285    }
286
287    /** {@inheritDoc} */
288    @Override
289    public void unregisterListenersForThisClass() {
290        _selectDelay.unregisterListeners();
291        _selectTimerUnit.unregisterListeners();
292    }
293
294    /** {@inheritDoc} */
295    @Override
296    public void propertyChange(PropertyChangeEvent evt) {
297        getConditionalNG().execute();
298    }
299
300    /** {@inheritDoc} */
301    @Override
302    public void disposeMe() {
303        if (_timerTask != null) {
304            _timerTask.stopTimer();
305            _timerTask = null;
306        }
307    }
308
309
310    private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket {
311
312        private ConditionalNG conditionalNG;
313        private SymbolTable newSymbolTable;
314
315        public InternalFemaleSocket() {
316            super(null, new FemaleSocketListener(){
317                @Override
318                public void connected(FemaleSocket socket) {
319                    // Do nothing
320                }
321
322                @Override
323                public void disconnected(FemaleSocket socket) {
324                    // Do nothing
325                }
326            }, "A");
327        }
328
329        @Override
330        public void execute() throws JmriException {
331            if (conditionalNG == null) { throw new NullPointerException("conditionalNG is null"); }
332            if (_actionSocket != null) {
333                SymbolTable oldSymbolTable = conditionalNG.getSymbolTable();
334                conditionalNG.setSymbolTable(newSymbolTable);
335                _actionSocket.execute();
336                conditionalNG.setSymbolTable(oldSymbolTable);
337            }
338        }
339
340    }
341
342    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Timeout.class);
343
344}