001package jmri.jmrit.logixng.implementation;
002
003import java.util.*;
004
005import javax.annotation.Nonnull;
006import javax.swing.JOptionPane;
007
008import jmri.*;
009import jmri.jmrit.logixng.*;
010import jmri.jmrit.logixng.Module;
011import jmri.jmrit.logixng.Stack;
012import jmri.jmrit.logixng.util.LogixNG_Thread;
013import jmri.util.ThreadingUtil;
014
015/**
016 * The default implementation of ConditionalNG.
017 *
018 * @author Daniel Bergqvist Copyright 2019
019 */
020public class DefaultConditionalNG extends AbstractBase
021        implements ConditionalNG, FemaleSocketListener {
022
023    private final LogixNG_Thread _thread;
024    private int _startupThreadId;
025    private Base _parent = null;
026    private String _socketSystemName = null;
027    private final FemaleDigitalActionSocket _femaleSocket;
028    private boolean _enabled = true;
029    private boolean _executeAtStartup = true;
030    private final ExecuteLock _executeLock = new ExecuteLock();
031    private boolean _runDelayed = true;
032    private final Stack _stack = new DefaultStack();
033    private SymbolTable _symbolTable;
034
035
036    public DefaultConditionalNG(String sys, String user)
037            throws BadUserNameException, BadSystemNameException  {
038        this(sys, user, LogixNG_Thread.DEFAULT_LOGIXNG_THREAD);
039    }
040
041    public DefaultConditionalNG(String sys, String user, int threadID)
042            throws BadUserNameException, BadSystemNameException  {
043        super(sys, user);
044
045        _startupThreadId = threadID;
046        _thread = LogixNG_Thread.getThread(threadID);
047        _thread.setThreadInUse();
048
049        // Do this test here to ensure all the tests are using correct system names
050        Manager.NameValidity isNameValid = InstanceManager.getDefault(ConditionalNG_Manager.class).validSystemNameFormat(mSystemName);
051        if (isNameValid != Manager.NameValidity.VALID) {
052            throw new IllegalArgumentException("system name is not valid");
053        }
054        _femaleSocket = new DefaultFemaleDigitalActionSocket(this, this, "A");
055    }
056
057    /** {@inheritDoc} */
058    @Override
059    public LogixNG_Thread getCurrentThread() {
060        return _thread;
061    }
062
063    /** {@inheritDoc} */
064    @Override
065    public int getStartupThreadId() {
066        return _startupThreadId;
067    }
068
069    /** {@inheritDoc} */
070    @Override
071    public void setStartupThreadId(int threadId) {
072        int oldStartupThreadId = _startupThreadId;
073        _startupThreadId = threadId;
074        firePropertyChange("Thread", oldStartupThreadId, _startupThreadId);
075    }
076
077    /** {@inheritDoc} */
078    @Override
079    public Base getParent() {
080        return _parent;
081    }
082
083    /** {@inheritDoc} */
084    @Override
085    public void setParent(Base parent) {
086        _parent = parent;
087
088        if (isActive()) registerListeners();
089        else unregisterListeners();
090    }
091
092    /** {@inheritDoc} */
093    @Override
094    public FemaleDigitalActionSocket getFemaleSocket() {
095        return _femaleSocket;
096    }
097
098    /** {@inheritDoc} */
099    @Override
100    public void setRunDelayed(boolean value) {
101        _runDelayed = value;
102    }
103
104    /** {@inheritDoc} */
105    @Override
106    public boolean getRunDelayed() {
107        return _runDelayed;
108    }
109
110    private void runOnLogixNG_Thread(
111            @Nonnull ThreadingUtil.ThreadAction ta,
112            boolean allowRunDelayed) {
113
114        if (_runDelayed && allowRunDelayed) {
115            _thread.runOnLogixNGEventually(ta);
116        } else {
117            _thread.runOnLogixNG(ta);
118        }
119    }
120
121    /** {@inheritDoc} */
122    @Override
123    public void execute() {
124        if (_executeAtStartup || !getLogixNG().isStartup()) {
125            if (_executeLock.once()) {
126                runOnLogixNG_Thread(new ExecuteTask(this, _executeLock, null), true);
127            }
128        }
129    }
130
131    /** {@inheritDoc} */
132    @Override
133    public void execute(boolean allowRunDelayed) {
134        if (_executeLock.once()) {
135            runOnLogixNG_Thread(new ExecuteTask(this, _executeLock, null), allowRunDelayed);
136        }
137    }
138
139    /** {@inheritDoc} */
140    @Override
141    public void execute(FemaleDigitalActionSocket socket) {
142        runOnLogixNG_Thread(() -> {internalExecute(this, socket);}, true);
143    }
144
145    /**
146     * Executes a LogixNG Module.
147     * @param module      The module to be executed
148     * @param parameters  The parameters
149     * @throws IllegalArgumentException when needed
150     */
151    public static void executeModule(Module module, Map<String, Object> parameters)
152            throws IllegalArgumentException {
153
154        if (module == null) {
155            throw new IllegalArgumentException("The parameter \"module\" is null");
156        }
157        if (!(module.getRootSocket() instanceof DefaultFemaleDigitalActionSocket)) {
158            throw new IllegalArgumentException("The module " + module.getDisplayName() + " is not a DigitalActionModule");
159        }
160        if (parameters == null) {
161            throw new IllegalArgumentException("The parameter \"parameters\" is null");
162        }
163
164        LogixNG_Thread thread = LogixNG_Thread.getThread(LogixNG_Thread.DEFAULT_LOGIXNG_THREAD);
165        ConditionalNG conditionalNG = new DefaultConditionalNG("IQC0000000", null);
166        InternalFemaleSocket socket = new InternalFemaleSocket(conditionalNG, module, parameters);
167        thread.runOnLogixNGEventually(() -> { internalExecute(conditionalNG, socket); });
168    }
169
170    private static class InternalFemaleSocket extends DefaultFemaleDigitalActionSocket {
171
172        private final ConditionalNG _conditionalNG;
173        private final Module _module;
174        private final Map<String, Object> _parameters;
175
176        public InternalFemaleSocket(ConditionalNG conditionalNG, Module module, Map<String, Object> parameters) {
177            super(null, new FemaleSocketListener(){
178                @Override
179                public void connected(FemaleSocket socket) {
180                    // Do nothing
181                }
182
183                @Override
184                public void disconnected(FemaleSocket socket) {
185                    // Do nothing
186                }
187            }, "A");
188            _conditionalNG = conditionalNG;
189            _module = module;
190            _parameters = parameters;
191        }
192
193        @Override
194        public void execute() throws JmriException {
195            FemaleSocket socket = _module.getRootSocket();
196            if (!(socket instanceof DefaultFemaleDigitalActionSocket)) {
197                throw new IllegalArgumentException("The module " + _module.getDisplayName() + " is not a DigitalActionModule");
198            }
199
200            synchronized(this) {
201                SymbolTable oldSymbolTable = _conditionalNG.getSymbolTable();
202                DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(_conditionalNG);
203                List<Module.ParameterData> _parameterData = new ArrayList<>();
204                for (Module.Parameter p : _module.getParameters()) {
205                    _parameterData.add(new Module.ParameterData(
206                            p.getName(), SymbolTable.InitialValueType.None, "",
207                            Module.ReturnValueType.None, ""));
208                }
209                newSymbolTable.createSymbols(_conditionalNG.getSymbolTable(), _parameterData);
210                for (var entry : _parameters.entrySet()) {
211                    newSymbolTable.setValue(entry.getKey(), entry.getValue());
212                }
213                _conditionalNG.setSymbolTable(newSymbolTable);
214
215                ((DefaultFemaleDigitalActionSocket)socket).execute();
216                _conditionalNG.setSymbolTable(oldSymbolTable);
217            }
218        }
219    }
220
221    private static void internalExecute(ConditionalNG conditionalNG, FemaleDigitalActionSocket femaleSocket) {
222        if (conditionalNG.isEnabled()) {
223            DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(conditionalNG);
224
225            try {
226                conditionalNG.setCurrentConditionalNG(conditionalNG);
227
228                conditionalNG.setSymbolTable(newSymbolTable);
229
230                LogixNG logixNG = conditionalNG.getLogixNG();
231                InlineLogixNG inlineLogixNG = null;
232                if (logixNG != null) {
233                    inlineLogixNG = logixNG.getInlineLogixNG();
234                }
235                if (inlineLogixNG != null) {
236                    List<SymbolTable.VariableData> localVariables = new ArrayList<>();
237                    localVariables.add(new SymbolTable.VariableData(
238                            "__InlineLogixNG__", SymbolTable.InitialValueType.String,
239                            inlineLogixNG.getNameString()));
240//                    localVariables.add(new SymbolTable.VariableData(
241//                            "__PositionableId__", SymbolTable.InitialValueType.String,
242//                            inlineLogixNG.getId()));
243                    localVariables.add(new SymbolTable.VariableData(
244                            "__Editor__", SymbolTable.InitialValueType.String,
245                            inlineLogixNG.getEditorName()));
246                    newSymbolTable.createSymbols(localVariables);
247                }
248
249                if (femaleSocket != null) {
250                    femaleSocket.execute();
251                } else {
252                    conditionalNG.getFemaleSocket().execute();
253                }
254            } catch (AbortConditionalNG_IgnoreException | ReturnException | ExitException e) {
255                // A AbortConditionalNG_IgnoreException should be ignored.
256                // A Return action in a ConditionalNG causes a ReturnException so this is okay.
257                // An Exit action in a ConditionalNG causes a ExitException so this is okay.
258            } catch (ValidationErrorException e) {
259                ThreadingUtil.runOnGUI(()->
260                        JOptionPane.showMessageDialog(null,
261                                e.getMessage(),
262                                Bundle.getMessage("LogixNG_ValidationError"),
263                                JOptionPane.ERROR_MESSAGE)
264                );
265            } catch (PassThruException e) {
266                // This happens due to a a Break action or a Continue action that isn't handled.
267                log.info("ConditionalNG {} was aborted during execute: {}",
268                        conditionalNG.getSystemName(), e.getCause(), e.getCause());
269            } catch (AbortConditionalNGExecutionException e) {
270                if (InstanceManager.getDefault(LogixNGPreferences.class).getShowSystemNameInException()) {
271                    log.warn("ConditionalNG {} was aborted during execute in the item {}: {}",
272                            conditionalNG.getSystemName(), e.getMaleSocket().getSystemName(), e.getCause(), e.getCause());
273                } else {
274                    log.warn("ConditionalNG {} was aborted during execute: {}",
275                            conditionalNG.getSystemName(), e.getCause(), e.getCause());
276                }
277            } catch (JmriException | RuntimeException e) {
278//                LoggingUtil.warnOnce(log, "ConditionalNG {} got an exception during execute: {}",
279//                        conditionalNG.getSystemName(), e, e);
280                log.warn("ConditionalNG {} got an exception during execute: {}",
281                        conditionalNG.getSystemName(), e, e);
282            }
283
284            conditionalNG.setSymbolTable(newSymbolTable.getPrevSymbolTable());
285        }
286    }
287
288    private static class ExecuteTask implements ThreadingUtil.ThreadAction {
289
290        private final ConditionalNG _conditionalNG;
291        private final ExecuteLock _executeLock;
292        private final FemaleDigitalActionSocket _localFemaleSocket;
293
294        public ExecuteTask(ConditionalNG conditionalNG, ExecuteLock executeLock, FemaleDigitalActionSocket femaleSocket) {
295            _conditionalNG = conditionalNG;
296            _executeLock = executeLock;
297            _localFemaleSocket = femaleSocket;
298        }
299
300        @Override
301        public void run() {
302            while (_executeLock.loop()) {
303                internalExecute(_conditionalNG, _localFemaleSocket);
304            }
305        }
306
307    }
308
309    /**
310     * Set the current ConditionalNG.
311     * @param conditionalNG the current ConditionalNG
312     */
313    @Override
314    public void setCurrentConditionalNG(ConditionalNG conditionalNG) {
315        if (this != conditionalNG) {
316            throw new UnsupportedOperationException("The new conditionalNG must be the same as myself");
317        }
318        for (Module m : InstanceManager.getDefault(ModuleManager.class).getNamedBeanSet()) {
319            m.setCurrentConditionalNG(conditionalNG);
320        }
321    }
322
323    /** {@inheritDoc} */
324    @Override
325    public Stack getStack() {
326        return _stack;
327    }
328
329    /** {@inheritDoc} */
330    @Override
331    public SymbolTable getSymbolTable() {
332        return _symbolTable;
333    }
334
335    /** {@inheritDoc} */
336    @Override
337    public void setSymbolTable(SymbolTable symbolTable) {
338        _symbolTable = symbolTable;
339    }
340
341    @Override
342    public String getBeanType() {
343        return Bundle.getMessage("BeanNameConditionalNG");
344    }
345
346    @Override
347    public void setState(int s) throws JmriException {
348        log.warn("Unexpected call to setState in DefaultConditionalNG.");  // NOI18N
349    }
350
351    @Override
352    public int getState() {
353        log.warn("Unexpected call to getState in DefaultConditionalNG.");  // NOI18N
354        return UNKNOWN;
355    }
356
357    @Override
358    public void connected(FemaleSocket socket) {
359        _socketSystemName = socket.getConnectedSocket().getSystemName();
360    }
361
362    @Override
363    public void disconnected(FemaleSocket socket) {
364        _socketSystemName = null;
365    }
366
367    @Override
368    public String getShortDescription(Locale locale) {
369        return "ConditionalNG: "+getDisplayName();
370    }
371
372    @Override
373    public String getLongDescription(Locale locale) {
374        if (_thread.getThreadId() != LogixNG_Thread.DEFAULT_LOGIXNG_THREAD) {
375            return "ConditionalNG: "+getDisplayName() + " on thread " + _thread.getThreadName();
376        }
377        return "ConditionalNG: "+getDisplayName();
378    }
379
380    @Override
381    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
382        if (index != 0) {
383            throw new IllegalArgumentException(
384                    String.format("index has invalid value: %d", index));
385        }
386
387        return _femaleSocket;
388    }
389
390    @Override
391    public int getChildCount() {
392        return 1;
393    }
394
395    @Override
396    public Category getCategory() {
397        throw new UnsupportedOperationException("Not supported.");
398    }
399
400    @Override
401    public void setSocketSystemName(String systemName) {
402        if ((systemName == null) || (!systemName.equals(_socketSystemName))) {
403            _femaleSocket.disconnect();
404        }
405        _socketSystemName = systemName;
406    }
407
408    @Override
409    public String getSocketSystemName() {
410        return _socketSystemName;
411    }
412
413    /** {@inheritDoc} */
414    @Override
415    final public void setup() {
416        if (!_femaleSocket.isConnected()
417                || !_femaleSocket.getConnectedSocket().getSystemName()
418                        .equals(_socketSystemName)) {
419
420            _femaleSocket.disconnect();
421
422            if (_socketSystemName != null) {
423                try {
424                    MaleSocket maleSocket =
425                            InstanceManager.getDefault(DigitalActionManager.class)
426                                    .getBySystemName(_socketSystemName);
427                    if (maleSocket != null) {
428                        _femaleSocket.connect(maleSocket);
429                        maleSocket.setup();
430                    } else {
431                        log.error("digital action is not found: {}", _socketSystemName);
432                    }
433                } catch (SocketAlreadyConnectedException ex) {
434                    // This shouldn't happen and is a runtime error if it does.
435                    throw new RuntimeException("socket is already connected");
436                }
437            }
438        } else {
439            _femaleSocket.setup();
440        }
441    }
442
443    /** {@inheritDoc} */
444    @Override
445    final public void disposeMe() {
446        _femaleSocket.dispose();
447    }
448
449    /** {@inheritDoc} */
450    @Override
451    public void setEnabled(boolean enable) {
452        _enabled = enable;
453        if (isActive()) {
454            LogixNG logixNG = getLogixNG();
455            if ((logixNG != null) && logixNG.isActive()) {
456                registerListeners();
457                if (_executeAtStartup) {
458                    execute();
459                }
460            }
461        } else {
462            unregisterListeners();
463        }
464    }
465
466    /** {@inheritDoc} */
467    @Override
468    public boolean isEnabled() {
469        return _enabled;
470    }
471
472    /** {@inheritDoc} */
473    @Override
474    public void setExecuteAtStartup(boolean value) {
475        _executeAtStartup = value;
476    }
477
478    /** {@inheritDoc} */
479    @Override
480    public boolean isExecuteAtStartup() {
481        return _executeAtStartup;
482    }
483
484    /** {@inheritDoc} */
485    @Override
486    public synchronized boolean isListenersRegistered() {
487        return _listenersAreRegistered;
488    }
489
490    /** {@inheritDoc} */
491    @Override
492    public synchronized void registerListenersForThisClass() {
493        _listenersAreRegistered = true;
494    }
495
496    /** {@inheritDoc} */
497    @Override
498    public synchronized void unregisterListenersForThisClass() {
499        _listenersAreRegistered = false;
500    }
501
502    @Override
503    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) {
504        throw new UnsupportedOperationException("Not supported yet.");
505    }
506
507    @Override
508    public boolean existsInTree() {
509        return true;
510    }
511
512    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultConditionalNG.class);
513
514}