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