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