001package jmri.jmrit.logixng.actions;
002
003import java.util.*;
004
005import jmri.jmrit.logixng.util.TimerUnit;
006
007import javax.annotation.Nonnull;
008
009import jmri.InstanceManager;
010import jmri.JmriException;
011import jmri.jmrit.logixng.*;
012import jmri.jmrit.logixng.implementation.DefaultSymbolTable;
013import jmri.jmrit.logixng.util.*;
014import jmri.jmrit.logixng.util.parser.*;
015import jmri.util.TimerUtil;
016import jmri.util.TypeConversionUtil;
017
018/**
019 * Executes a digital action delayed.
020 * 
021 * @author Daniel Bergqvist Copyright 2021
022 */
023public class ExecuteDelayed
024        extends AbstractDigitalAction
025        implements FemaleSocketListener {
026
027    private String _socketSystemName;
028    private final FemaleDigitalActionSocket _socket;
029    private ProtectedTimerTask _defaultTimerTask;
030    private NamedBeanAddressing _stateAddressing = NamedBeanAddressing.Direct;
031    private int _delay;
032    private String _stateReference = "";
033    private String _stateLocalVariable = "";
034    private String _stateFormula = "";
035    private ExpressionNode _stateExpressionNode;
036    private TimerUnit _unit = TimerUnit.MilliSeconds;
037    private boolean _resetIfAlreadyStarted;
038    private boolean _useIndividualTimers;
039    
040    private final InternalFemaleSocket _defaultInternalSocket = new InternalFemaleSocket();
041    
042    
043    public ExecuteDelayed(String sys, String user) {
044        super(sys, user);
045        _socket = InstanceManager.getDefault(DigitalActionManager.class)
046                .createFemaleSocket(this, this, "A");
047    }
048    
049    @Override
050    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
051        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
052        String sysName = systemNames.get(getSystemName());
053        String userName = userNames.get(getSystemName());
054        if (sysName == null) sysName = manager.getAutoSystemName();
055        ExecuteDelayed copy = new ExecuteDelayed(sysName, userName);
056        copy.setComment(getComment());
057        copy.setDelayAddressing(_stateAddressing);
058        copy.setDelay(_delay);
059        copy.setDelayFormula(_stateFormula);
060        copy.setDelayLocalVariable(_stateLocalVariable);
061        copy.setDelayReference(_stateReference);
062        copy.setUnit(_unit);
063        copy.setResetIfAlreadyStarted(_resetIfAlreadyStarted);
064        copy.setUseIndividualTimers(_useIndividualTimers);
065        return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames);
066    }
067    
068    /** {@inheritDoc} */
069    @Override
070    public Category getCategory() {
071        return Category.COMMON;
072    }
073/*
074    private String getVariables(SymbolTable symbolTable) {
075        java.io.StringWriter stringWriter = new java.io.StringWriter();
076        java.io.PrintWriter writer = new java.io.PrintWriter(stringWriter);
077        symbolTable.printSymbolTable(writer);
078        return stringWriter.toString();
079    }
080*/    
081    /**
082     * Get a new timer task.
083     * @param conditionalNG  the ConditionalNG
084     * @param symbolTable    the symbol table
085     * @param timerDelay     the time the timer should wait
086     * @param timerStart     the time when the timer was started
087     */
088    private ProtectedTimerTask getNewTimerTask(ConditionalNG conditionalNG, SymbolTable symbolTable, long timerDelay, long timerStart) throws JmriException {
089
090        DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(symbolTable);
091        
092        return new ProtectedTimerTask() {
093            @Override
094            public void execute() {
095                try {
096                    synchronized(ExecuteDelayed.this) {
097                        if (!_useIndividualTimers) _defaultTimerTask = null;
098                        long currentTime = System.currentTimeMillis();
099                        long currentTimerTime = currentTime - timerStart;
100                        if (currentTimerTime < timerDelay) {
101                            scheduleTimer(conditionalNG, symbolTable, timerDelay - currentTimerTime, currentTime);
102                        } else {
103                            InternalFemaleSocket internalSocket;
104                            if (_useIndividualTimers) {
105                                internalSocket = new InternalFemaleSocket();
106                            } else {
107                                internalSocket = _defaultInternalSocket;
108                            }
109                            internalSocket.conditionalNG = conditionalNG;
110                            internalSocket.newSymbolTable = newSymbolTable;
111                            conditionalNG.execute(internalSocket);
112                        }
113                    }
114                } catch (RuntimeException | JmriException e) {
115                    log.error("Exception thrown", e);
116                }
117            }
118        };
119    }
120    
121    private void scheduleTimer(ConditionalNG conditionalNG, SymbolTable symbolTable, long timerDelay, long timerStart) throws JmriException {
122        synchronized(ExecuteDelayed.this) {
123            if (!_useIndividualTimers && (_defaultTimerTask != null)) {
124                _defaultTimerTask.stopTimer();
125            }
126            ProtectedTimerTask timerTask =
127                    getNewTimerTask(conditionalNG, symbolTable, timerDelay, timerStart);
128            if (!_useIndividualTimers) {
129                _defaultTimerTask = timerTask;
130            }
131            TimerUtil.schedule(timerTask, timerDelay);
132        }
133    }
134    
135    private long getNewDelay() throws JmriException {
136        
137        switch (_stateAddressing) {
138            case Direct:
139                return _delay;
140                
141            case Reference:
142                return TypeConversionUtil.convertToLong(ReferenceUtil.getReference(
143                        getConditionalNG().getSymbolTable(), _stateReference));
144                
145            case LocalVariable:
146                SymbolTable symbolTable = getConditionalNG().getSymbolTable();
147                return TypeConversionUtil
148                        .convertToLong(symbolTable.getValue(_stateLocalVariable));
149                
150            case Formula:
151                return _stateExpressionNode != null
152                        ? TypeConversionUtil.convertToLong(
153                                _stateExpressionNode.calculate(
154                                        getConditionalNG().getSymbolTable()))
155                        : 0;
156                
157            default:
158                throw new IllegalArgumentException("invalid _addressing state: " + _stateAddressing.name());
159        }
160    }
161    
162    /** {@inheritDoc} */
163    @Override
164    public void execute() throws JmriException {
165        synchronized(this) {
166            if (!_useIndividualTimers && (_defaultTimerTask != null)) {
167                if (_resetIfAlreadyStarted) _defaultTimerTask.stopTimer();
168                else return;
169            }
170            long timerDelay = getNewDelay() * _unit.getMultiply();
171            long timerStart = System.currentTimeMillis();
172            ConditionalNG conditonalNG = getConditionalNG();
173            scheduleTimer(conditonalNG, conditonalNG.getSymbolTable(), timerDelay, timerStart);
174        }
175    }
176    
177    public void setDelayAddressing(NamedBeanAddressing addressing) throws ParserException {
178        _stateAddressing = addressing;
179        parseDelayFormula();
180    }
181    
182    public NamedBeanAddressing getDelayAddressing() {
183        return _stateAddressing;
184    }
185    
186    /**
187     * Get the delay.
188     * @return the delay
189     */
190    public int getDelay() {
191        return _delay;
192    }
193    
194    /**
195     * Set the delay.
196     * @param delay the delay
197     */
198    public void setDelay(int delay) {
199        _delay = delay;
200    }
201    
202    public void setDelayReference(@Nonnull String reference) {
203        if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) {
204            throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference");
205        }
206        _stateReference = reference;
207    }
208    
209    public String getDelayReference() {
210        return _stateReference;
211    }
212    
213    public void setDelayLocalVariable(@Nonnull String localVariable) {
214        _stateLocalVariable = localVariable;
215    }
216    
217    public String getDelayLocalVariable() {
218        return _stateLocalVariable;
219    }
220    
221    public void setDelayFormula(@Nonnull String formula) throws ParserException {
222        _stateFormula = formula;
223        parseDelayFormula();
224    }
225    
226    public String getDelayFormula() {
227        return _stateFormula;
228    }
229    
230    private void parseDelayFormula() throws ParserException {
231        if (_stateAddressing == NamedBeanAddressing.Formula) {
232            Map<String, Variable> variables = new HashMap<>();
233            
234            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
235            _stateExpressionNode = parser.parseExpression(_stateFormula);
236        } else {
237            _stateExpressionNode = null;
238        }
239    }
240    
241    /**
242     * Get the unit
243     * @return the unit
244     */
245    public TimerUnit getUnit() {
246        return _unit;
247    }
248    
249    /**
250     * Set the unit
251     * @param unit the unit
252     */
253    public void setUnit(TimerUnit unit) {
254        _unit = unit;
255    }
256    
257    /**
258     * Get reset if timer is already started.
259     * @return true if the timer should be reset if this action is executed
260     *         while timer is ticking, false othervise
261     */
262    public boolean getResetIfAlreadyStarted() {
263        return _resetIfAlreadyStarted;
264    }
265    
266    /**
267     * Set reset if timer is already started.
268     * @param resetIfAlreadyStarted true if the timer should be reset if this
269     *                              action is executed while timer is ticking,
270     *                              false othervise
271     */
272    public void setResetIfAlreadyStarted(boolean resetIfAlreadyStarted) {
273        _resetIfAlreadyStarted = resetIfAlreadyStarted;
274    }
275    
276    /**
277     * Get use individual timers.
278     * @return true if the timer should use individual timers, false othervise
279     */
280    public boolean getUseIndividualTimers() {
281        return _useIndividualTimers;
282    }
283    
284    /**
285     * Set reset if timer is already started.
286     * @param useIndividualTimers true if the timer should use individual timers,
287     *                              false othervise
288     */
289    public void setUseIndividualTimers(boolean useIndividualTimers) {
290        _useIndividualTimers = useIndividualTimers;
291    }
292    
293    @Override
294    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
295        switch (index) {
296            case 0:
297                return _socket;
298                
299            default:
300                throw new IllegalArgumentException(
301                        String.format("index has invalid value: %d", index));
302        }
303    }
304
305    @Override
306    public int getChildCount() {
307        return 1;
308    }
309
310    @Override
311    public void connected(FemaleSocket socket) {
312        if (socket == _socket) {
313            _socketSystemName = socket.getConnectedSocket().getSystemName();
314        } else {
315            throw new IllegalArgumentException("unkown socket");
316        }
317    }
318
319    @Override
320    public void disconnected(FemaleSocket socket) {
321        if (socket == _socket) {
322            _socketSystemName = null;
323        } else {
324            throw new IllegalArgumentException("unkown socket");
325        }
326    }
327
328    @Override
329    public String getShortDescription(Locale locale) {
330        return Bundle.getMessage(locale, "ExecuteDelayed_Short");
331    }
332
333    @Override
334    public String getLongDescription(Locale locale) {
335        String delay;
336        
337        switch (_stateAddressing) {
338            case Direct:
339                delay = Bundle.getMessage(locale, "ExecuteDelayed_DelayByDirect", _unit.getTimeWithUnit(_delay));
340                break;
341                
342            case Reference:
343                delay = Bundle.getMessage(locale, "ExecuteDelayed_DelayByReference", _stateReference, _unit.toString());
344                break;
345                
346            case LocalVariable:
347                delay = Bundle.getMessage(locale, "ExecuteDelayed_DelayByLocalVariable", _stateLocalVariable, _unit.toString());
348                break;
349                
350            case Formula:
351                delay = Bundle.getMessage(locale, "ExecuteDelayed_DelayByFormula", _stateFormula, _unit.toString());
352                break;
353                
354            default:
355                throw new IllegalArgumentException("invalid _stateAddressing state: " + _stateAddressing.name());
356        }
357        
358        return Bundle.getMessage(locale,
359                "ExecuteDelayed_Long",
360                _socket.getName(),
361                delay,
362                _resetIfAlreadyStarted
363                        ? Bundle.getMessage("ExecuteDelayed_Options", Bundle.getMessage("ExecuteDelayed_ResetRepeat"))
364                        : Bundle.getMessage("ExecuteDelayed_Options", Bundle.getMessage("ExecuteDelayed_IgnoreRepeat")),
365                _useIndividualTimers
366                        ? Bundle.getMessage("ExecuteDelayed_Options", Bundle.getMessage("ExecuteDelayed_UseIndividualTimers"))
367                        : "");
368    }
369
370    public FemaleDigitalActionSocket getSocket() {
371        return _socket;
372    }
373
374    public String getSocketSystemName() {
375        return _socketSystemName;
376    }
377
378    public void setSocketSystemName(String systemName) {
379        _socketSystemName = systemName;
380    }
381
382    /** {@inheritDoc} */
383    @Override
384    public void setup() {
385        try {
386            if (!_socket.isConnected()
387                    || !_socket.getConnectedSocket().getSystemName()
388                            .equals(_socketSystemName)) {
389                
390                String socketSystemName = _socketSystemName;
391                
392                _socket.disconnect();
393                
394                if (socketSystemName != null) {
395                    MaleSocket maleSocket =
396                            InstanceManager.getDefault(DigitalActionManager.class)
397                                    .getBySystemName(socketSystemName);
398                    if (maleSocket != null) {
399                        _socket.connect(maleSocket);
400                        maleSocket.setup();
401                    } else {
402                        log.error("cannot load analog action {}", socketSystemName);
403                    }
404                }
405            } else {
406                _socket.getConnectedSocket().setup();
407            }
408        } catch (SocketAlreadyConnectedException ex) {
409            // This shouldn't happen and is a runtime error if it does.
410            throw new RuntimeException("socket is already connected");
411        }
412    }
413    
414    /** {@inheritDoc} */
415    @Override
416    public void registerListenersForThisClass() {
417    }
418    
419    /** {@inheritDoc} */
420    @Override
421    public void unregisterListenersForThisClass() {
422        synchronized(ExecuteDelayed.this) {
423            if (!_useIndividualTimers && (_defaultTimerTask != null)) {
424                _defaultTimerTask.stopTimer();
425                _defaultTimerTask = null;
426            }
427        }
428    }
429    
430    /** {@inheritDoc} */
431    @Override
432    public void disposeMe() {
433    }
434    
435    
436    
437    private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket {
438        
439        private ConditionalNG conditionalNG;
440        private SymbolTable newSymbolTable;
441        
442        public InternalFemaleSocket() {
443            super(null, new FemaleSocketListener(){
444                @Override
445                public void connected(FemaleSocket socket) {
446                    // Do nothing
447                }
448
449                @Override
450                public void disconnected(FemaleSocket socket) {
451                    // Do nothing
452                }
453            }, "A");
454        }
455        
456        @Override
457        public void execute() throws JmriException {
458            if (conditionalNG == null) { throw new NullPointerException("conditionalNG is null"); }
459            if (_socket != null) {
460                SymbolTable oldSymbolTable = conditionalNG.getSymbolTable();
461                conditionalNG.setSymbolTable(newSymbolTable);
462                _socket.execute();
463                conditionalNG.setSymbolTable(oldSymbolTable);
464            }
465        }
466        
467    }
468    
469    
470    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExecuteDelayed.class);
471    
472}