001package jmri.jmrix.bidib;
002
003import java.util.HashMap;
004import java.util.Map;
005import javax.annotation.*;
006
007import jmri.implementation.AbstractSignalMast;
008import jmri.SystemConnectionMemo;
009import jmri.InstanceManager;
010import jmri.SignalMast;
011import org.bidib.jbidibc.messages.enums.LcOutputType;
012
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016/**
017 * This class implements a SignalMast that use <B>BiDiB Accessories</B>
018 * to set aspects.
019 * <p>
020 * This implementation writes out to BiDiB when it's commanded to
021 * change appearance, and updates its internal state when it receives a status feedback from BiDiB.
022 * <p>
023 * System name specifies the creation information:
024 * <pre>
025 * BF$bsm:basic:one-searchlight(123)
026 * </pre> The name is a colon-separated series of terms:
027 * <ul>
028 * <li>B - system prefix
029 * <li>F$bsm - defines signal masts of this type
030 * <li>basic - name of the signaling system
031 * <li>one-searchlight - name of the particular aspect map
032 * <li>(node:123) - BiDiB Accessory address
033 * </ul>
034 * <p>
035 * To keep the state consistent, {@link #setAspect} does not immediately
036 * change the local aspect.  Instead, it produces the relevant BiDiB message on the
037 * network, waiting for that to return and do the local state change, notification, etc.
038 * <p>
039 * Based upon {@link jmri.implementation.DccSignalMast} by Kevin Dickerson
040 *
041 * @author Bob Jacobsen    Copyright (c) 2017, 2018
042 * @author Eckart Meyer    Copyright (c) 2020-2023
043 */
044public class BiDiBSignalMast extends AbstractSignalMast implements BiDiBNamedBeanInterface {
045    
046    BiDiBTrafficController tc;
047    //MessageListener messageListener = null;
048    BiDiBOutputMessageHandler messageHandler = null;
049    BiDiBAddress addr;
050    private char typeLetter;
051    int commandedAspect = -1;
052
053    public BiDiBSignalMast(String sys, String user) {
054        super(sys, user);
055        configureFromName(sys);
056    }
057
058    public BiDiBSignalMast(String sys) {
059        super(sys);
060        configureFromName(sys);
061    }
062
063    public BiDiBSignalMast(String sys, String user, String mastSubType) {
064        super(sys, user);
065        mastType = mastSubType;
066        configureFromName(sys);
067    }
068
069    //protected String mastType = "F$bsm";
070    protected String mastType = getNamePrefix();
071    
072    static public String getNamePrefix() {
073        return "F$bsm";
074    }
075
076
077    private void configureFromName(String systemName) {
078        // split out the basic information
079        BiDiBSystemConnectionMemo memo = null;
080        String[] parts = systemName.split(":", 3); //the last part contains a BiDiB address and therfor may contain a colon itself
081        if (parts.length < 3) {
082            log.error("SignalMast system name needs at least three parts: {}", systemName);
083            throw new IllegalArgumentException("System name needs at least three parts: " + systemName);
084        }
085        if (!parts[0].endsWith(mastType)) {
086            log.warn("First part of SignalMast system name is incorrect {} : {}", systemName, mastType);
087        } else {
088            String systemPrefix = parts[0].substring(0, parts[0].indexOf("$") - 1);
089            java.util.List<SystemConnectionMemo> memoList = jmri.InstanceManager.getList(SystemConnectionMemo.class);
090
091            for (SystemConnectionMemo m : memoList) {
092                if (m.getSystemPrefix().equals(systemPrefix)) {
093                    if (m instanceof jmri.jmrix.bidib.BiDiBSystemConnectionMemo) {
094                        tc = ((BiDiBSystemConnectionMemo) m).getBiDiBTrafficController();
095                        memo = (BiDiBSystemConnectionMemo)m;
096                    } else {
097                        log.error("Can't create mast \"{}\" because system \"{}}\" is not BiDiBSystemConnectionMemo but rather {}",
098                                systemName, systemPrefix, m.getClass());
099                    }
100                    break;
101                }
102            }
103
104            if (tc == null) {
105                log.error("No BiDiB connection found for system prefix \"{}\", so mast \"{}\" will not function",
106                            systemPrefix, systemName);
107            }
108        }
109        String system = parts[1];
110        String mast = parts[2];
111
112        mast = mast.substring(0, mast.indexOf("("));
113        log.trace("In configureFromName setMastType to {}", mast);
114        setMastType(mast);
115        
116        if (memo != null) {
117            char accessoryTypeLetter = 'T';
118            String accessorySystemName = memo.getSystemPrefix() + accessoryTypeLetter + parts[2].substring(parts[2].indexOf("(") + 1, parts[2].indexOf(")"));
119            addr = new BiDiBAddress(accessorySystemName, accessoryTypeLetter, memo);
120            log.info("New SIGNALMAST created: {} {} -> {}", systemName, accessorySystemName, addr);
121            typeLetter = accessoryTypeLetter;
122//            if (!addr.isValid()) {
123//                log.warn("BiDiB signal mast accessory address SystemName {} is not in the correct format", systemName);
124//            }
125        }
126        configureSignalSystemDefinition(system);
127        configureAspectTable(system, mast);
128        
129        createSignalMastListener();
130        
131        messageHandler.sendQueryConfig();
132    }
133
134
135    protected HashMap<String, Integer> appearanceToOutput = new HashMap<>();
136
137    public void setOutputForAppearance(String appearance, int number) {
138        log.debug("setOutputForAppearance: {} -> {}", appearance, number);
139        if (appearanceToOutput.containsKey(appearance)) {
140            log.debug("Appearance {} is already defined as {}", appearance, appearanceToOutput.get(appearance));
141            appearanceToOutput.remove(appearance);
142        }
143        appearanceToOutput.put(appearance, number);
144    }
145
146    public int getOutputForAppearance(String appearance) {
147        if (!appearanceToOutput.containsKey(appearance)) {
148            log.error("Trying to get appearance {} but it has not been configured", appearance);
149            return -1;
150        }
151        return appearanceToOutput.get(appearance);
152    }
153
154    /*
155     0.  "Stop"
156     1.  "Take Siding"
157     2.  "Stop-Orders"
158     3.  "Stop-Proceed"
159     4.  "Restricting"
160     5.  "Permissive"
161     6.  "Slow-Approach"
162     7.  "Slow"
163     8.  "Slow-Medium"
164     9.  "Slow-Limited"
165     10. "Slow-Clear"
166     11. "Medium-Approach"
167     12. "Medium-Slow"
168     13. "Medium"
169     14. "Medium-Ltd"
170     15. "Medium-Clr"
171     16. "Limited-Approach"
172     17. "Limited-Slow"
173     18. "Limited-Med"
174     19. "Limited"
175     20. "Limited-Clear"
176     21. "Approach"
177     22. "Advance-Appr"
178     23. "Appr-Slow"
179     24. "Adv-Appr-Slow"
180     25. "Appr-Medium"
181     26. "Adv-Appr-Med"
182     27. "Appr-Limited"
183     28. "Adv-Appr-Ltd"
184     29. "Clear"
185     30. "Cab-Speed"
186     31. "Dark" */
187
188    @Override
189    public void setAspect(@Nonnull String aspect) {
190        if (appearanceToOutput.containsKey(aspect) && appearanceToOutput.get(aspect) != -1) {
191            sendMessage(appearanceToOutput.get(aspect));
192        } else {
193            log.warn("Trying to set aspect ({}) that has not been configured on mast {}", aspect, getDisplayName());
194        }
195        //super.setAspect(aspect);
196        String oldAspect = this.aspect;
197        this.aspect = null; //means UNKNOWN - there is no INCONSISTENT
198        //this.speed = (String) getSignalSystem().getProperty(aspect, "speed"); // NOI18N
199        firePropertyChange("Aspect", oldAspect, aspect); // NOI18N
200    }
201
202    @Override
203    public void setLit(boolean newLit) {
204        if (!allowUnLit() || newLit == getLit()) {
205            return;
206        }
207        if (newLit) {
208            String a = getAspect();
209            if (a != null) {
210                setAspect(a);
211            }
212            super.setLit(newLit);
213        } else {
214            sendMessage(unLitId);
215        }
216        //super.setLit(newLit);
217    }
218
219    /**
220     * Request the state of the accessory from the layout.
221     * The listener gets the answer.
222     */
223    public void queryAccessory() {
224        messageHandler.sendQuery();
225    }
226
227    /**
228     * Send a accessory message to BiDiB
229     * 
230     * @param aspect to send
231     */
232    protected void sendMessage(int aspect) {
233        // TODO: check FEATURE_GEN_SWITCH_ACK
234        log.debug("Signal Mast set aspect: {}, addr: {}", aspect, addr);
235        //newKnownAspect(INCONSISTENT);
236        if (addr.isValid()) {
237            int state;
238            if (addr.isPortAddr()) {
239                state = (aspect == 0) ? 0 : 1;
240                switch (messageHandler.getLcType()) {
241                    case LIGHTPORT:
242                        state = (aspect == 0) ? 2 : 3; //use Dim function - we can't configure this so far...
243                        break;
244                    case SERVOPORT:
245                    case ANALOGPORT:
246                    case BACKLIGHTPORT:
247                        state = (aspect == 0) ? 0 : 255;
248                        break;
249                    case MOTORPORT:
250                        state = (aspect == 0) ? 0 : 126;
251                        break;
252                    case INPUTPORT:
253                        log.warn("output to INPUT port is not possible, addr: {}", addr);
254                        return;
255                    default:
256                        break;
257                }
258            }
259            else {
260                state = aspect;
261            }
262            messageHandler.sendOutput(state);
263            commandedAspect = aspect;
264        }
265    }
266        
267    int unLitId = 31;
268
269    public void setUnlitId(int i) {
270        unLitId = i;
271    }
272
273    public int getUnlitId() {
274        return unLitId;
275    }
276
277    public String getAccessoryAddress() {
278        if (addr.isValid()) {
279            return addr.getAddrString();
280        }
281        return "";
282    }
283    
284    public BiDiBTrafficController getTrafficController() {
285        return tc;
286    }
287    
288    /**
289     * {@inheritDoc}
290     */
291    @Override
292    public BiDiBAddress getAddr() {
293        return addr;
294    }
295    
296    /**
297     * {@inheritDoc}
298     */
299    @Override
300    public void nodeNew() {
301        //create a new BiDiBAddress
302        addr = new BiDiBAddress(getSystemName(), typeLetter, tc.getSystemConnectionMemo());
303        if (addr.isValid()) {
304            log.info("new signal mast address created: {} -> {}", getSystemName(), addr);
305            log.debug("current aspect is {}, commanded aspect {}", getAspect(), commandedAspect);
306            if (addr.isPortAddr()) {
307                messageHandler.sendQueryConfig();
308                messageHandler.waitQueryConfig();
309            }
310            sendMessage(commandedAspect);
311        }
312    }
313
314    /**
315     * {@inheritDoc}
316     */
317    @Override
318    public void nodeLost() {
319        super.setLit(false);
320    }
321    
322    /**
323     * {@inheritDoc}
324     * 
325     * Remove the Message Listener for this signal mast
326     */
327    @Override
328    public void dispose() {
329        if (messageHandler != null) {
330            tc.removeMessageListener(messageHandler);        
331            messageHandler = null;
332        }
333        super.dispose();
334    }
335
336    public static String isAccessoryAddressUsed(BiDiBAddress address) {
337        for (SignalMast mast : InstanceManager.getDefault(jmri.SignalMastManager.class).getNamedBeanSet()) {
338            if (mast instanceof BiDiBSignalMast) {
339                if (((BiDiBSignalMast) mast).getAddr().isAddressEqual(address)) {
340                    return ((BiDiBSignalMast) mast).getDisplayName();
341                }
342            }
343        }
344        return null;
345    }
346    
347    private void newKnownAspect(int aspectNum) {
348        //log.debug("Available aspects: {}, addr: {}", appearanceToOutput.size(), addr);
349        for(Map.Entry<String, Integer> entry : appearanceToOutput.entrySet()) {
350            //log.debug("  check for aspect {}", entry.getValue());
351            if (entry.getValue() == aspectNum) {
352                //log.debug("set aspect {} ({})", entry.getKey(), aspectNum);
353                if (aspectNum == unLitId) {
354                    //log.debug(" setLit");
355                    super.setLit(false);
356                }
357                else {
358                    log.debug(" setAspect to {}", entry.getKey());
359                    super.setAspect(entry.getKey());
360                    super.setLit(true);
361                }
362            }
363        }
364    }
365    
366    private void createSignalMastListener() {
367        //messageHandler = new BiDiBOutputMessageHandler("SIGNALMAST", addr, tc){
368        messageHandler = new BiDiBOutputMessageHandler(this, "SIGNALMAST", tc){
369            @Override
370            public void newOutputState(int state) {
371                int newAspect;
372                if (addr.isPortAddr()) {
373                    // since we do not know what other states than 0 (or 2 for LIGHTPORTS) mean (which aspect),
374                    // we simply use what was commanded. Does not really work for spontaneous messages.
375                    // So, preferrably use accessory addresses which supports the various aspects.
376                    if (messageHandler.getLcType() == LcOutputType.LIGHTPORT) {
377                        newAspect = (state == 2 || state == 0) ? 0 : commandedAspect;
378                    }
379                    else {
380                        newAspect = (state == 0) ? 0 : commandedAspect;
381                    }
382                }
383                else {
384                    // for others (accessories), the state is the new aspect number
385                    newAspect = state;
386                }
387                log.debug("SIGNALMAST new aspect: {}", newAspect);
388                newKnownAspect(newAspect);
389            }
390            @Override
391            public void outputWait(int time) {
392                log.debug("SIGNALMAST wait: {}", time);
393                //newKnownAspect(commandedAspect);
394            }
395            @Override
396            public void errorState(int err) {
397                log.warn("SIGNALMAST error: {} addr: {}", err, addr);
398                //newKnownAspect(getUnlitId());
399                //super.setAspect(aspect);
400                String oldAspect = aspect;
401                aspect = null; //means UNKNOWN - there is no INCONSISTENT
402                //this.speed = (String) getSignalSystem().getProperty(aspect, "speed"); // NOI18N
403                firePropertyChange("Aspect", oldAspect, aspect); // NOI18N
404            }
405        };
406        tc.addMessageListener(messageHandler);
407    }
408    
409    private final static Logger log = LoggerFactory.getLogger(BiDiBSignalMast.class);
410
411}
412
413