001package jmri.jmrit.logixng.actions;
002
003import java.util.*;
004import java.util.concurrent.atomic.AtomicInteger;
005
006import jmri.*;
007import jmri.jmrit.logixng.*;
008import jmri.jmrit.logixng.implementation.DefaultSymbolTable;
009import jmri.jmrit.logixng.util.LogixNG_SelectComboBox;
010import jmri.jmrit.logixng.util.LogixNG_SelectEnum;
011import jmri.jmrit.logixng.util.LogixNG_SelectInteger;
012
013/**
014 * Program a CV on main.
015 *
016 * @author Daniel Bergqvist Copyright 2024
017 */
018public class ProgramOnMain extends AbstractDigitalAction
019        implements FemaleSocketListener {
020
021    private static final ResourceBundle rbx =
022            ResourceBundle.getBundle("jmri.jmrit.logixng.implementation.ImplementationBundle");
023
024    private String _executeSocketSystemName;
025    private final FemaleDigitalActionSocket _executeSocket;
026    private SystemConnectionMemo _memo;
027    private AddressedProgrammerManager _programmerManager;
028    private ThrottleManager _throttleManager;
029    private final LogixNG_SelectComboBox _selectProgrammingMode;
030    private final LogixNG_SelectEnum<LongOrShortAddress> _selectLongOrShortAddress =
031            new LogixNG_SelectEnum<>(this, LongOrShortAddress.values(), LongOrShortAddress.Auto);
032    private final LogixNG_SelectInteger _selectAddress = new LogixNG_SelectInteger(this);
033    private final LogixNG_SelectInteger _selectCV = new LogixNG_SelectInteger(this);
034    private final LogixNG_SelectInteger _selectValue = new LogixNG_SelectInteger(this);
035    private String _localVariableForStatus = "";
036    private boolean _wait = true;
037    private final InternalFemaleSocket _internalSocket = new InternalFemaleSocket();
038
039    public ProgramOnMain(String sys, String user) {
040        super(sys, user);
041
042        // The array is updated with correct values when setMemo() is called
043        POMItem[] modes = {new POMItem("")};
044        _selectProgrammingMode = new LogixNG_SelectComboBox(this, modes, modes[0]);
045
046        // Set the _programmerManager and _throttleManager variables
047        setMemo(null);
048
049        _executeSocket = InstanceManager.getDefault(DigitalActionManager.class)
050                .createFemaleSocket(this, this, Bundle.getMessage("ProgramOnMain_Socket"));
051    }
052
053    @Override
054    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
055        DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class);
056        String sysName = systemNames.get(getSystemName());
057        String userName = userNames.get(getSystemName());
058        if (sysName == null) sysName = manager.getAutoSystemName();
059        ProgramOnMain copy = new ProgramOnMain(sysName, userName);
060        copy.setComment(getComment());
061        copy.setMemo(_memo);
062        _selectProgrammingMode.copy(copy._selectProgrammingMode);
063        _selectLongOrShortAddress.copy(copy._selectLongOrShortAddress);
064        _selectAddress.copy(copy._selectAddress);
065        _selectCV.copy(copy._selectCV);
066        _selectValue.copy(copy._selectValue);
067        copy._wait = _wait;
068        copy.setLocalVariableForStatus(_localVariableForStatus);
069        return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames);
070    }
071
072    public final LogixNG_SelectComboBox getSelectProgrammingMode() {
073        return _selectProgrammingMode;
074    }
075
076    public final LogixNG_SelectInteger getSelectAddress() {
077        return _selectAddress;
078    }
079
080    public LogixNG_SelectEnum<LongOrShortAddress> getSelectLongOrShortAddress() {
081        return _selectLongOrShortAddress;
082    }
083
084    public final LogixNG_SelectInteger getSelectCV() {
085        return _selectCV;
086    }
087
088    public final LogixNG_SelectInteger getSelectValue() {
089        return _selectValue;
090    }
091
092    public void setLocalVariableForStatus(String localVariable) {
093        _localVariableForStatus = localVariable;
094    }
095
096    public String getLocalVariableForStatus() {
097        return _localVariableForStatus;
098    }
099
100    public void setWait(boolean wait) {
101        _wait = wait;
102    }
103
104    public boolean getWait() {
105        return _wait;
106    }
107
108    public final void setMemo(SystemConnectionMemo memo) {
109        assertListenersAreNotRegistered(log, "setMemo");
110
111        _memo = memo;
112        if (_memo != null) {
113            _programmerManager = _memo.get(AddressedProgrammerManager.class);
114            _throttleManager = _memo.get(ThrottleManager.class);
115            if (_throttleManager == null) {
116                throw new IllegalArgumentException("Memo "+memo.getUserName()+" doesn't have a ThrottleManager");
117            }
118
119            // LocoNet memo doesn't have a programmer during tests
120            if (_programmerManager == null) {
121                _programmerManager = InstanceManager.getDefault(AddressedProgrammerManager.class);
122            }
123        } else {
124            _programmerManager = InstanceManager.getDefault(AddressedProgrammerManager.class);
125            _throttleManager = InstanceManager.getDefault(ThrottleManager.class);
126        }
127
128        List<POMItem> modeList = new ArrayList<>();
129        for (ProgrammingMode mode : _programmerManager.getDefaultModes()) {
130            log.debug("Available programming mode: {}", mode);
131            modeList.add(new POMItem(mode.getStandardName()));
132        }
133
134        // Add OPSBYTEMODE in case we don't have any mode,
135        // for example if we are running a simulator.
136        if (modeList.isEmpty()) {
137            modeList.add(new POMItem(ProgrammingMode.OPSBYTEMODE.getStandardName()));
138        }
139
140        POMItem[] modes = modeList.toArray(POMItem[]::new);
141        _selectProgrammingMode.setValues(modes);
142    }
143
144    public final SystemConnectionMemo getMemo() {
145        return _memo;
146    }
147
148    /** {@inheritDoc} */
149    @Override
150    public LogixNG_Category getCategory() {
151        return LogixNG_Category.ITEM;
152    }
153
154    private void doProgrammingOnMain(ConditionalNG conditionalNG,
155            DefaultSymbolTable newSymbolTable, ProgrammingMode progMode,
156            int address, LongOrShortAddress longOrShort, int cv, int value, boolean wait)
157            throws JmriException {
158        try {
159            boolean longAddress;
160
161            switch (longOrShort) {
162                case Short:
163                    longAddress = false;
164                    break;
165
166                case Long:
167                    longAddress = true;
168                    break;
169
170                case Auto:
171                    longAddress = !_throttleManager.canBeShortAddress(address);
172                    break;
173
174                default:
175                    throw new IllegalArgumentException("longOrShort has unknown value");
176            }
177
178            AddressedProgrammer programmer = _programmerManager.getAddressedProgrammer(
179                    new DccLocoAddress(address, longAddress));
180
181            if (programmer != null) {
182                programmer.setMode(progMode);
183                if (!progMode.equals(programmer.getMode())) {
184                    throw new IllegalArgumentException("The addressed programmer doesn't support mode " + progMode.getStandardName());
185                }
186                AtomicInteger result = new AtomicInteger(-1);
187                programmer.writeCV("" + cv, value, (int value1, int status) -> {
188                    result.set(status);
189
190                    log.debug("Result of programming cv {} to value {} for address {}: {}", cv, value, address, status);
191
192                    synchronized(ProgramOnMain.this) {
193                        _internalSocket.conditionalNG = conditionalNG;
194                        _internalSocket.newSymbolTable = newSymbolTable;
195                        _internalSocket.status = status;
196                        conditionalNG.execute(_internalSocket);
197                    }
198                });
199
200                if (wait) {
201                    try {
202                        while (result.get() == -1) {
203                            Thread.sleep(10);
204                        }
205                    } catch (InterruptedException e) {
206                        log.warn("Waiting for programmer to complete was aborted");
207                    }
208                }
209
210            } else {
211                throw new IllegalArgumentException("An addressed programmer isn't available for address " + address);
212            }
213        } catch (ProgrammerException e) {
214            throw new JmriException(e);
215        }
216    }
217
218    /** {@inheritDoc} */
219    @Override
220    public void execute() throws JmriException {
221        ConditionalNG conditionalNG = this.getConditionalNG();
222        DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(conditionalNG.getSymbolTable());
223
224        String progModeStr = _selectProgrammingMode.evaluateValue(conditionalNG).getKey();
225
226        ProgrammingMode progMode = null;
227        for (ProgrammingMode mode : _programmerManager.getDefaultModes()) {
228            if (mode.getStandardName().equals(progModeStr)) {
229                progMode = mode;
230            }
231        }
232
233        if (progMode == null) {
234            throw new IllegalArgumentException("Programming mode "+progModeStr+" is not found");
235        }
236
237        int address = _selectAddress.evaluateValue(conditionalNG);
238        LongOrShortAddress longOrShort = _selectLongOrShortAddress.evaluateEnum(conditionalNG);
239        int cv = _selectCV.evaluateValue(conditionalNG);
240        int value = _selectValue.evaluateValue(conditionalNG);
241
242        doProgrammingOnMain(conditionalNG, newSymbolTable, progMode, address, longOrShort, cv, value, _wait);
243    }
244
245    @Override
246    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
247        switch (index) {
248            case 0:
249                return _executeSocket;
250
251            default:
252                throw new IllegalArgumentException(
253                        String.format("index has invalid value: %d", index));
254        }
255    }
256
257    @Override
258    public int getChildCount() {
259        return 1;
260    }
261
262    @Override
263    public void connected(FemaleSocket socket) {
264        if (socket == _executeSocket) {
265            _executeSocketSystemName = socket.getConnectedSocket().getSystemName();
266        } else {
267            throw new IllegalArgumentException("unkown socket");
268        }
269    }
270
271    @Override
272    public void disconnected(FemaleSocket socket) {
273        if (socket == _executeSocket) {
274            _executeSocketSystemName = null;
275        } else {
276            throw new IllegalArgumentException("unkown socket");
277        }
278    }
279
280    @Override
281    public String getShortDescription(Locale locale) {
282        return Bundle.getMessage(locale, "ProgramOnMain_Short");
283    }
284
285    @Override
286    public String getLongDescription(Locale locale) {
287        if (_memo != null) {
288            return Bundle.getMessage(locale, "ProgramOnMain_LongConnection",
289                    _selectLongOrShortAddress.getDescription(locale),
290                    _selectAddress.getDescription(locale, false),
291                    _selectCV.getDescription(locale, false),
292                    _selectValue.getDescription(locale, false),
293                    _selectProgrammingMode.getDescription(locale),
294                    _memo.getUserName());
295        } else {
296            return Bundle.getMessage(locale, "ProgramOnMain_Long",
297                    _selectLongOrShortAddress.getDescription(locale),
298                    _selectAddress.getDescription(locale, false),
299                    _selectCV.getDescription(locale, false),
300                    _selectValue.getDescription(locale, false),
301                    _selectProgrammingMode.getDescription(locale));
302        }
303    }
304
305    public FemaleDigitalActionSocket getExecuteSocket() {
306        return _executeSocket;
307    }
308
309    public String getExecuteSocketSystemName() {
310        return _executeSocketSystemName;
311    }
312
313    public void setExecuteSocketSystemName(String systemName) {
314        _executeSocketSystemName = systemName;
315    }
316
317    /** {@inheritDoc} */
318    @Override
319    public void setup() {
320        try {
321            if (!_executeSocket.isConnected()
322                    || !_executeSocket.getConnectedSocket().getSystemName()
323                            .equals(_executeSocketSystemName)) {
324
325                String socketSystemName = _executeSocketSystemName;
326
327                _executeSocket.disconnect();
328
329                if (socketSystemName != null) {
330                    MaleSocket maleSocket =
331                            InstanceManager.getDefault(DigitalActionManager.class)
332                                    .getBySystemName(socketSystemName);
333                    if (maleSocket != null) {
334                        _executeSocket.connect(maleSocket);
335                        maleSocket.setup();
336                    } else {
337                        log.error("cannot load digital action {}", socketSystemName);
338                    }
339                }
340            } else {
341                _executeSocket.getConnectedSocket().setup();
342            }
343        } catch (SocketAlreadyConnectedException ex) {
344            // This shouldn't happen and is a runtime error if it does.
345            throw new RuntimeException("socket is already connected");
346        }
347    }
348
349
350    private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket {
351
352        private ConditionalNG conditionalNG;
353        private SymbolTable newSymbolTable;
354        private int status;
355
356        public InternalFemaleSocket() {
357            super(null, new FemaleSocketListener(){
358                @Override
359                public void connected(FemaleSocket socket) {
360                    // Do nothing
361                }
362
363                @Override
364                public void disconnected(FemaleSocket socket) {
365                    // Do nothing
366                }
367            }, "A");
368        }
369
370        @Override
371        public void execute() throws JmriException {
372            if (_executeSocket != null) {
373                MaleSocket maleSocket = (MaleSocket)ProgramOnMain.this.getParent();
374                try {
375                    SymbolTable oldSymbolTable = conditionalNG.getSymbolTable();
376                    conditionalNG.setSymbolTable(newSymbolTable);
377                    if (!_localVariableForStatus.isEmpty()) {
378                        newSymbolTable.setValue(_localVariableForStatus, status);
379                    }
380                    _executeSocket.execute();
381                    conditionalNG.setSymbolTable(oldSymbolTable);
382                } catch (JmriException e) {
383                    if (e.getErrors() != null) {
384                        maleSocket.handleError(ProgramOnMain.this, rbx.getString("ExceptionExecuteMulti"), e.getErrors(), e, log);
385                    } else {
386                        maleSocket.handleError(ProgramOnMain.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log);
387                    }
388                } catch (RuntimeException e) {
389                    maleSocket.handleError(ProgramOnMain.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log);
390                }
391            }
392        }
393
394    }
395
396
397    public enum LongOrShortAddress {
398        Short(Bundle.getMessage("ProgramOnMain_LongOrShortAddress_Short")),
399        Long(Bundle.getMessage("ProgramOnMain_LongOrShortAddress_Long")),
400        Auto(Bundle.getMessage("ProgramOnMain_LongOrShortAddress_Auto"));
401
402        private final String _text;
403
404        private LongOrShortAddress(String text) {
405            this._text = text;
406        }
407
408        @Override
409        public String toString() {
410            return _text;
411        }
412
413    }
414
415
416    private static class POMItem implements LogixNG_SelectComboBox.Item {
417
418        private final String _value;
419
420        public POMItem(String value) {
421            this._value = value;
422        }
423
424        @Override
425        public String getKey() {
426            return _value;
427        }
428
429        @Override
430        public String toString() {
431            return _value;
432        }
433
434    }
435
436
437    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ProgramOnMain.class);
438
439}