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