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