001package jmri.jmrix.bidib;
002
003import java.util.Map;
004import java.util.HashMap;
005import java.util.Collections;
006import java.util.Locale;
007import java.util.regex.Matcher;
008import java.util.regex.Pattern;
009
010import org.bidib.jbidibc.messages.Node;
011import org.bidib.jbidibc.messages.utils.NodeUtils;
012import org.bidib.jbidibc.messages.utils.ByteUtils;
013import org.bidib.jbidibc.messages.LcConfig;
014import org.bidib.jbidibc.messages.LcConfigX;
015import org.bidib.jbidibc.messages.BidibPort;
016import org.bidib.jbidibc.messages.enums.LcOutputType;
017import org.bidib.jbidibc.messages.enums.PortModelEnum;
018
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021
022/**
023 * Utilities for handling BiDiB adresses
024 * <p>
025 * @author Eckart Meyer Copyright (C) 2019-2023
026 * 
027 */
028public class BiDiBAddress {
029    
030    private String aString = null;
031    private long nodeuid = 0;
032    private int addr = -1; //port address or DCC address
033    private String addrType = ""; //t: DCC address ("on the track"), p: local port, default is DCC address if a command station node is present
034    private LcOutputType portType; //used in type address mode only, not in flat address mode
035    private Node node = null;
036
037    static final String addrRegex = "^(?:[xX]([0-9a-fA-F]+):|([a-zA-Z0-9_\\-\\.]+):|)([afptAFPT]{0,1})(\\d+)([SLVUMABPI]{0,1})$";
038    
039    // Groups:
040    // 0 - all
041    // 1 - node (hex with X prefix) - null of not present
042    // 2 - node (name starting with a letter, but not X) - null if not present
043    // 3 - address type letter (a, f, p or t), empty string if not present
044    // 4 - address (decimal), required
045    // 5 - port type letter (type address model only), empty string of not present
046    
047    private static volatile Pattern addrPattern = Pattern.compile(addrRegex);
048    
049    private static final Map<Character, LcOutputType> portTypeList = createPortTypeList(); //port type map
050    
051    private static Map<Character, LcOutputType> createPortTypeList() {
052        Map<Character, LcOutputType> l = new HashMap<>();
053        l.put('S', LcOutputType.SWITCHPORT);
054        l.put('L', LcOutputType.LIGHTPORT);
055        l.put('V', LcOutputType.SERVOPORT);
056        l.put('U', LcOutputType.SOUNDPORT);
057        l.put('M', LcOutputType.MOTORPORT);
058        l.put('A', LcOutputType.ANALOGPORT);
059        l.put('B', LcOutputType.BACKLIGHTPORT);
060        l.put('P', LcOutputType.SWITCHPAIRPORT);
061        l.put('I', LcOutputType.INPUTPORT);
062        return Collections.unmodifiableMap(l);
063    }
064    
065    
066    /**
067     * Construct from system name - needs prefix and type letter
068     * 
069     * @param systemName the JMRI system name for which the adress object is to be created
070     * @param typeLetter the type letter from the calling manager (T, L, S, R)
071     * @param memo connection memo object
072     */
073    public BiDiBAddress(String systemName, char typeLetter, BiDiBSystemConnectionMemo memo) {
074        aString = systemName.substring(memo.getSystemPrefix().length() + 1);
075        log.debug("ctor: systemName: {}, typeLetter: {}, systemPrefix: {}", systemName, typeLetter, memo.getSystemPrefix());
076        
077        parse(systemName, typeLetter, memo);
078    }
079    
080    // now parse
081    // supported formats are 
082    //      <nodeuid>:<addr>
083    //      <addr>            use root node
084    // For outputs (Turnouts and signals, type "T"), addr may start with "t" (DCC address), "p" (local port) or "a" (local assessory).
085    // If no address prefix is given, it defaults to DCC address ("t") as long as the node is a command station.
086    // If the node is not command station, it defaults to BiDiB accessory number ("a") for Turnouts and Signals (type letter T)
087    // otherwise to a local port number ("p").
088    // For inputs (Sensors), addr may start with "f" (Bidib feedback) or "p" (just an input port). Default is "f".
089
090    // type addressing for ports: S=Switch, L=Light, V=Servo, U=Sound, M=Motor, A=Analogout, B=Backlight, P=Switchpair, I=Input
091    // addr: p123S
092
093    private void parse(String systemName, char typeLetter, BiDiBSystemConnectionMemo memo) {
094        BiDiBTrafficController tc = memo.getBiDiBTrafficController();
095        if (!aString.isEmpty()  &&  systemName.charAt(memo.getSystemPrefix().length()) == typeLetter) {
096            Node foundNode;
097            try {
098                Matcher matcher = addrPattern.matcher(aString);
099                if (!matcher.matches()) {
100                    log.trace("systemName {} does not match regular expression", systemName);
101                    //throw new Exception("Illegal address: " + aString);
102                    throw new jmri.NamedBean.BadSystemNameException(Locale.getDefault(), "InvalidSystemName",systemName,"");
103                }
104                  // DEBUG
105//                for (int i = 0; i <= matcher.groupCount(); i++) {
106//                    log.trace("  {}: {}", i, matcher.group(i));
107//                }
108                if (matcher.group(1) != null) {
109                    nodeuid = Long.parseLong(matcher.group(1), 16); //nodeuid in hex
110                }
111                else if (matcher.group(2) != null) {
112                    Node n = tc.getNodeByUserName(matcher.group(2));
113                    if (n != null) {
114                        nodeuid = n.getUniqueId() & 0xFFFFFFFFFFL;
115                    }
116                    else {
117                        throw new Exception("No such node: " + matcher.group(2));
118                    }
119                }
120                addrType = matcher.group(3).toLowerCase();
121                addr = Integer.parseInt(matcher.group(4));
122                String t = matcher.group(5).toUpperCase();
123                if (!t.isEmpty()) {
124                    portType = portTypeList.get(t.charAt(0));
125                }
126                
127                if (nodeuid == 0) {
128                    // no unique id given - use root node which always has node address 0
129                    foundNode = tc.getRootNode();
130                    if (foundNode != null) {
131                        nodeuid = foundNode.getUniqueId() & 0xFFFFFFFFFFL;
132                    }
133                }
134                else {
135                    log.trace("trying UID {}", ByteUtils.formatHexUniqueId(nodeuid));
136                    foundNode = tc.getNodeByUniqueID(nodeuid);
137                }
138                log.trace("found node: {}", foundNode); 
139                if (foundNode != null) {
140                    long uid = foundNode.getUniqueId();
141                    if (typeLetter == 'S') {
142                        switch(addrType) {
143                            case "t":
144                                addrType = "f"; //what does "t" mean here? Silently convert to "f"
145                                if (!NodeUtils.hasFeedbackFunctions(uid)) addrType = "";
146                                break; //don't use "fall through" as some code checkers does not like it...
147                            case "f":
148                                if (!NodeUtils.hasFeedbackFunctions(uid)) addrType = "";
149                                break;
150                            case "p":
151                                if (!NodeUtils.hasSwitchFunctions(uid)) addrType = "";
152                                break;
153                            case "":
154                                if (NodeUtils.hasFeedbackFunctions(uid)) addrType = "f";
155                                else if (NodeUtils.hasSwitchFunctions(uid)) addrType = "p";
156                                break;
157                            default:
158                                addrType = "";
159                                break;
160                        }
161                        if (addrType.equals("p")) {
162                            if (portType == null) {
163                                portType = LcOutputType.INPUTPORT; //types other than Input do not make sense...
164                            }
165                            if (!portType.equals(LcOutputType.INPUTPORT)) {
166                                addrType = "";
167                            }
168                        }
169                    }
170                    else if (typeLetter == 'R') {
171                        if (addrType.isEmpty()) {
172                            addrType = "f";
173                        }
174                        if (!addrType.equals("f")) {
175                            addrType = "";
176                        }
177                    }
178                    else if (typeLetter == 'T') {
179                        switch(addrType) {
180                            case "a":
181                                if (!NodeUtils.hasAccessoryFunctions(uid)) addrType = "";
182                                break;
183                            case "p":
184                                if (!NodeUtils.hasSwitchFunctions(uid)) addrType = "";
185                                break;
186                            case "t":
187                                if (!NodeUtils.hasCommandStationFunctions(uid)) addrType = "";
188                                break;
189                            case "":
190                                if (NodeUtils.hasCommandStationFunctions(uid)) addrType = "t";
191                                else if (NodeUtils.hasAccessoryFunctions(uid)) addrType = "a";
192                                else if (NodeUtils.hasSwitchFunctions(uid)) addrType = "p";
193                                break;
194                            default:
195                                addrType = "";
196                                break;
197                        }
198                        if (addrType.equals("p")  &&  portType != null  &&  portType.equals(LcOutputType.INPUTPORT)) {
199                            addrType = "";
200                        }
201                    }
202                    else if (typeLetter == 'L') {
203                        switch(addrType) {
204                            case "p":
205                                if (!NodeUtils.hasSwitchFunctions(uid)) addrType = "";
206                                break;
207                            case "t":
208                                if (!NodeUtils.hasCommandStationFunctions(uid)) addrType = "";
209                                break;
210                            case "":
211                                if (NodeUtils.hasSwitchFunctions(uid)) addrType = "p";
212                                else if (NodeUtils.hasCommandStationFunctions(uid)) addrType = "t";
213                                break;
214                            default:
215                                addrType = "";
216                                break;
217                        }
218                        if (addrType.equals("p")  &&  portType != null  &&  portType.equals(LcOutputType.INPUTPORT)) {
219                            addrType = "";
220                        }
221                    }
222                    if (addrType.equals("p")) {
223                        if (!foundNode.isPortFlatModelAvailable()  &&  portType == null) {
224//                            addrType = ""; //type addr model must have a port type
225                            portType = LcOutputType.SWITCHPORT;
226                        }
227                    }
228                    else {
229                        if (portType != null) {
230                            addrType = ""; //port type not allowed on other address types than 'p'
231                        }
232                    }
233                    if (addr >= 0  &&  !addrType.isEmpty()) {
234                        node = foundNode;
235                    }
236                }
237                if (!isValid()) {
238                    throw new Exception("Invalid BiDiB address: " + systemName);
239                }
240            }
241            catch (Exception e) {
242                //log.trace("parse of BiDiBAddress throws {}", e);
243                node = null;
244            }
245        }
246        
247        if (isValid()) {
248            log.debug("BiDiB \"{}\" -> {}", systemName, toString());
249        }
250        else {
251            log.warn("*** BiDiB system name \"{}\" is invalid", systemName);
252        }
253    }
254
255    /**
256     * Static method to check system name syntax. Does not check if the node is available
257     * 
258     * @param systemName the JMRI system name for which the adress object is to be created
259     * @param typeLetter the type letter from the calling manager (T, L, S, R)
260     * @param memo connection memo object
261     * @return true if the system name is syntactically valid.
262     */
263    static public boolean isValidSystemNameFormat(String systemName, char typeLetter, BiDiBSystemConnectionMemo memo) {
264        String aString = systemName.substring(memo.getSystemPrefix().length() + 1);
265        if (addrPattern == null) {
266            addrPattern = Pattern.compile(addrRegex);
267            log.trace("regexp: {}", addrRegex);
268        }
269        if (!aString.isEmpty()  &&  systemName.charAt(memo.getSystemPrefix().length()) == typeLetter) {
270            Matcher matcher = addrPattern.matcher(aString);
271            if (matcher.matches()) {
272                return true;
273            }
274            else {
275                log.trace("systemName {} does not match regular expression", systemName);
276                //throw new Exception("Illegal address: " + aString);
277                //throw new jmri.NamedBean.BadSystemNameException(Locale.getDefault(), "InvalidSystemName",systemName);
278                return false;
279            }
280        }
281        return false;
282    }
283    
284    /**
285     * Invalidate this BiDiBAddress by removing the node.
286     * Used when the node gets lost.
287     */
288    public void invalidate() {
289        log.warn("BiDiB address invalidated: {}", this);
290        node = null;
291        nodeuid = 0;
292    }
293    
294    /**
295     * Check if the object contains a valid BiDiB address
296     * The object is invalied the the system is syntactically wrong or if the requested node is not available
297     * 
298     * @return true if valid
299     */
300    public boolean isValid() {
301        return (node != null);
302    }
303    
304    /**
305     * Check if the address is a BiDiB Port address (LC)
306     * 
307     * @return true if the object represents a BiDiB Port address 
308     */
309    public boolean isPortAddr() {
310        return (addrType.equals("p"));
311    }
312    
313    /**
314     * Check if the address is a BiDiB Accessory address.
315     * 
316     * @return true if the object represents a BiDiB Accessory address 
317     */
318    public boolean isAccessoryAddr() {
319        return (addrType.equals("a"));
320    }
321    
322    /**
323     * Check if the address is a BiDiB feedback Number (BM).
324     * 
325     * @return true if the object represents a BiDiB feedback Number
326     */
327    public boolean isFeedbackAddr() {
328        return (addrType.equals("f"));
329    }
330    
331    /**
332     * Check if the address is a BiDiB track address (i.e. a DCC accessory address).
333     * 
334     * @return true if the object represents a BiDiB track address
335     */
336    public boolean isTrackAddr() {
337        return (addrType.equals("t"));
338    }
339    
340    /**
341     * Get the 40 bit unique ID of the found node
342     * 
343     * @return the 40 bit node unique ID
344     */
345    public long getNodeUID() {
346        return nodeuid;
347    }
348    
349    /**
350     * Get the address inside the node.
351     * This may be a DCC address (for DCC-Accessories), an accessory number (for BiDiB accessories)
352     * or a port number (for LC ports)
353     * 
354     * @return address inside node
355     */
356    public int getAddr() {
357        return addr;
358    }
359    
360    /**
361     * Get the address as string exactly as given when the instance has been created
362     * 
363     * @return address as string
364     */
365    public String getAddrString() {
366        return aString;
367    }
368    
369    /**
370     * Get the BiDiB Node object.
371     * If the node is not available, null is returned.
372     * 
373     * @return Node object or null, if node is not available
374     */
375    public Node getNode() {
376        return node;
377    }
378    
379    /**
380     * Get the BiDiB Node address.
381     * If the node is not available, an empty address array is returned.
382     * Note: The BiDiB node address is dynamically created address from the BiDiBbus
383     * and is not suitable as a node ID. Use the Unique ID for that purpose.
384     * 
385     * @return Node address (byte array) or empty address array, if node is not available
386     */
387    public byte[] getNodeAddr() {
388        byte[] ret = {};
389        if (node != null) {
390            ret = node.getAddr();
391        }
392        return ret;
393    }
394    
395    /**
396     * Get the port type as an LcOutputType object (SWITCHPORT, LIGHTPORT, ...)
397     * 
398     * @return LcOutputType object
399     */
400    public LcOutputType getPortType() {
401        return portType;
402    }
403    
404    /**
405     * Get the address type as a lowercase single letter:
406     * t    - DCC address of decoder (t stands for "Track")
407     * a    - BiDiB Accessory Number
408     * p    - BiDiB Port
409     * f    - BiDiB Feedback Number (BM)
410     * 
411     * Not a public method since we want to hide this letter,
412     * use isPortAddr() or isAccessoryAddr() instead.
413     * 
414     * @return single letter address type
415     */
416    protected String getAddrtype() {
417        return addrType;
418    }
419    
420    // some convenience methods
421    
422    /**
423     * Check if the object contains an address in the BiDiB type based address model.
424     * Returns false if the address is in the flat address model.
425     * 
426     * @return true if address is in the BiDiB type based address model.
427     */
428    public boolean isPortTypeBasedModel() {
429        if (node != null) {
430            if (isPortAddr()  &&  !node.isPortFlatModelAvailable()) {
431                return true;
432            }
433        }
434        return false;
435    }
436    
437    /**
438     * Check address against a BiDiBAddress object.
439     * 
440     * @param other as BiDiBAddress
441     * @return true if same
442     */
443    public boolean isAddressEqual(BiDiBAddress other) {
444        if (node == null  ||  other.getNodeUID() != getNodeUID()  ||  other.getAddr() != addr  ||  !other.getAddrtype().equals(addrType)) {
445            return false;
446        }
447        if (isPortAddr()  &&  isPortTypeBasedModel()) {
448            return other.getPortType() == portType;
449        }
450        return true;
451    }
452
453    /**
454     * Check address against a LcConfig object.
455     * 
456     * @param lcConfig as LcConfig
457     * @return true if the address contained in the LcConfig object is the same
458     */
459    public boolean isAddressEqual(LcConfig lcConfig) {
460        return isAddressEqual(lcConfig.getBidibPort());
461    }
462    
463    /**
464     * Check address against a LcConfigX object.
465     * 
466     * @param lcConfigX as LcConfig
467     * @return true if the address contained in the LcConfigX object is the same
468     */
469    public boolean isAddressEqual(LcConfigX lcConfigX) {
470        return isAddressEqual(lcConfigX.getBidibPort());
471    }
472    
473    /**
474     * Check address against a BiDiBAddress object.
475     * 
476     * @param bidibPort as BiDiBPort
477     * @return true if same
478     */
479    public boolean isAddressEqual(BidibPort bidibPort) {
480        if (node == null  ||  !isPortAddr()) {
481            return false;
482        }
483        if (node.isPortFlatModelAvailable()) {
484            return bidibPort.getPortNumber(PortModelEnum.flat_extended) == addr;
485        }
486        else {
487            return (bidibPort.getPortNumber(PortModelEnum.type) == addr  &&  bidibPort.getPortType(PortModelEnum.type) == portType);
488        }
489    }
490    
491    /**
492     * Create a BiDiBPort object from this object
493     * 
494     * @return new BiDiBPort object 
495     */
496    public BidibPort makeBidibPort() {
497        if (node == null  ||  !isPortAddr()) {
498            return null;
499        }
500        return BidibPort.prepareBidibPort(PortModelEnum.getPortModel(node), portType, addr);
501    }
502    
503    /**
504     * Static method to parse a system Name.
505     * A temporary BiDiDAdress object is created.
506     * 
507     * @param systemName the JMRI system name for which the adress object is to be created
508     * @param typeLetter the type letter from the calling manager (T, L, S, R)
509     * @param memo connection memo object
510     * @return true if the system name is valid and the BiDiB Node is available
511     */
512    static public boolean isValidAddress(String systemName, char typeLetter, BiDiBSystemConnectionMemo memo) throws IllegalArgumentException {
513        BiDiBAddress addr = new BiDiBAddress(systemName, typeLetter, memo);
514        return addr.isValid();
515    }
516    
517    /**
518     * {@inheritDoc}
519     */
520    @Override
521    public String toString() {
522        String s = "";
523        if (isPortAddr()) {
524            s = "(" + (isPortTypeBasedModel() ? "type-based" : "flat") + "),portType=" + portType;
525        }
526        return "BiDiBAdress[UID=" + ByteUtils.formatHexUniqueId(nodeuid) + ",addrType=" + addrType + ",addr=" + addr + s + "]";
527    }
528    
529    private final static Logger log = LoggerFactory.getLogger(BiDiBAddress.class);
530}