001package jmri.jmrix.openlcb;
002
003import java.util.regex.Matcher;
004import java.util.regex.Pattern;
005
006import javax.annotation.CheckReturnValue;
007
008import jmri.NamedBean.BadSystemNameException;
009
010import jmri.jmrix.can.CanMessage;
011import jmri.jmrix.can.CanReply;
012import jmri.jmrix.can.CanSystemConnectionMemo;
013
014import org.openlcb.EventID;
015
016import javax.annotation.Nonnull;
017
018/**
019 * Utilities for handling OpenLCB event messages as addresses.
020 * <p>
021 * OpenLCB event messages have header information, plus an EventID in the data
022 * part. JMRI maps these into address strings.
023 * <p>
024 * String forms:
025 * <dl>
026 * <dt>Special case for DCC Turnout addressing:  Tnnn where nnn is a decimal number
027 *
028 * <dt>Full hex string preceeded by "x"<dd>Needs to be pairs of digits: 0123,
029 * not 123
030  *
031 * <dt>Full 8 byte ID as pairs separated by "."
032 * </dl>
033 * <p>
034 * Note: the {@link #check()} routine does a full, expensive
035 * validity check of the name.  All other operations
036 * assume correctness, diagnose some invalid-format strings, but
037 * may appear to successfully handle other invalid forms.
038 *
039 * @author Bob Jacobsen Copyright (C) 2008, 2010, 2018, 2024
040 */
041public final class OlcbAddress {
042
043    static final String singleAddressPattern = "([xX](\\p{XDigit}\\p{XDigit}){1,8})|((\\p{XDigit}?\\p{XDigit}.){7}\\p{XDigit}?\\p{XDigit})";
044
045    private Matcher hCode = null;
046
047    private Matcher getMatcher() {
048        if (hCode == null)  hCode = Pattern.compile("^" + singleAddressPattern + "$").matcher("");
049        return hCode;
050    }
051
052    private String aString;         // String value of the address
053    private int[] aFrame = null;    // int[8] of event ID; if null, aString might be two addresses
054    private boolean match = false;  // true if address properly parsed; false (may) mean two-part address
055    private boolean fromName = false; // true if this originate as an event name
056    /**
057     * Construct from OlcbEvent.
058     *
059     * @param e the event ID.
060     */
061    public OlcbAddress(EventID e) {
062        byte[] contents = e.getContents();
063        aFrame = new int[contents.length];
064        int i = 0;
065        for (byte b : contents) {
066            aFrame[i++] = b;
067        }
068        aString = toCanonicalString();
069    }
070
071    /**
072     * Construct from string without leading system or type letters
073     * @param input hex coded string of address
074     */
075    public OlcbAddress(String input, final CanSystemConnectionMemo memo) {
076        // This is done manually, rather than via regular expressions, for performance reasons.
077
078        String s = input.strip();
079        
080        OlcbEventNameStore nameStore = null;
081        if (memo != null) { 
082            nameStore = memo.get(OlcbEventNameStore.class);
083        }
084        EventID eid;
085        if (nameStore != null && (eid = nameStore.getEventID(s)) != null) {
086            // name form
087            // load the event ID into the aFrame c.f. OlcbAddress(EventID) ctor
088            byte[] contents = eid.getContents();
089            aFrame = new int[contents.length];
090            int i = 0;
091            for (byte b : contents) {
092                aFrame[i++] = b;
093            }
094            match = true;
095            fromName = true;
096            // leave aString as original argument
097            return;
098        }
099        
100        // check for special addressing forms
101        if (s.startsWith("T")) {
102            // leading T, so convert to numeric form from turnout number
103            int from;
104            try {
105                from = Integer.parseInt(s.substring(1));
106            } catch (NumberFormatException e) {
107                from = 0;
108            }
109
110            int DD = (from-1) & 0x3;
111            int aaaaaa = (( (from-1) >> 2)+1 ) & 0x3F;
112            int AAA = ( (from) >> 8) & 0x7;
113            long event = 0x0101020000FF0000L | (AAA << 9) | (aaaaaa << 3) | (DD << 1);
114
115            s = String.format("%016X;%016X", event, event+1);
116            log.trace(" Turnout form converted to {}", s);
117        } else if (s.startsWith("S")) {
118            // leading S, so convert to numeric form from sensor number
119            int from;
120            try {
121                from = Integer.parseInt(s.substring(1));
122            } catch (NumberFormatException e) {
123                from = 0;
124            }
125
126            from = 0xFFF & (from - 1); // 1 based name to 0 based network, 12 bit value
127            
128            long event1 = 0x0101020000FB0000L | from; // active/on
129            long event2 = 0x0101020000FA0000L | from; // inactive/off
130 
131            s = String.format("%016X;%016X", event1, event2);
132            log.trace(" Sensor form converted to {}", s);
133        }
134
135        aString = s;
136
137        // numeric address string format
138        if (aString.contains(";")) {
139            // multi-part address; leave match false and aFrame null; only aString has content
140            // will later be split up and parsed with #split() call
141            return;
142        }
143        
144        // check for name vs numeric address formats
145        
146        if (aString.contains(".")) {
147            // dotted form, 7 dots
148            String[] terms = s.split("\\.");
149            if (terms.length != 8) {
150                log.debug("unexpected number of terms: {}, address is {}", terms.length, s);
151            }
152            int[] tFrame = new int[terms.length];
153            int i = -1;
154            try {
155                for (i = 0; i < terms.length; i++) {
156                    tFrame[i] = Integer.parseInt(terms[i].strip(), 16);
157                }
158            } catch (NumberFormatException ex) {
159                // leaving the string unparsed
160                log.debug("failed to parse EventID \"{}\" at {} due to {}; might be a partial value", s, i, terms[i].strip());
161                return; 
162            } 
163            aFrame = tFrame;
164            match = true;
165        } else {
166            // assume single hex string - drop leading x if present
167            if (aString.startsWith("x")) aString = aString.substring(1);
168            if (aString.startsWith("X")) aString = aString.substring(1);
169            int len = aString.length() / 2;
170            int[] tFrame  = new int[len];
171            // get the frame data
172            try {
173                for (int i = 0; i < len; i++) {
174                    String two = aString.substring(2 * i, 2 * i + 2);
175                    tFrame[i] = Integer.parseInt(two, 16);
176                }
177            } catch (NumberFormatException ex) { 
178                log.debug("failed to parse EventID \"{}\"; might be a partial value", s);
179                return;
180            }  // leaving the string unparsed
181            aFrame = tFrame;
182            match = true;
183        }
184    }
185
186    /**
187     * Two addresses are equal if they result in the same numeric contents
188     */
189    @Override
190    public boolean equals(Object r) {
191        if (r == null) {
192            return false;
193        }
194        if (!(r.getClass().equals(this.getClass()))) { // final class simplifies this
195            return false;
196        }
197        OlcbAddress opp = (OlcbAddress) r;
198        if (this.aFrame == null || opp.aFrame == null) {
199            // one or the other has just a string, e.g A;B form.
200            // compare strings
201            return this.aString.equals(opp.aString);
202        }
203        if (opp.aFrame.length != this.aFrame.length) {
204            return false;
205        }
206        for (int i = 0; i < this.aFrame.length; i++) {
207            if (this.aFrame[i] != opp.aFrame[i]) {
208                return false;
209            }
210        }
211        return true;
212    }
213
214    @Override
215    public int hashCode() {
216        int ret = 0;
217        for (int value : this.aFrame) {
218            ret += value*8; // don't want to overflow int, do want to spread out
219        }
220        return ret;
221    }
222
223    public int compare(@Nonnull OlcbAddress opp) {
224        // if neither matched, just do a lexical sort
225        if (!match && !opp.match) return aString.compareTo(opp.aString);
226
227        // match (single address) sorts before non-matched (double address)
228        if (match && !opp.match) return -1;
229        if (!match && opp.match) return +1;
230
231        // both matched, usual case: comparing on content
232        for (int i = 0; i < Math.min(aFrame.length, opp.aFrame.length); i++) {
233            if (aFrame[i] != opp.aFrame[i]) return Integer.signum(aFrame[i] - opp.aFrame[i]);
234        }
235        // check for different length (shorter sorts first)
236        return Integer.signum(aFrame.length - opp.aFrame.length);
237    }
238
239    public CanMessage makeMessage() {
240        CanMessage c = new CanMessage(aFrame, 0x195B4000);
241        c.setExtended(true);
242        return c;
243    }
244
245    /**
246     * Confirm that the address string (provided earlier) is fully
247     * valid.
248     * <p>
249     * This is an expensive call. It's complete-compliance done
250     * using a regular expression. It can reject some
251     * forms that the code will normally handle OK.
252     * @return true if valid, else false.
253     */
254    public boolean check() {
255        return getMatcher().reset(aString).matches();
256    }
257
258    boolean match(CanReply r) {
259        // check address first
260        if (r.getNumDataElements() != aFrame.length) {
261            return false;
262        }
263        for (int i = 0; i < aFrame.length; i++) {
264            if (aFrame[i] != r.getElement(i)) {
265                return false;
266            }
267        }
268        // check for event message type
269        if (!r.isExtended()) {
270            return false;
271        }
272        return (r.getHeader() & 0x1FFFF000) == 0x195B4000;
273    }
274
275    boolean match(CanMessage r) {
276        // check address first
277        if (r.getNumDataElements() != aFrame.length) {
278            return false;
279        }
280        for (int i = 0; i < aFrame.length; i++) {
281            if (aFrame[i] != r.getElement(i)) {
282                return false;
283            }
284        }
285        // check for event message type
286        if (!r.isExtended()) {
287            return false;
288        }
289        return (r.getHeader() & 0x1FFFF000) == 0x195B4000;
290    }
291
292    /**
293     * Split a string containing one or more addresses into individual ones.
294     *
295     * @return null if entire string can't be parsed.
296     */
297     @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
298        justification = "Documented API, no resources to improve")
299    public OlcbAddress[] split(final CanSystemConnectionMemo memo) {
300        // reject strings ending in ";"
301        if (aString == null || aString.endsWith(";")) {
302            return null;
303        }
304
305        // split string at ";" points
306        String[] pStrings = aString.split(";");
307
308        OlcbAddress[] retval = new OlcbAddress[pStrings.length];
309
310        for (int i = 0; i < pStrings.length; i++) {
311            // check validity of each
312            if (pStrings[i].equals("")) {
313                return null;
314            }
315
316            // too expensive to do full regex check here, as this is used a lot in e.g. sorts
317            // if (!getMatcher().reset(pStrings[i]).matches()) return null;
318
319            retval[i] = new OlcbAddress(pStrings[i], memo);
320            if (!retval[i].match) {
321                return null;
322            }
323        }
324        return retval;
325    }
326
327    public boolean checkSplit( final CanSystemConnectionMemo memo) {
328        return (split(memo) != null);
329    }
330
331    int[] elements() {
332        return aFrame;
333    }
334
335    @Override
336    /**
337     * @return The string that was used to create this address
338     */
339    public String toString() {
340        return aString;
341    }
342
343    /**
344     * @return The canonical form of 0x1122334455667788
345     */
346    public String toCanonicalString() {
347        String retval = "x";
348        for (int value : aFrame) {
349            retval = jmri.util.StringUtil.appendTwoHexFromInt(value, retval);
350        }
351        return retval;
352    }
353
354    /**
355     * Provide as dotted pairs.
356     * @return dotted pair form off string.
357     */
358    public String toDottedString() {
359        String retval = "";
360        if (aFrame == null) return retval;
361        for (int value : aFrame) {
362            if (!retval.isEmpty())
363                retval += ".";
364            retval = jmri.util.StringUtil.appendTwoHexFromInt(value, retval);
365        }
366        return retval;
367    }
368
369    /**
370     * @return null if no valid address was parsed earlier, e.g. there was a ; in the data
371     */
372    public EventID toEventID() {
373        if (aFrame == null) return null;
374        byte[] b = new byte[8];
375        for (int i = 0; i < Math.min(8, aFrame.length); ++i) b[i] = (byte)aFrame[i];
376        return new EventID(b);
377    }
378
379    /**
380     * Was this parsed from a name (e.g. not explicit ID, not pair)
381     * @return true if constructed from an event name
382     */
383    public boolean isFromName() { return fromName; }
384    /**
385     * Validates Strings for OpenLCB format.
386     * @param name   the system name to validate.
387     * @param locale the locale for a localized exception.
388     * @param prefix system prefix, eg. MT for OpenLcb turnout.
389     * @return the unchanged value of the name parameter.
390     * @throws jmri.NamedBean.BadSystemNameException if provided name is an invalid format.
391     */
392    @Nonnull
393    public static String validateSystemNameFormat(@Nonnull String name, @Nonnull java.util.Locale locale,
394        @Nonnull String prefix, final CanSystemConnectionMemo memo) throws BadSystemNameException {
395        String oAddr = name.substring(prefix.length());
396        OlcbAddress a = new OlcbAddress(oAddr, memo);
397        OlcbAddress[] v = a.split(memo);
398        if (v == null) {
399            throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Did not find usable system name: " + name + " does not convert to a valid Olcb address");
400        }
401        switch (v.length) {
402            case 1:
403            case 2:
404                break;
405            default:
406                throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Wrong number of events in address: " + name);
407        }
408        return name;
409    }
410
411    /**
412     * Validates 2 part Hardware Address Strings for OpenLCB format.
413     * @param name   the system name to validate.
414     * @param locale the locale for a localized exception.
415     * @param prefix system prefix, eg. MT for OpenLcb turnout.
416     * @return the unchanged value of the name parameter.
417     * @throws jmri.NamedBean.BadSystemNameException if provided name is an invalid format.
418     */
419    @Nonnull
420    public static String validateSystemNameFormat2Part(@Nonnull String name, @Nonnull java.util.Locale locale,
421        @Nonnull String prefix, final CanSystemConnectionMemo memo) throws BadSystemNameException {
422        String oAddr = name.substring(prefix.length());
423        OlcbAddress a = new OlcbAddress(oAddr, memo);
424        OlcbAddress[] v = a.split(memo);
425        if (v == null) {
426            throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Did not find usable system name: " + name + " to a valid Olcb address");
427        }
428        if ( v.length == 2 ) {
429            return name;
430        }
431        throw new BadSystemNameException(locale,"InvalidSystemNameCustom","Address requires 2 Events: " + name);
432    }
433
434    /**
435     * See {@link jmri.NamedBean#compareSystemNameSuffix} for background.
436     * This is a common implementation for OpenLCB Sensors and Turnouts
437     * of the comparison method.
438     *
439     * @param suffix1 1st suffix to compare.
440     * @param suffix2 2nd suffix to compare.
441     * @return true if suffixes match, else false.
442     */
443    @CheckReturnValue
444    public static int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, final CanSystemConnectionMemo memo) {
445
446        // extract addresses
447        OlcbAddress[] array1 = new OlcbAddress(suffix1, memo).split(memo);
448        OlcbAddress[] array2 = new OlcbAddress(suffix2, memo).split(memo);
449
450        // compare on content
451        for (int i = 0; i < Math.min(array1.length, array2.length); i++) {
452            int c = array1[i].compare(array2[i]);
453            if (c != 0) return c;
454        }
455        // check for different length (shorter sorts first)
456        return Integer.signum(array1.length - array2.length);
457    }
458
459    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbAddress.class);
460
461}
462
463
464