001package jmri.jmrit.logixng.actions;
002
003import jmri.util.TimerUnit;
004
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 {
021
022    private ProtectedTimerTask _timerTask;
023    private final LogixNG_SelectInteger _selectDelay = new LogixNG_SelectInteger(this);
024    private final LogixNG_SelectEnum<TimerUnit> _selectTimerUnit =
025            new LogixNG_SelectEnum<>(this, TimerUnit.values(), TimerUnit.MilliSeconds);
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 LogixNG_Category getCategory() {
060        return LogixNG_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 disposeMe() {
281        if (_timerTask != null) {
282            _timerTask.stopTimer();
283            _timerTask = null;
284        }
285    }
286
287
288    private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket {
289
290        private ConditionalNG conditionalNG;
291        private SymbolTable newSymbolTable;
292
293        public InternalFemaleSocket() {
294            super(null, new FemaleSocketListener(){
295                @Override
296                public void connected(FemaleSocket socket) {
297                    // Do nothing
298                }
299
300                @Override
301                public void disconnected(FemaleSocket socket) {
302                    // Do nothing
303                }
304            }, "A");
305        }
306
307        @Override
308        public void execute() throws JmriException {
309            if (conditionalNG == null) { throw new NullPointerException("conditionalNG is null"); }
310            if (_actionSocket != null) {
311                SymbolTable oldSymbolTable = conditionalNG.getSymbolTable();
312                conditionalNG.setSymbolTable(newSymbolTable);
313                _actionSocket.execute();
314                conditionalNG.setSymbolTable(oldSymbolTable);
315            }
316        }
317
318    }
319
320    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Timeout.class);
321
322}