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        String[] modes = {""};
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<String> modeList = new ArrayList<>();
131        for (ProgrammingMode mode : _programmerManager.getDefaultModes()) {
132            log.debug("Available programming mode: {}", mode);
133            modeList.add(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(ProgrammingMode.OPSBYTEMODE.getStandardName());
140        }
141
142        String[] modes = modeList.toArray(String[]::new);
143        _selectProgrammingMode.setValues(modes);
144    }
145
146    public final SystemConnectionMemo getMemo() {
147        return _memo;
148    }
149
150    /** {@inheritDoc} */
151    @Override
152    public Category getCategory() {
153        return 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);
227        ProgrammingMode progMode = new ProgrammingMode(progModeStr);
228
229        int address = _selectAddress.evaluateValue(conditionalNG);
230        LongOrShortAddress longOrShort = _selectLongOrShortAddress.evaluateEnum(conditionalNG);
231        int cv = _selectCV.evaluateValue(conditionalNG);
232        int value = _selectValue.evaluateValue(conditionalNG);
233
234        doProgrammingOnMain(conditionalNG, newSymbolTable, progMode, address, longOrShort, cv, value, _wait);
235    }
236
237    @Override
238    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
239        switch (index) {
240            case 0:
241                return _executeSocket;
242
243            default:
244                throw new IllegalArgumentException(
245                        String.format("index has invalid value: %d", index));
246        }
247    }
248
249    @Override
250    public int getChildCount() {
251        return 1;
252    }
253
254    @Override
255    public void connected(FemaleSocket socket) {
256        if (socket == _executeSocket) {
257            _executeSocketSystemName = socket.getConnectedSocket().getSystemName();
258        } else {
259            throw new IllegalArgumentException("unkown socket");
260        }
261    }
262
263    @Override
264    public void disconnected(FemaleSocket socket) {
265        if (socket == _executeSocket) {
266            _executeSocketSystemName = null;
267        } else {
268            throw new IllegalArgumentException("unkown socket");
269        }
270    }
271
272    @Override
273    public String getShortDescription(Locale locale) {
274        return Bundle.getMessage(locale, "ProgramOnMain_Short");
275    }
276
277    @Override
278    public String getLongDescription(Locale locale) {
279        if (_memo != null) {
280            return Bundle.getMessage(locale, "ProgramOnMain_LongConnection",
281                    _selectLongOrShortAddress.getDescription(locale),
282                    _selectAddress.getDescription(locale, false),
283                    _selectCV.getDescription(locale, false),
284                    _selectValue.getDescription(locale, false),
285                    _selectProgrammingMode.getDescription(locale),
286                    _memo.getUserName());
287        } else {
288            return Bundle.getMessage(locale, "ProgramOnMain_Long",
289                    _selectLongOrShortAddress.getDescription(locale),
290                    _selectAddress.getDescription(locale, false),
291                    _selectCV.getDescription(locale, false),
292                    _selectValue.getDescription(locale, false),
293                    _selectProgrammingMode.getDescription(locale));
294        }
295    }
296
297    public FemaleDigitalActionSocket getExecuteSocket() {
298        return _executeSocket;
299    }
300
301    public String getExecuteSocketSystemName() {
302        return _executeSocketSystemName;
303    }
304
305    public void setExecuteSocketSystemName(String systemName) {
306        _executeSocketSystemName = systemName;
307    }
308
309    /** {@inheritDoc} */
310    @Override
311    public void setup() {
312        try {
313            if (!_executeSocket.isConnected()
314                    || !_executeSocket.getConnectedSocket().getSystemName()
315                            .equals(_executeSocketSystemName)) {
316
317                String socketSystemName = _executeSocketSystemName;
318
319                _executeSocket.disconnect();
320
321                if (socketSystemName != null) {
322                    MaleSocket maleSocket =
323                            InstanceManager.getDefault(DigitalActionManager.class)
324                                    .getBySystemName(socketSystemName);
325                    if (maleSocket != null) {
326                        _executeSocket.connect(maleSocket);
327                        maleSocket.setup();
328                    } else {
329                        log.error("cannot load digital action {}", socketSystemName);
330                    }
331                }
332            } else {
333                _executeSocket.getConnectedSocket().setup();
334            }
335        } catch (SocketAlreadyConnectedException ex) {
336            // This shouldn't happen and is a runtime error if it does.
337            throw new RuntimeException("socket is already connected");
338        }
339    }
340
341    /** {@inheritDoc} */
342    @Override
343    public void propertyChange(PropertyChangeEvent evt) {
344        getConditionalNG().execute();
345    }
346
347
348    private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket {
349
350        private ConditionalNG conditionalNG;
351        private SymbolTable newSymbolTable;
352        private int status;
353
354        public InternalFemaleSocket() {
355            super(null, new FemaleSocketListener(){
356                @Override
357                public void connected(FemaleSocket socket) {
358                    // Do nothing
359                }
360
361                @Override
362                public void disconnected(FemaleSocket socket) {
363                    // Do nothing
364                }
365            }, "A");
366        }
367
368        @Override
369        public void execute() throws JmriException {
370            if (_executeSocket != null) {
371                MaleSocket maleSocket = (MaleSocket)ProgramOnMain.this.getParent();
372                try {
373                    SymbolTable oldSymbolTable = conditionalNG.getSymbolTable();
374                    conditionalNG.setSymbolTable(newSymbolTable);
375                    if (!_localVariableForStatus.isEmpty()) {
376                        newSymbolTable.setValue(_localVariableForStatus, status);
377                    }
378                    _executeSocket.execute();
379                    conditionalNG.setSymbolTable(oldSymbolTable);
380                } catch (JmriException e) {
381                    if (e.getErrors() != null) {
382                        maleSocket.handleError(ProgramOnMain.this, rbx.getString("ExceptionExecuteMulti"), e.getErrors(), e, log);
383                    } else {
384                        maleSocket.handleError(ProgramOnMain.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log);
385                    }
386                } catch (RuntimeException e) {
387                    maleSocket.handleError(ProgramOnMain.this, Bundle.formatMessage(rbx.getString("ExceptionExecuteAction"), e.getLocalizedMessage()), e, log);
388                }
389            }
390        }
391
392    }
393
394
395    public enum LongOrShortAddress {
396        Short(Bundle.getMessage("ProgramOnMain_LongOrShortAddress_Short")),
397        Long(Bundle.getMessage("ProgramOnMain_LongOrShortAddress_Long")),
398        Auto(Bundle.getMessage("ProgramOnMain_LongOrShortAddress_Auto"));
399
400        private final String _text;
401
402        private LongOrShortAddress(String text) {
403            this._text = text;
404        }
405
406        @Override
407        public String toString() {
408            return _text;
409        }
410
411    }
412
413
414    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ProgramOnMain.class);
415
416}