001package jmri.jmrix;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.*;
006import javax.annotation.Nonnull;
007import javax.annotation.concurrent.GuardedBy;
008
009import jmri.BasicRosterEntry;
010import jmri.DccLocoAddress;
011import jmri.DccThrottle;
012import jmri.LocoAddress;
013import jmri.SpeedStepMode;
014import jmri.SystemConnectionMemo;
015import jmri.Throttle;
016import jmri.ThrottleListener;
017import jmri.ThrottleManager;
018import jmri.util.swing.JmriJOptionPane;
019
020/**
021 * Abstract implementation of a ThrottleManager.
022 * <p>
023 * Based on Glen Oberhauser's original {@link jmri.jmrix.loconet.LnThrottleManager} implementation.
024 *
025 * @author Bob Jacobsen Copyright (C) 2001
026 * @author Steve Rawlinson Copyright (C) 2016
027 */
028abstract public class AbstractThrottleManager implements ThrottleManager {
029
030    public AbstractThrottleManager() {
031    }
032
033    public AbstractThrottleManager(SystemConnectionMemo memo) {
034        adapterMemo = memo;
035    }
036
037    protected SystemConnectionMemo adapterMemo;
038
039    protected String userName = "Internal";
040
041    /**
042     * {@inheritDoc}
043     */
044    @Override
045    public String getUserName() {
046        if (adapterMemo != null) {
047            return adapterMemo.getUserName();
048        }
049        return userName;
050    }
051
052    /**
053     * By default, only DCC in this implementation
054     */
055    @Override
056    public String[] getAddressTypes() {
057        return new String[]{
058            LocoAddress.Protocol.DCC.getPeopleName(), 
059            LocoAddress.Protocol.DCC_SHORT.getPeopleName(), 
060            LocoAddress.Protocol.DCC_LONG.getPeopleName()};
061    }
062
063    /**
064     * By default, only DCC in this implementation
065     */
066    @Override
067    public String getAddressTypeString(LocoAddress.Protocol prot) {
068        return prot.getPeopleName();
069    }
070
071    /**
072     * {@inheritDoc}
073     */
074    @Override
075    public LocoAddress.Protocol[] getAddressProtocolTypes() {
076        return new LocoAddress.Protocol[]{
077            LocoAddress.Protocol.DCC, 
078            LocoAddress.Protocol.DCC_SHORT, 
079            LocoAddress.Protocol.DCC_LONG};
080    }
081
082    /**
083     * {@inheritDoc}
084     */
085    @Override
086    public LocoAddress getAddress(String value, LocoAddress.Protocol protocol) {
087        if (value == null) {
088            return null;
089        }
090        if (protocol == null) {
091            return null;
092        }
093        int num = Integer.parseInt(value);
094
095        // if DCC long and can't be, or short and can't be, fix
096        if ((LocoAddress.Protocol.DCC == protocol || LocoAddress.Protocol.DCC_SHORT == protocol) && !canBeShortAddress(num)) {
097            protocol = LocoAddress.Protocol.DCC_LONG;
098        }
099        if ((LocoAddress.Protocol.DCC == protocol || LocoAddress.Protocol.DCC_LONG == protocol) && !canBeLongAddress(num)) {
100            protocol = LocoAddress.Protocol.DCC_SHORT;
101        }
102
103        // if still ambiguous, prefer short
104        if (protocol == LocoAddress.Protocol.DCC) {
105            protocol = LocoAddress.Protocol.DCC_SHORT;
106        }
107
108        return new DccLocoAddress(num, protocol);
109    }
110
111    /**
112     * {@inheritDoc}
113     */
114    @Override
115    public LocoAddress getAddress(String value, String protocol) {
116        if (value == null) {
117            return null;
118        }
119        if (protocol == null) {
120            return null;
121        }
122        LocoAddress.Protocol p = getProtocolFromString(protocol);
123
124        return getAddress(value, p);
125    }
126
127    /**
128     * {@inheritDoc}
129     */
130    @Override
131    public LocoAddress.Protocol getProtocolFromString(String selection) {
132        return LocoAddress.Protocol.getByPeopleName(selection);
133    }
134
135    /**
136     * throttleListeners is indexed by the address, and contains as elements an
137     * ArrayList of WaitingThrottle objects, each of which has one ThrottleListener.
138     * This allows more than one ThrottleListener to request a throttle at a time.
139     * The entries in this Hashmap are only valid during the throttle setup process.
140     */
141    @GuardedBy("this")
142    private final HashMap<LocoAddress, ArrayList<WaitingThrottle>> throttleListeners = new HashMap<>(5);
143
144    static class WaitingThrottle {
145
146        ThrottleListener l;
147        BasicRosterEntry re;
148        PropertyChangeListener pl;
149        boolean canHandleDecisions;
150
151        WaitingThrottle(ThrottleListener _l, BasicRosterEntry _re, boolean _canHandleDecisions) {
152            l = _l;
153            re = _re;
154            canHandleDecisions = _canHandleDecisions;
155        }
156
157        WaitingThrottle(PropertyChangeListener _pl, BasicRosterEntry _re, boolean _canHandleDecisions) {
158            pl = _pl;
159            re = _re;
160            canHandleDecisions = _canHandleDecisions;
161        }
162
163        PropertyChangeListener getPropertyChangeListener() {
164            return pl;
165        }
166
167        ThrottleListener getListener() {
168            return l;
169        }
170
171        BasicRosterEntry getRosterEntry() {
172            return re;
173        }
174        
175        boolean canHandleDecisions() {
176            return canHandleDecisions;
177        }
178        
179    }
180    
181    /**
182     * listenerOnly is indexed by the address, and contains as elements an
183     * ArrayList of propertyChangeListeners objects that have requested
184     * notification of changes to a throttle that hasn't yet been created. The
185     * entries in this Hashmap are only valid during the throttle setup process.
186     */
187    @GuardedBy("this")
188    private final HashMap<LocoAddress, ArrayList<WaitingThrottle>> listenerOnly = new HashMap<>(5);
189
190    /**
191     * Keeps a map of all the current active DCC loco Addresses that are in use.
192     * <p>
193     * addressThrottles is indexed by the address, and contains as elements a
194     * subclass of the throttle assigned to an address and the number of
195     * requests and active users for this address.
196     */
197    @GuardedBy("this")
198    private final Hashtable<LocoAddress, Addresses> addressThrottles = new Hashtable<>();
199
200    /**
201     * Does this DCC system allow a Throttle (e.g. an address) to be used by
202     * only one user at a time?
203     * @return true or false
204     */
205    protected boolean singleUse() {
206        return true;
207    }
208    
209    /**
210     * {@inheritDoc}
211     */
212    @Override
213    public boolean requestThrottle(int address, boolean isLongAddress, ThrottleListener l, boolean canHandleDecisions) {
214        DccLocoAddress la = new DccLocoAddress(address, isLongAddress);
215        return requestThrottle(la, null, l, canHandleDecisions);
216    }
217    
218    /**
219     * {@inheritDoc}
220     */
221    @Override
222    public boolean requestThrottle(@Nonnull BasicRosterEntry re, ThrottleListener l, boolean canHandleDecisions) {
223        return requestThrottle(re.getDccLocoAddress(), re, l, canHandleDecisions);
224    }
225
226    /**
227     * {@inheritDoc}
228     */
229    @Override
230    public boolean requestThrottle(LocoAddress la, ThrottleListener l, boolean canHandleDecisions) {
231        return requestThrottle(la, null, l, canHandleDecisions);
232    }
233    
234    /**
235     * Request a throttle, given a decoder address.
236     * <p>
237     * When the decoder address is 
238     * located, the ThrottleListener gets a callback via the
239     * ThrottleListener.notifyThrottleFound method.
240     *
241     * @param la LocoAddress of the decoder desired.
242     * @param l  The ThrottleListener awaiting notification of a found throttle.
243     * @param re A BasicRosterEntry can be passed, this is attached to a throttle after creation.
244     * @param canHandleDecisions true if theThrottleListener can make a steal or share decision, else false.
245     * @return True if the request will continue, false if the request will not
246     *         be made. False may be returned if a the throttle is already in
247     *         use.
248     */
249    protected synchronized boolean requestThrottle(LocoAddress la, BasicRosterEntry re, ThrottleListener l, boolean canHandleDecisions) {
250        boolean throttleFree = true;
251        
252        // check for a valid throttle address
253        if (!canBeLongAddress(la.getNumber()) && !canBeShortAddress(la.getNumber())) {
254            return false;
255        }
256
257        // put the list in if not present
258        ArrayList<WaitingThrottle> a;
259        if (!throttleListeners.containsKey(la)) {
260            throttleListeners.put(la, new ArrayList<>());
261        }
262        // get the corresponding list to check length
263        a = throttleListeners.get(la);
264        if (addressThrottles.containsKey(la)) {
265            log.debug("A throttle to address {} already exists, so will return that throttle", la.getNumber());
266            a.add(new WaitingThrottle(l, re, canHandleDecisions));
267            notifyThrottleKnown(addressThrottles.get(la).getThrottle(), la);
268            return throttleFree;
269        } else {
270            log.debug("LocoAddress {} has not been created before", la.getNumber());
271        }
272
273        log.debug("After request in ATM: {}", a.size());
274        
275        // check length
276        if (singleUse() && (a.size() > 0)) {
277            throttleFree = false;
278            log.debug("singleUser() is true, and the list of WaitingThrottles isn't empty, returning false");
279        } else if (a.size() == 0) {
280            a.add(new WaitingThrottle(l, re, canHandleDecisions));
281            log.debug("list of WaitingThrottles is empty: {}; {}", la, a);
282            log.debug("calling requestThrottleSetup()");
283            requestThrottleSetup(la, true);
284        } else {
285            a.add(new WaitingThrottle(l, re, canHandleDecisions));
286            log.debug("singleUse() returns false and there are existing WaitThrottles, adding a one to the list");
287        }
288        return throttleFree;
289    }
290
291    /**
292     * Request Throttle with no Steal / Share Callbacks
293     * {@inheritDoc}
294     * Request a throttle, given a decoder address. When the decoder address is
295     * located, the ThrottleListener gets a callback via the
296     * ThrottleListener.notifyThrottleFound method.
297     * <p>
298     * This is a convenience version of the call, which uses system-specific
299     * logic to tell whether the address is a short or long form.
300     *
301     * @param address The decoder address desired.
302     * @param l       The ThrottleListener awaiting notification of a found
303     *                throttle.
304     * @return True if the request will continue, false if the request will not
305     *         be made. False may be returned if a the throttle is already in
306     *         use.
307     */
308    @Override
309    public boolean requestThrottle(int address, ThrottleListener l) {
310        boolean isLong = true;
311        if (canBeShortAddress(address)) {
312            isLong = false;
313        }
314        return requestThrottle(new DccLocoAddress(address, isLong), null, l, false);
315    }
316    
317    /**
318     * {@inheritDoc}
319     */
320    @Override
321    public boolean requestThrottle(int address, ThrottleListener l, boolean canHandleDecisions) {
322        boolean isLong = true;
323        if (canBeShortAddress(address)) {
324            isLong = false;
325        }
326        return requestThrottle(new DccLocoAddress(address, isLong), null, l, canHandleDecisions);
327    }
328
329    /**
330     * Abstract member to actually do the work of configuring a new throttle, 
331     * usually via interaction with the DCC system.
332     * @param a  address
333     * @param control  false  - read only.
334     */
335    abstract public void requestThrottleSetup(LocoAddress a, boolean control);
336
337    /**
338     * Abstract member to actually do the work of configuring a new throttle, 
339     * usually via interaction with the DCC system
340     * @param a  address.
341     */
342    public void requestThrottleSetup(LocoAddress a) {
343        requestThrottleSetup(a, true);
344    }
345    
346    /**
347     * {@inheritDoc}
348     */
349    @Override
350    public void cancelThrottleRequest(int address, boolean isLong, ThrottleListener l) {
351        DccLocoAddress la = new DccLocoAddress(address, isLong);
352        cancelThrottleRequest(la, l);
353    }
354
355    /**
356     * {@inheritDoc}
357     */
358    @Override
359    public void cancelThrottleRequest(BasicRosterEntry re, ThrottleListener l) {
360            cancelThrottleRequest(re.getDccLocoAddress(), l);
361    }
362    
363    /**
364     * {@inheritDoc}
365     */
366    @Override
367    public synchronized void cancelThrottleRequest(LocoAddress la, ThrottleListener l) {
368        // failedThrottleRequest(la, "Throttle request was cancelled."); // needs I18N
369        ArrayList<WaitingThrottle> a = throttleListeners.get(la);
370        if (a == null || l == null ) {
371            return;
372        }
373        a.removeIf(wt -> l == wt.getListener()); // Safely remove the current element from the iterator and the list
374    }
375    
376    /**
377     * {@inheritDoc}
378     * Cancel a request for a throttle.
379     * <p>
380     * This is a convenience version of the call, which uses system-specific
381     * logic to tell whether the address is a short or long form.
382     *
383     * @param address The decoder address desired.
384     * @param l       The ThrottleListener cancelling request for a throttle.
385     */
386    @Override
387    public void cancelThrottleRequest(int address, ThrottleListener l) {
388        boolean isLong = true;
389        if (canBeShortAddress(address)) {
390            isLong = false;
391        }
392        cancelThrottleRequest(address, isLong, l);
393    }
394    
395    /**
396     * {@inheritDoc}
397     */
398    @Override
399    public void responseThrottleDecision(int address, ThrottleListener l, ThrottleListener.DecisionType decision) {
400        boolean isLong = true;
401        if (canBeShortAddress(address)) {
402            isLong = false;
403        }
404        responseThrottleDecision(address, isLong, l, decision);
405
406    }
407    
408    /**
409     * {@inheritDoc}
410     */
411    @Override
412    public void responseThrottleDecision(int address, boolean isLong, ThrottleListener l, ThrottleListener.DecisionType decision) {
413        DccLocoAddress la = new DccLocoAddress(address, isLong);
414        responseThrottleDecision(la, l, decision);
415    }
416
417    /**
418     * {@inheritDoc}
419     */
420    @Override
421    public void responseThrottleDecision(LocoAddress address, ThrottleListener l, ThrottleListener.DecisionType decision) {
422        log.debug("Received response from ThrottleListener, this method should be overridden by a hardware type");
423    }
424
425    /**
426     * If the system-specific ThrottleManager has been unable to create the DCC
427     * throttle then it needs to be removed from the throttleListeners, 
428     * otherwise any subsequent request for that address results in the address
429     * being reported as already in use, if singleUse is set. This also sends a
430     * notification message back to the requestor with a string reason as to why
431     * the request has failed.
432     *
433     * @param address The Loco Address that the request failed on.
434     * @param reason  A text string passed by the ThrottleManager as to why
435     */
436    public synchronized void failedThrottleRequest(LocoAddress address, String reason) {
437        ArrayList<WaitingThrottle> a = throttleListeners.get(address);
438        if (a == null) {
439            log.warn("failedThrottleRequest with zero-length listeners: {}", address);
440        } else {
441            for (WaitingThrottle waitingThrottle : new ArrayList<>(a)) {
442                ThrottleListener l = waitingThrottle.getListener();
443                l.notifyFailedThrottleRequest(address, reason);
444            }
445        }
446        throttleListeners.remove(address);
447        ArrayList<WaitingThrottle> p = listenerOnly.get(address);
448        if (p == null) {
449            log.debug("failedThrottleRequest with zero-length PropertyChange listeners: {}", address);
450        } else {
451            for (WaitingThrottle waitingThrottle : p) {
452                PropertyChangeListener l = waitingThrottle.getPropertyChangeListener();
453                l.propertyChange(new PropertyChangeEvent(this, "attachFailed", address, null));
454            }
455        }
456        listenerOnly.remove(address);
457    }
458
459    /**
460     * Handle throttle information when it's finally available, e.g. when a new
461     * Throttle object has been created.
462     * <p>
463     * This method creates a throttle for all ThrottleListeners of that address
464     * and notifies them via the ThrottleListener.notifyThrottleFound method.
465     * @param throttle  throttle object
466     * @param addr  address.
467     */
468    public synchronized void notifyThrottleKnown(DccThrottle throttle, LocoAddress addr) {
469        log.debug("notifyThrottleKnown for {}", addr);
470        Addresses ads = null;
471        if (!addressThrottles.containsKey(addr)) {
472            log.debug("Address {} doesn't already exists so will add", addr);
473            ads = new Addresses(throttle);
474            addressThrottles.put(addr, ads);
475        } else {
476            addressThrottles.get(addr).setThrottle(throttle);
477        }
478        ArrayList<WaitingThrottle> a = throttleListeners.get(addr);
479        if (a == null) {
480            log.debug("notifyThrottleKnown with zero-length listeners: {}", addr);
481        } else {
482            for (int i = 0; i < a.size(); i++) {
483                ThrottleListener l = a.get(i).getListener();
484                log.debug("Notify listener {} of {}", (i + 1), a.size() );
485                l.notifyThrottleFound(throttle);
486                addressThrottles.get(addr).incrementUse();
487                addressThrottles.get(addr).addListener(l);
488                if (ads != null && a.get(i).getRosterEntry() != null && throttle.getRosterEntry() == null) {
489                    throttle.setRosterEntry(a.get(i).getRosterEntry());
490                }
491                updateNumUsers(addr, addressThrottles.get(addr).getUseCount());
492            }
493            throttleListeners.remove(addr);
494        }
495        ArrayList<WaitingThrottle> p = listenerOnly.get(addr);
496        if (p == null) {
497            log.debug("notifyThrottleKnown with zero-length propertyChangeListeners: {}", addr);
498        } else {
499            for (WaitingThrottle waitingThrottle : p) {
500                PropertyChangeListener l = waitingThrottle.getPropertyChangeListener();
501                log.debug("Notify propertyChangeListener");
502                l.propertyChange(new PropertyChangeEvent(this, "throttleAssigned", null, addr));
503                if (ads != null && waitingThrottle.getRosterEntry() != null && throttle.getRosterEntry() == null) {
504                    throttle.setRosterEntry(waitingThrottle.getRosterEntry());
505                }
506                throttle.addPropertyChangeListener(l);
507            }
508            listenerOnly.remove(addr);
509        }
510    }
511
512
513    /**
514     * For when a steal / share decision is needed and the ThrottleListener has delegated
515     * this decision to the ThrottleManager.
516     * <p>
517     * Responds to the question by requesting a Throttle "Steal" by default.
518     * <p>
519     * Can be overridden by hardware types which do not wish the default behaviour to Steal.
520     * <p>
521     * This applies only to those systems where "stealing" or "sharing" applies, such as LocoNet.
522     * @param address The LocoAddress the steal / share question relates to
523     * @param question The Question to be put to the ThrottleListener
524     */
525    protected void makeHardwareDecision(LocoAddress address, ThrottleListener.DecisionType question){
526        responseThrottleDecision(address, null, ThrottleListener.DecisionType.STEAL );
527    }
528
529    /**
530     * When the system-specific ThrottleManager has been unable to create the DCC
531     * throttle because it is already in use and must be "stolen" or "shared" to take control, 
532     * it needs to notify the listener of this situation.
533     * <p>
534     * This applies only to those systems where "stealing" or "sharing" applies, such as LocoNet.
535     *
536     * @param address The LocoAddress the steal / share question relates to
537     * @param question The Question to be put to the ThrottleListener
538     */
539    protected synchronized void notifyDecisionRequest(LocoAddress address, ThrottleListener.DecisionType question) {
540        ArrayList<WaitingThrottle> a = throttleListeners.get(address);
541        if (a == null) {
542            log.debug("Cannot issue question. No throttle listeners registered for address {}", address.getNumber());
543            return;
544        }
545        ThrottleListener l;
546        log.debug("{} listener(s) registered for address {}", a.size(), address.getNumber());
547        for (int i = 0; i < a.size(); i++) { // enhanced for (WaitingThrottle waitingThrottle : a) doesn't work somehow
548            if (a.get(i).canHandleDecisions()) {
549                l = a.get(i).getListener();
550                log.debug("Notifying a throttle listener (address {}) of the steal share situation", address.getNumber());
551                l.notifyDecisionRequired(address, question);
552            } else {
553                log.debug("Passing {} to hardware steal / share decision making", address.getNumber());
554                makeHardwareDecision(address, question);
555            }
556        }
557    }
558
559    /**
560     * Check to see if the Dispatch Button should be enabled or not Default to
561     * true, override if necessary
562     *
563     */
564    @Override
565    public boolean hasDispatchFunction() {
566        return true;
567    }
568
569    /**
570     * What speed modes are supported by this system? value should be xor of
571     * possible modes specifed by the DccThrottle interface
572     */
573    @Override
574    public EnumSet<SpeedStepMode> supportedSpeedModes() {
575        return EnumSet.of(SpeedStepMode.NMRA_DCC_128);
576    }
577    
578    /**
579     * Hardware that uses the Silent Steal preference
580     * will need to override
581     * {@inheritDoc}
582     */
583    @Override
584    public boolean enablePrefSilentStealOption() {
585        return false;
586    }
587    
588    /**
589     * Hardware that uses the Silent Share preference
590     * will need to override
591     * {@inheritDoc}
592     */
593    @Override
594    public boolean enablePrefSilentShareOption() {
595        return false;
596    }
597    
598    /**
599     * {@inheritDoc}
600     */
601    @Override
602    public synchronized void attachListener(LocoAddress la, java.beans.PropertyChangeListener p) {
603        if (addressThrottles.containsKey(la)) {
604            addressThrottles.get(la).getThrottle().addPropertyChangeListener(p);
605            p.propertyChange(new PropertyChangeEvent(this, "throttleAssigned", null, la));
606        } else {
607            if (!listenerOnly.containsKey(la)) {
608                listenerOnly.put(la, new ArrayList<>());
609            }
610
611            // get the corresponding list to check length
612            ArrayList<WaitingThrottle> a = listenerOnly.get(la);
613            a.add(new WaitingThrottle(p, null, false));
614            //Only request that the throttle is set up if it hasn't already been
615            //requested.
616            if ((!throttleListeners.containsKey(la)) && (a.size() == 1)) {
617                requestThrottleSetup(la, false);
618            }
619        }
620    }
621
622    /**
623     * {@inheritDoc}
624     */
625    @Override
626    public synchronized void removeListener(LocoAddress la, java.beans.PropertyChangeListener p) {
627        if (addressThrottles.containsKey(la)) {
628            addressThrottles.get(la).getThrottle().removePropertyChangeListener(p);
629            p.propertyChange(new PropertyChangeEvent(this, "throttleRemoved", la, null));
630            return;
631        }
632        p.propertyChange(new PropertyChangeEvent(this, "throttleNotFoundInRemoval", la, null));
633    }
634
635    /**
636     * {@inheritDoc}
637     */
638    @Override
639    public synchronized boolean addressStillRequired(LocoAddress la) {
640        if (addressThrottles.containsKey(la)) {
641            log.debug("usage count is {}", addressThrottles.get(la).getUseCount());
642            return (addressThrottles.get(la).getUseCount() > 0);
643        }
644        return false;
645    }
646    
647    /**
648     * {@inheritDoc}
649     */
650    @Override
651    public boolean addressStillRequired(int address, boolean isLongAddress) {
652        DccLocoAddress la = new DccLocoAddress(address, isLongAddress);
653        return addressStillRequired(la);
654    }
655
656    /**
657     * {@inheritDoc}
658     */
659    @Override
660    public boolean addressStillRequired(int address) {
661        boolean isLong = true;
662        if (canBeShortAddress(address)) {
663            isLong = false;
664        }
665        return addressStillRequired(address, isLong);
666    }
667
668    /**
669     * {@inheritDoc}
670     */
671    @Override
672    public boolean addressStillRequired(BasicRosterEntry re) {
673        return addressStillRequired(re.getDccLocoAddress());
674    }
675
676    /**
677     * {@inheritDoc}
678     */
679    @Override
680    public void releaseThrottle(DccThrottle t, ThrottleListener l) {
681        log.debug("AbstractThrottleManager.releaseThrottle: {}, {}", t, l);
682        disposeThrottle(t, l);
683    }
684
685    /**
686     * {@inheritDoc}
687     */
688    @Override
689    public boolean disposeThrottle(DccThrottle t, ThrottleListener l) {
690        log.debug("AbstractThrottleManager.disposeThrottle: {}, {}", t, l);
691
692//        if (!active) log.error("Dispose called when not active");  <-- might need to control this in the sub class
693        LocoAddress la = t.getLocoAddress();
694        if (addressReleased(la, l)) {
695            log.debug("Address {} still has active users", t.getLocoAddress());
696            return false;
697        }
698        if (t.getPropertyChangeListeners().length > 0) {
699            log.debug("Throttle {} still has {} active propertyChangeListeners registered to the throttle", t.getLocoAddress(), t.getPropertyChangeListeners().length);
700            return false;
701        }
702        synchronized (this) {
703            if (addressThrottles.containsKey(la)) {
704                addressThrottles.remove(la);
705                log.debug("Loco Address {} removed from the stack ", la);
706            } else {
707                log.debug("Loco Address {} not found in the stack ", la);
708            }
709        }
710        return true;
711    }
712    
713    /**
714     * Throttle can no longer be relied upon, 
715     * potentially from an external forced steal or hardware error.
716     * <p>
717     * Normally, #releaseThrottle should be used to close throttles.
718     * <p>
719     * Removes locoaddress from list to force new throttle requests
720     * to request new sessions where the Command station model
721     * implements a dynamic stack, not a static stack.
722     * 
723     * <p>
724     * Managers still need to advise listeners that the session has 
725     * been cancelled and actually dispose of the throttle
726     * @param la address release
727     */
728    protected void forceDisposeThrottle(LocoAddress la) {
729        log.debug("force dispose address {}", la);
730        if (addressThrottles.containsKey(la)) {
731            addressThrottles.remove(la);
732            log.debug("Loco Address {} removed from the stack ", la);
733        } else {
734            log.debug("Loco Address {} not found in the stack ", la);
735        }
736    }
737
738    /**
739     * {@inheritDoc}
740     */
741    @Override
742    public void dispatchThrottle(DccThrottle t, ThrottleListener l) {
743        releaseThrottle(t, l);
744    }
745
746    /**
747     * {@inheritDoc}
748     */
749    @Override
750    public void dispose() {
751    }
752
753    /**
754     * {@inheritDoc}
755     */
756    @Override
757    public synchronized int getThrottleUsageCount(LocoAddress la) {
758        if (addressThrottles.containsKey( la)) {
759            return addressThrottles.get(la).getUseCount();
760        }
761        return 0;
762    }
763    
764    /**
765     * {@inheritDoc}
766     */
767    @Override
768    public int getThrottleUsageCount(int address, boolean isLongAddress) {
769        DccLocoAddress la = new DccLocoAddress(address, isLongAddress);
770        return getThrottleUsageCount(la);
771    }
772    
773    /**
774     * {@inheritDoc}
775     */
776    @Override
777    public int getThrottleUsageCount(int address) {
778        boolean isLong = true;
779        if (canBeShortAddress(address)) {
780            isLong = false;
781        }
782        return getThrottleUsageCount(address, isLong);
783    }
784    
785    /**
786     * {@inheritDoc}
787     */
788    @Override
789    public int getThrottleUsageCount(BasicRosterEntry re) {
790        return getThrottleUsageCount(re.getDccLocoAddress());
791    }
792
793    /**
794     * Release a Throttle from a ThrottleListener.
795     * @param la address release
796     * @param l listening object
797     * @return True if throttle still has listeners or a positive use count, else False
798     */
799    protected synchronized boolean addressReleased(LocoAddress la, ThrottleListener l) {
800        if (addressThrottles.containsKey(la)) {
801            if (addressThrottles.get(la).containsListener(l)) {
802                log.debug("decrementUse called with listener {}", l);
803                addressThrottles.get(la).decrementUse();
804                addressThrottles.get(la).removeListener(l);
805            } else if (l == null) {
806                log.debug("decrementUse called withOUT listener");
807                /*The release release has been called, but as no listener has
808                 been specified, we can only decrement the use flag*/
809                addressThrottles.get(la).decrementUse();
810            }
811        }
812        if (addressThrottles.containsKey(la)) {
813            if (addressThrottles.get(la).getUseCount() > 0) {
814                updateNumUsers(la, addressThrottles.get(la).getUseCount());
815                log.debug("addressReleased still has at least one listener");
816                return true;
817            }
818        }
819        return false;
820    }
821    
822    /**
823     * The number of users of this throttle has been updated
824     * <p>
825     * Typically used to update dispatch / release availablility
826     * specific implementations can override this function to get updates
827     *
828     * @param la the Loco Address which has been updated
829     * @param numUsers current number of users
830     */
831    protected void updateNumUsers( LocoAddress la, int numUsers ){
832        log.debug("Throttle {} now has {} users", la, numUsers);
833    }
834
835    /**
836     * {@inheritDoc}
837     */
838    @Override
839    public Object getThrottleInfo(LocoAddress la, String item) {
840        DccThrottle t;
841        synchronized (this) {
842            if (addressThrottles.containsKey(la)) {
843                t = addressThrottles.get(la).getThrottle();
844            } else {
845                return null;
846            }
847        }
848        if (item.equals(Throttle.ISFORWARD)) {
849            return t.getIsForward();
850        } else if (item.startsWith("Speed")) {
851            switch (item) {
852                case Throttle.SPEEDSETTING:
853                    return t.getSpeedSetting();
854                case Throttle.SPEEDINCREMENT:
855                    return t.getSpeedIncrement();
856                case Throttle.SPEEDSTEPMODE:
857                    return t.getSpeedStepMode();
858                default: // skip
859            }
860        }
861        for ( int i = 0; i< t.getFunctions().length; i++ ) {
862            if (item.equals(Throttle.getFunctionString(i))) {
863                return t.getFunction(i);
864            }
865        }
866        return null;
867    }
868
869    private boolean _hideStealNotifications = false;
870
871    /**
872     * If not headless, display a session stolen dialogue box with
873     * checkbox to hide notifications for rest of JMRI session
874     *
875     * @param address the LocoAddress of the stolen / cancelled Throttle
876     */
877    protected void showSessionCancelDialogue(LocoAddress address){
878        if ((!java.awt.GraphicsEnvironment.isHeadless()) && (!_hideStealNotifications)){
879            jmri.util.ThreadingUtil.runOnGUI(() -> {
880                javax.swing.JCheckBox checkbox = new javax.swing.JCheckBox(
881                    Bundle.getMessage("HideFurtherAlerts"));
882                Object[] params = {Bundle.getMessage("LocoStolen", address), checkbox};
883                java.awt.event.ActionListener stolenpopupcheckbox = (java.awt.event.ActionEvent evt) ->
884                    this.hideStealNotifications(checkbox.isSelected());
885                checkbox.addActionListener(stolenpopupcheckbox);
886                JmriJOptionPane.showMessageDialogNonModal(null, params, 
887                    Bundle.getMessage("LocoStolen", address),
888                    JmriJOptionPane.WARNING_MESSAGE, null);
889            });
890        }
891    }
892
893    /**
894     * Receive notification from a throttle dialogue
895     * to display steal dialogues for rest of the JMRI instance session.
896     * False by default to show notifications
897     *
898     * @param hide set True to hide notifications, else False.
899     */
900    public void hideStealNotifications(boolean hide){
901        _hideStealNotifications = hide;
902    }
903
904    /**
905     * This subClass keeps track of which loco address have been requested and
906     * by whom. It primarily uses an increment count to keep track of all the
907     * Addresses in use as not all external code will have been refactored over
908     * to use the new disposeThrottle.
909     */
910    protected static class Addresses {
911
912        int useActiveCount = 0;
913        DccThrottle throttle;
914        ArrayList<ThrottleListener> listeners = new ArrayList<>();
915        BasicRosterEntry re = null;
916
917        protected Addresses(DccThrottle throttle) {
918            this.throttle = throttle;
919        }
920
921        void incrementUse() {
922            useActiveCount++;
923            log.debug("{} increased Use Size to {}", throttle.getLocoAddress(), useActiveCount);
924        }
925
926        void decrementUse() {
927            // Do not want to go below 0 on the usage front!
928            if (useActiveCount > 0) {
929                useActiveCount--;
930            }
931            log.debug("{} decreased Use Size to {}", throttle.getLocoAddress(), useActiveCount);
932        }
933
934        int getUseCount() {
935            return useActiveCount;
936        }
937
938        DccThrottle getThrottle() {
939            return throttle;
940        }
941
942        void setThrottle(DccThrottle throttle) {
943            DccThrottle old = this.throttle;
944            this.throttle = throttle;
945            if ((old == null) || (old == throttle)) {
946                return;
947            }
948
949            // As the throttle has changed, we need to inform the listeners.
950            // However if a throttle hasn't used the new code, it will not have been
951            // removed and will get a notification.
952            log.debug("Throttle assigned {} has been changed, need to notify throttle users", throttle.getLocoAddress() );
953
954            this.throttle = throttle;
955            for (ThrottleListener listener : listeners) {
956                listener.notifyThrottleFound(throttle);
957            }
958            //This handles moving the listeners from the old throttle to the new one
959            LocoAddress la = this.throttle.getLocoAddress();
960            PropertyChangeEvent e = new PropertyChangeEvent(this, "throttleAssignmentChanged", null, la); // NOI18N
961            for (PropertyChangeListener prop : old.getPropertyChangeListeners()) {
962                this.throttle.addPropertyChangeListener(prop);
963                prop.propertyChange(e);
964            }
965        }
966
967        void setRosterEntry(BasicRosterEntry _re) {
968            re = _re;
969        }
970
971        BasicRosterEntry getRosterEntry() {
972            return re;
973        }
974
975        void addListener(ThrottleListener l) {
976            // Check for duplication here
977            if (listeners.contains(l))
978                log.debug("this Addresses listeners already includes listener {}", l);
979            else
980                listeners.add(l);
981        }
982
983        void removeListener(ThrottleListener l) {
984            listeners.remove(l);
985        }
986
987        boolean containsListener(ThrottleListener l) {
988            return listeners.contains(l);
989        }
990    }
991
992    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractThrottleManager.class);
993
994}