001package jmri.jmrix.dcc4pc;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.LinkedHashMap;
006import java.util.List;
007import java.util.Map;
008
009/**
010 * Works out which locomotives are in which block from Raw encoding packets.
011 * <p>
012 * Channel 1 is a broadcast: every decoder in a block sends its address in the
013 * same time slot, so with two locos the ADR_LOW halves collide into invalid
014 * codes. Channel 2 is only used by the decoder the DCC packet was addressed
015 * to, so a valid channel 2 reply on an input right after a packet to address
016 * X means X is in that block. A clean ADR_HIGH + ADR_LOW pair on channel 1 is
017 * also accepted, which covers a lone loco the command station isn't
018 * addressing.
019 * <p>
020 * A loco stays in a block until the block reads unoccupied, or until it has
021 * been seen in another block and not in this one for {@link #MOVE_MILLIS}.
022 * There is no plain timeout: a stopped decoder can go many seconds without a
023 * channel 2 reply.
024 * <p>
025 * A loco that is still present and heard again is reported as a
026 * {@link Change#refresh refresh}, at most every {@link #REFRESH_MILLIS}, so
027 * its "last seen" time keeps up without flooding the GUI.
028 *
029 * @author swalton00 Copyright (C) 2026
030 */
031public class Dcc4PcBlockLocos {
032
033    /** How long a loco seen elsewhere keeps its old block, to allow for bridging a gap. */
034    public static final long MOVE_MILLIS = 2000;
035
036    /** Shortest time between refreshes for a loco that stays in a block. */
037    public static final long REFRESH_MILLIS = 1000;
038
039    /**
040     * RailCom from a loco on an input proves the input is occupied, whatever
041     * the debounced state says. The debounced state lags the raw one (a block
042     * becomes occupied after 2 packets, and unoccupied only after 300 ms), so
043     * a loco entering a block can be heard before its block reads occupied.
044     * An unoccupied reading therefore only removes locos not heard on that
045     * input for this long, which matches the unoccupied hold in the sensor
046     * manager: a block that has really been empty that long has had no RailCom.
047     */
048    public static final long HEARD_PROVES_OCCUPIED_MILLIS = 300;
049
050    /** Added to an address to mark it as a long (14-bit) address. */
051    public static final int LONG = 0x10000;
052
053    /** A loco added to or removed from a block, or heard again in it. */
054    public static class Change {
055
056        /** Input number, from 1. */
057        public final int input;
058        /** Address, with {@link #LONG} added for a long address. */
059        public final int address;
060        public final boolean added;
061        /** True if the loco was already in the block and has been heard again; {@link #added} is false. */
062        public final boolean refresh;
063        /** Why, for the log. */
064        public final String how;
065
066        Change(int input, int address, boolean added, String how) {
067            this(input, address, added, false, how);
068        }
069
070        private Change(int input, int address, boolean added, boolean refresh, String how) {
071            this.input = input;
072            this.address = address;
073            this.added = added;
074            this.refresh = refresh;
075            this.how = how;
076        }
077
078        static Change heard(int input, int address, String how) {
079            return new Change(input, address, false, true, how);
080        }
081
082        @Override
083        public String toString() {
084            return (refresh ? "=" : added ? "+" : "-") + name(address) + " on input " + input + " (" + how + ")";
085        }
086    }
087
088    private final int[] inputs;
089    /** Per enabled input: address -> last time evidence was seen. */
090    private final List<Map<Integer, Long>> seen = new ArrayList<>();
091    /** Per enabled input: address -> when it was last added or refreshed. */
092    private final List<Map<Integer, Long>> reported = new ArrayList<>();
093    private final Integer[] adrHigh;
094
095    /**
096     * @param inputs the enabled inputs, numbered from 1, in the decoder's order
097     */
098    public Dcc4PcBlockLocos(int[] inputs) {
099        this.inputs = inputs.clone();
100        adrHigh = new Integer[inputs.length];
101        for (int i = 0; i < inputs.length; i++) {
102            seen.add(new LinkedHashMap<>());
103            reported.add(new LinkedHashMap<>());
104        }
105    }
106
107    /**
108     * @param input input number, from 1
109     * @return the locos in that block, in the order they were first seen
110     */
111    public List<Integer> locos(int input) {
112        int i = indexOf(input);
113        return i < 0 ? new ArrayList<>() : new ArrayList<>(seen.get(i).keySet());
114    }
115
116    /** Forget everything, as after a resync: the next packets rebuild it. */
117    public void clear() {
118        Arrays.fill(adrHigh, null);
119        for (Map<Integer, Long> m : seen) {
120            m.clear();
121        }
122        for (Map<Integer, Long> m : reported) {
123            m.clear();
124        }
125    }
126
127    /**
128     * Apply one decoded packet.
129     *
130     * @param p      the packet
131     * @param states the input states to use, debounced, in the decoder's order
132     * @param now    the time, in milliseconds
133     * @return what changed
134     */
135    public List<Change> apply(Dcc4PcRawDecoder.Packet p, int[] states, long now) {
136        List<Change> changes = new ArrayList<>();
137        if (p.kind == Dcc4PcRawDecoder.Kind.OVERFLOW) {
138            // spec 9.3: don't pair an ADR_HIGH from before an overflow with an ADR_LOW after it
139            Arrays.fill(adrHigh, null);
140            return changes;
141        }
142        if (p.kind != Dcc4PcRawDecoder.Kind.DCC) {
143            return changes;
144        }
145        for (int i = 0; i < inputs.length; i++) {
146            if (states[i] == Dcc4PcRawDecoder.STATE_UNOCCUPIED) {
147                adrHigh[i] = null;
148                for (Map.Entry<Integer, Long> e : new ArrayList<>(seen.get(i).entrySet())) {
149                    if (now - e.getValue() < HEARD_PROVES_OCCUPIED_MILLIS) {
150                        continue; // heard here just now, so the block is occupied
151                    }
152                    seen.get(i).remove(e.getKey());
153                    reported.get(i).remove(e.getKey());
154                    changes.add(new Change(inputs[i], e.getKey(), false, "block unoccupied"));
155                }
156            }
157        }
158        int dest = mobileAddress(p.dcc);
159        for (Dcc4PcRawDecoder.InputData d : p.railCom) {
160            int i = indexOf(d.input);
161            if (i < 0 || d.data == null) {
162                continue;
163            }
164            int[] ch1 = Arrays.copyOfRange(d.data, 0, Math.min(2, d.data.length));
165            int[] ch2 = Arrays.copyOfRange(d.data, ch1.length, d.data.length);
166            // Only fresh data counts as evidence. A duplicate just says "the same bytes as
167            // the Nth previous packet", and those bytes can be another loco's reply: on the
168            // layout, packets to short 9 and long 9999 (idle throttle slots, no such locos)
169            // were "answered" this way, even when the earlier packet went to the same
170            // address. Fresh replies come often enough to find every loco.
171            if (dest >= 0 && d.duplicateOf == 0 && hasValidReply(ch2)) {
172                see(i, dest, now, "channel 2 reply " + hex(d.data) + " after DCC " + hex(p.dcc), changes);
173            }
174            if (ch1.length == 2) {
175                int a = decode(ch1[0]);
176                int b = decode(ch1[1]);
177                if (a < 0 || b < 0) {
178                    // a collision or noise: an ADR_HIGH held can't pair with what follows
179                    adrHigh[i] = null;
180                    continue;
181                }
182                int id = a >> 2;
183                int payload = ((a & 0x03) << 6) | b;
184                if (id == 1) { // ADR_HIGH
185                    adrHigh[i] = payload;
186                } else if (id == 2 && adrHigh[i] != null) { // ADR_LOW
187                    int address = address(adrHigh[i], payload);
188                    if (address > 0) {
189                        see(i, address, now, "channel 1 address " + hex(ch1) + " (ADR_HIGH " + Integer.toHexString(adrHigh[i]) + ")", changes);
190                    }
191                }
192            }
193        }
194        return changes;
195    }
196
197    private void see(int i, int address, long now, String how, List<Change> changes) {
198        if (seen.get(i).put(address, now) == null) {
199            reported.get(i).put(address, now);
200            changes.add(new Change(inputs[i], address, true, how));
201        } else if (now - reported.get(i).getOrDefault(address, 0L) >= REFRESH_MILLIS) {
202            reported.get(i).put(address, now);
203            changes.add(Change.heard(inputs[i], address, how));
204        }
205        for (int j = 0; j < inputs.length; j++) {
206            Long last = j == i ? null : seen.get(j).get(address);
207            if (last != null && now - last > MOVE_MILLIS) {
208                seen.get(j).remove(address);
209                reported.get(j).remove(address);
210                changes.add(new Change(inputs[j], address, false, "seen on input " + inputs[i]));
211            }
212        }
213    }
214
215    private int indexOf(int input) {
216        for (int i = 0; i < inputs.length; i++) {
217            if (inputs[i] == input) {
218                return i;
219            }
220        }
221        return -1;
222    }
223
224    /**
225     * @param address an address as used here
226     * @return it as text, such as "9328(L)" or "3(S)"
227     */
228    public static String name(int address) {
229        return (address & ~LONG) + ((address & LONG) != 0 ? "(L)" : "(S)");
230    }
231
232    /**
233     * The locomotive address a DCC packet is sent to.
234     *
235     * @param dcc the packet bytes, without the error byte or with it
236     * @return the address, with {@link #LONG} added for a long address, or -1
237     *         for a broadcast, idle, accessory or malformed packet
238     */
239    public static int mobileAddress(int[] dcc) {
240        if (dcc == null || dcc.length == 0) {
241            return -1;
242        }
243        int b0 = dcc[0] & 0xFF;
244        if (b0 >= 1 && b0 <= 127) {
245            return b0;
246        }
247        if (b0 >= 192 && b0 <= 231 && dcc.length >= 2) {
248            return LONG | ((b0 & 0x3F) << 8) | (dcc[1] & 0xFF);
249        }
250        return -1;
251    }
252
253    /**
254     * Combine channel 1 ADR_HIGH and ADR_LOW payloads (RCN-217 5.2.1).
255     *
256     * @return the address, with {@link #LONG} added for a long address, or -1
257     *         if ADR_HIGH isn't a valid address pattern
258     */
259    static int address(int high, int low) {
260        if (high == 0) {
261            return low & 0x7F;
262        }
263        if ((high & 0xC0) == 0x80) {
264            return LONG | ((high & 0x3F) << 8) | (low & 0xFF);
265        }
266        if ((high & 0xF0) == 0x60) {
267            return low & 0x7F; // consist address
268        }
269        return -1;
270    }
271
272    /**
273     * True if channel 2 holds at least one well-formed datagram or an
274     * ACK/NACK/BUSY.
275     */
276    static boolean hasValidReply(int[] ch2) {
277        int pos = 0;
278        while (pos < ch2.length) {
279            int first = decode(ch2[pos]);
280            if (first == SPECIAL) {
281                return true;
282            }
283            if (first < 0) {
284                pos++;
285                continue;
286            }
287            int id = first >> 2;
288            int symbols = (id == 3 || id == 7) ? 3 : (id >= 8 && id <= 11) ? 6 : 2;
289            if (pos + symbols > ch2.length) {
290                return false;
291            }
292            boolean valid = true;
293            for (int k = 1; k < symbols; k++) {
294                if (decode(ch2[pos + k]) < 0) {
295                    valid = false;
296                    break;
297                }
298            }
299            if (valid) {
300                return true;
301            }
302            pos += symbols;
303        }
304        return false;
305    }
306
307    static final int INVALID = -1;
308    /** ACK, NACK or BUSY. */
309    static final int SPECIAL = -2;
310
311    /** ENCODE[value] is the RailCom 4-of-8 line code for a six-bit value. */
312    private static final int[] ENCODE = {
313        0xAC, 0xAA, 0xA9, 0xA5, 0xA3, 0xA6, 0x9C, 0x9A,
314        0x99, 0x95, 0x96, 0x93, 0x8E, 0x8D, 0x8B, 0xB1,
315        0xB2, 0xB4, 0xB8, 0x74, 0x72, 0x6C, 0x6A, 0x69,
316        0x66, 0x65, 0x63, 0x5C, 0x5A, 0x59, 0x55, 0x56,
317        0x53, 0x4E, 0x4D, 0x4B, 0x47, 0x71, 0xE8, 0xE4,
318        0xE2, 0xD1, 0xC9, 0xC5, 0xD8, 0xD4, 0xD2, 0xCA,
319        0xC6, 0xCC, 0x78, 0x17, 0x1B, 0x1D, 0x1E, 0x2E,
320        0x36, 0x3A, 0x27, 0x2B, 0x2D, 0x35, 0x39, 0x33};
321
322    private static final int[] DECODE = new int[256];
323
324    static {
325        Arrays.fill(DECODE, INVALID);
326        for (int v = 0; v < ENCODE.length; v++) {
327            DECODE[ENCODE[v]] = v;
328        }
329        DECODE[0x0F] = SPECIAL; // ACK
330        DECODE[0xF0] = SPECIAL; // ACK
331        DECODE[0x3C] = SPECIAL; // NACK
332        DECODE[0xE1] = SPECIAL; // BUSY
333    }
334
335    /** @return the bytes as hex, for the log */
336    static String hex(int[] bytes) {
337        if (bytes == null) {
338            return "?";
339        }
340        StringBuilder sb = new StringBuilder();
341        for (int b : bytes) {
342            if (sb.length() > 0) {
343                sb.append(' ');
344            }
345            sb.append(String.format("%02X", b & 0xFF));
346        }
347        return sb.toString();
348    }
349
350    /**
351     * @return the six-bit value of a 4-of-8 code, {@link #SPECIAL} or
352     *         {@link #INVALID}
353     */
354    static int decode(int b) {
355        return DECODE[b & 0xFF];
356    }
357}