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                // setRosterEntry() must run BEFORE notifyThrottleFound() here --
486                // a listener that inspects the roster entry during its own
487                // notifyThrottleFound() (e.g. JsonThrottle.sendStatus()) would
488                // otherwise always see null.
489                if (ads != null && a.get(i).getRosterEntry() != null && throttle.getRosterEntry() == null) {
490                    throttle.setRosterEntry(a.get(i).getRosterEntry());
491                }
492                l.notifyThrottleFound(throttle);
493                addressThrottles.get(addr).incrementUse();
494                addressThrottles.get(addr).addListener(l);
495                updateNumUsers(addr, addressThrottles.get(addr).getUseCount());
496            }
497            throttleListeners.remove(addr);
498        }
499        ArrayList<WaitingThrottle> p = listenerOnly.get(addr);
500        if (p == null) {
501            log.debug("notifyThrottleKnown with zero-length propertyChangeListeners: {}", addr);
502        } else {
503            for (WaitingThrottle waitingThrottle : p) {
504                PropertyChangeListener l = waitingThrottle.getPropertyChangeListener();
505                log.debug("Notify propertyChangeListener");
506                l.propertyChange(new PropertyChangeEvent(this, "throttleAssigned", null, addr));
507                if (ads != null && waitingThrottle.getRosterEntry() != null && throttle.getRosterEntry() == null) {
508                    throttle.setRosterEntry(waitingThrottle.getRosterEntry());
509                }
510                throttle.addPropertyChangeListener(l);
511            }
512            listenerOnly.remove(addr);
513        }
514    }
515
516
517    /**
518     * For when a steal / share decision is needed and the ThrottleListener has delegated
519     * this decision to the ThrottleManager.
520     * <p>
521     * Responds to the question by requesting a Throttle "Steal" by default.
522     * <p>
523     * Can be overridden by hardware types which do not wish the default behaviour to Steal.
524     * <p>
525     * This applies only to those systems where "stealing" or "sharing" applies, such as LocoNet.
526     * @param address The LocoAddress the steal / share question relates to
527     * @param question The Question to be put to the ThrottleListener
528     */
529    protected void makeHardwareDecision(LocoAddress address, ThrottleListener.DecisionType question){
530        responseThrottleDecision(address, null, ThrottleListener.DecisionType.STEAL );
531    }
532
533    /**
534     * When the system-specific ThrottleManager has been unable to create the DCC
535     * throttle because it is already in use and must be "stolen" or "shared" to take control, 
536     * it needs to notify the listener of this situation.
537     * <p>
538     * This applies only to those systems where "stealing" or "sharing" applies, such as LocoNet.
539     *
540     * @param address The LocoAddress the steal / share question relates to
541     * @param question The Question to be put to the ThrottleListener
542     */
543    protected synchronized void notifyDecisionRequest(LocoAddress address, ThrottleListener.DecisionType question) {
544        ArrayList<WaitingThrottle> a = throttleListeners.get(address);
545        if (a == null) {
546            log.debug("Cannot issue question. No throttle listeners registered for address {}", address.getNumber());
547            return;
548        }
549        ThrottleListener l;
550        log.debug("{} listener(s) registered for address {}", a.size(), address.getNumber());
551        for (int i = 0; i < a.size(); i++) { // enhanced for (WaitingThrottle waitingThrottle : a) doesn't work somehow
552            if (a.get(i).canHandleDecisions()) {
553                l = a.get(i).getListener();
554                log.debug("Notifying a throttle listener (address {}) of the steal share situation", address.getNumber());
555                l.notifyDecisionRequired(address, question);
556            } else {
557                log.debug("Passing {} to hardware steal / share decision making", address.getNumber());
558                makeHardwareDecision(address, question);
559            }
560        }
561    }
562
563    /**
564     * Check to see if the Dispatch Button should be enabled or not Default to
565     * true, override if necessary
566     *
567     */
568    @Override
569    public boolean hasDispatchFunction() {
570        return true;
571    }
572
573    /**
574     * What speed modes are supported by this system? value should be xor of
575     * possible modes specifed by the DccThrottle interface
576     */
577    @Override
578    public EnumSet<SpeedStepMode> supportedSpeedModes() {
579        return EnumSet.of(SpeedStepMode.NMRA_DCC_128);
580    }
581    
582    /**
583     * Hardware that uses the Silent Steal preference
584     * will need to override
585     * {@inheritDoc}
586     */
587    @Override
588    public boolean enablePrefSilentStealOption() {
589        return false;
590    }
591    
592    /**
593     * Hardware that uses the Silent Share preference
594     * will need to override
595     * {@inheritDoc}
596     */
597    @Override
598    public boolean enablePrefSilentShareOption() {
599        return false;
600    }
601    
602    /**
603     * {@inheritDoc}
604     */
605    @Override
606    public synchronized void attachListener(LocoAddress la, java.beans.PropertyChangeListener p) {
607        if (addressThrottles.containsKey(la)) {
608            addressThrottles.get(la).getThrottle().addPropertyChangeListener(p);
609            p.propertyChange(new PropertyChangeEvent(this, "throttleAssigned", null, la));
610        } else {
611            if (!listenerOnly.containsKey(la)) {
612                listenerOnly.put(la, new ArrayList<>());
613            }
614
615            // get the corresponding list to check length
616            ArrayList<WaitingThrottle> a = listenerOnly.get(la);
617            a.add(new WaitingThrottle(p, null, false));
618            //Only request that the throttle is set up if it hasn't already been
619            //requested.
620            if ((!throttleListeners.containsKey(la)) && (a.size() == 1)) {
621                requestThrottleSetup(la, false);
622            }
623        }
624    }
625
626    /**
627     * {@inheritDoc}
628     */
629    @Override
630    public synchronized void removeListener(LocoAddress la, java.beans.PropertyChangeListener p) {
631        if (addressThrottles.containsKey(la)) {
632            addressThrottles.get(la).getThrottle().removePropertyChangeListener(p);
633            p.propertyChange(new PropertyChangeEvent(this, "throttleRemoved", la, null));
634            return;
635        }
636        p.propertyChange(new PropertyChangeEvent(this, "throttleNotFoundInRemoval", la, null));
637    }
638
639    /**
640     * {@inheritDoc}
641     */
642    @Override
643    public synchronized boolean addressStillRequired(LocoAddress la) {
644        if (addressThrottles.containsKey(la)) {
645            log.debug("usage count is {}", addressThrottles.get(la).getUseCount());
646            return (addressThrottles.get(la).getUseCount() > 0);
647        }
648        return false;
649    }
650    
651    /**
652     * {@inheritDoc}
653     */
654    @Override
655    public boolean addressStillRequired(int address, boolean isLongAddress) {
656        DccLocoAddress la = new DccLocoAddress(address, isLongAddress);
657        return addressStillRequired(la);
658    }
659
660    /**
661     * {@inheritDoc}
662     */
663    @Override
664    public boolean addressStillRequired(int address) {
665        boolean isLong = true;
666        if (canBeShortAddress(address)) {
667            isLong = false;
668        }
669        return addressStillRequired(address, isLong);
670    }
671
672    /**
673     * {@inheritDoc}
674     */
675    @Override
676    public boolean addressStillRequired(BasicRosterEntry re) {
677        return addressStillRequired(re.getDccLocoAddress());
678    }
679
680    /**
681     * {@inheritDoc}
682     */
683    @Override
684    public void releaseThrottle(DccThrottle t, ThrottleListener l) {
685        log.debug("AbstractThrottleManager.releaseThrottle: {}, {}", t, l);
686        disposeThrottle(t, l);
687    }
688
689    /**
690     * {@inheritDoc}
691     */
692    @Override
693    public boolean disposeThrottle(DccThrottle t, ThrottleListener l) {
694        log.debug("AbstractThrottleManager.disposeThrottle: {}, {}", t, l);
695
696//        if (!active) log.error("Dispose called when not active");  <-- might need to control this in the sub class
697        LocoAddress la = t.getLocoAddress();
698        if (addressReleased(la, l)) {
699            log.debug("Address {} still has active users", t.getLocoAddress());
700            return false;
701        }
702        if (t.getPropertyChangeListeners().length > 0) {
703            log.debug("Throttle {} still has {} active propertyChangeListeners registered to the throttle", t.getLocoAddress(), t.getPropertyChangeListeners().length);
704            return false;
705        }
706        synchronized (this) {
707            if (addressThrottles.containsKey(la)) {
708                addressThrottles.remove(la);
709                log.debug("Loco Address {} removed from the stack ", la);
710            } else {
711                log.debug("Loco Address {} not found in the stack ", la);
712            }
713        }
714        return true;
715    }
716    
717    /**
718     * Throttle can no longer be relied upon, 
719     * potentially from an external forced steal or hardware error.
720     * <p>
721     * Normally, #releaseThrottle should be used to close throttles.
722     * <p>
723     * Removes locoaddress from list to force new throttle requests
724     * to request new sessions where the Command station model
725     * implements a dynamic stack, not a static stack.
726     * 
727     * <p>
728     * Managers still need to advise listeners that the session has 
729     * been cancelled and actually dispose of the throttle
730     * @param la address release
731     */
732    protected void forceDisposeThrottle(LocoAddress la) {
733        log.debug("force dispose address {}", la);
734        if (addressThrottles.containsKey(la)) {
735            addressThrottles.remove(la);
736            log.debug("Loco Address {} removed from the stack ", la);
737        } else {
738            log.debug("Loco Address {} not found in the stack ", la);
739        }
740    }
741
742    /**
743     * {@inheritDoc}
744     */
745    @Override
746    public void dispatchThrottle(DccThrottle t, ThrottleListener l) {
747        releaseThrottle(t, l);
748    }
749
750    /**
751     * {@inheritDoc}
752     */
753    @Override
754    public void dispose() {
755    }
756
757    /**
758     * {@inheritDoc}
759     */
760    @Override
761    public synchronized int getThrottleUsageCount(LocoAddress la) {
762        if (addressThrottles.containsKey( la)) {
763            return addressThrottles.get(la).getUseCount();
764        }
765        return 0;
766    }
767    
768    /**
769     * {@inheritDoc}
770     */
771    @Override
772    public int getThrottleUsageCount(int address, boolean isLongAddress) {
773        DccLocoAddress la = new DccLocoAddress(address, isLongAddress);
774        return getThrottleUsageCount(la);
775    }
776    
777    /**
778     * {@inheritDoc}
779     */
780    @Override
781    public int getThrottleUsageCount(int address) {
782        boolean isLong = true;
783        if (canBeShortAddress(address)) {
784            isLong = false;
785        }
786        return getThrottleUsageCount(address, isLong);
787    }
788    
789    /**
790     * {@inheritDoc}
791     */
792    @Override
793    public int getThrottleUsageCount(BasicRosterEntry re) {
794        return getThrottleUsageCount(re.getDccLocoAddress());
795    }
796
797    /**
798     * Release a Throttle from a ThrottleListener.
799     * @param la address release
800     * @param l listening object
801     * @return True if throttle still has listeners or a positive use count, else False
802     */
803    protected synchronized boolean addressReleased(LocoAddress la, ThrottleListener l) {
804        if (addressThrottles.containsKey(la)) {
805            if (addressThrottles.get(la).containsListener(l)) {
806                log.debug("decrementUse called with listener {}", l);
807                addressThrottles.get(la).decrementUse();
808                addressThrottles.get(la).removeListener(l);
809            } else if (l == null) {
810                log.debug("decrementUse called withOUT listener");
811                /*The release release has been called, but as no listener has
812                 been specified, we can only decrement the use flag*/
813                addressThrottles.get(la).decrementUse();
814            }
815        }
816        if (addressThrottles.containsKey(la)) {
817            if (addressThrottles.get(la).getUseCount() > 0) {
818                updateNumUsers(la, addressThrottles.get(la).getUseCount());
819                log.debug("addressReleased still has at least one listener");
820                return true;
821            }
822        }
823        return false;
824    }
825    
826    /**
827     * The number of users of this throttle has been updated
828     * <p>
829     * Typically used to update dispatch / release availablility
830     * specific implementations can override this function to get updates
831     *
832     * @param la the Loco Address which has been updated
833     * @param numUsers current number of users
834     */
835    protected void updateNumUsers( LocoAddress la, int numUsers ){
836        log.debug("Throttle {} now has {} users", la, numUsers);
837    }
838
839    /**
840     * {@inheritDoc}
841     */
842    @Override
843    public Object getThrottleInfo(LocoAddress la, String item) {
844        DccThrottle t;
845        synchronized (this) {
846            if (addressThrottles.containsKey(la)) {
847                t = addressThrottles.get(la).getThrottle();
848            } else {
849                return null;
850            }
851        }
852        if (item.equals(Throttle.ISFORWARD)) {
853            return t.getIsForward();
854        } else if (item.startsWith("Speed")) {
855            switch (item) {
856                case Throttle.SPEEDSETTING:
857                    return t.getSpeedSetting();
858                case Throttle.SPEEDINCREMENT:
859                    return t.getSpeedIncrement();
860                case Throttle.SPEEDSTEPMODE:
861                    return t.getSpeedStepMode();
862                default: // skip
863            }
864        }
865        for ( int i = 0; i< t.getFunctions().length; i++ ) {
866            if (item.equals(Throttle.getFunctionString(i))) {
867                return t.getFunction(i);
868            }
869        }
870        return null;
871    }
872
873    private boolean _hideStealNotifications = false;
874
875    /**
876     * If not headless, display a session stolen dialogue box with
877     * checkbox to hide notifications for rest of JMRI session
878     *
879     * @param address the LocoAddress of the stolen / cancelled Throttle
880     */
881    protected void showSessionCancelDialogue(LocoAddress address){
882        if ((!java.awt.GraphicsEnvironment.isHeadless()) && (!_hideStealNotifications)){
883            jmri.util.ThreadingUtil.runOnGUI(() -> {
884                javax.swing.JCheckBox checkbox = new javax.swing.JCheckBox(
885                    Bundle.getMessage("HideFurtherAlerts"));
886                Object[] params = {Bundle.getMessage("LocoStolen", address), checkbox};
887                java.awt.event.ActionListener stolenpopupcheckbox = (java.awt.event.ActionEvent evt) ->
888                    this.hideStealNotifications(checkbox.isSelected());
889                checkbox.addActionListener(stolenpopupcheckbox);
890                JmriJOptionPane.showMessageDialogNonModal(null, params, 
891                    Bundle.getMessage("LocoStolen", address),
892                    JmriJOptionPane.WARNING_MESSAGE, null);
893            });
894        }
895    }
896
897    /**
898     * Receive notification from a throttle dialogue
899     * to display steal dialogues for rest of the JMRI instance session.
900     * False by default to show notifications
901     *
902     * @param hide set True to hide notifications, else False.
903     */
904    public void hideStealNotifications(boolean hide){
905        _hideStealNotifications = hide;
906    }
907
908    /**
909     * This subClass keeps track of which loco address have been requested and
910     * by whom. It primarily uses an increment count to keep track of all the
911     * Addresses in use as not all external code will have been refactored over
912     * to use the new disposeThrottle.
913     */
914    protected static class Addresses {
915
916        int useActiveCount = 0;
917        DccThrottle throttle;
918        ArrayList<ThrottleListener> listeners = new ArrayList<>();
919        BasicRosterEntry re = null;
920
921        protected Addresses(DccThrottle throttle) {
922            this.throttle = throttle;
923        }
924
925        void incrementUse() {
926            useActiveCount++;
927            log.debug("{} increased Use Size to {}", throttle.getLocoAddress(), useActiveCount);
928        }
929
930        void decrementUse() {
931            // Do not want to go below 0 on the usage front!
932            if (useActiveCount > 0) {
933                useActiveCount--;
934            }
935            log.debug("{} decreased Use Size to {}", throttle.getLocoAddress(), useActiveCount);
936        }
937
938        int getUseCount() {
939            return useActiveCount;
940        }
941
942        DccThrottle getThrottle() {
943            return throttle;
944        }
945
946        void setThrottle(DccThrottle throttle) {
947            DccThrottle old = this.throttle;
948            this.throttle = throttle;
949            if ((old == null) || (old == throttle)) {
950                return;
951            }
952
953            // As the throttle has changed, we need to inform the listeners.
954            // However if a throttle hasn't used the new code, it will not have been
955            // removed and will get a notification.
956            log.debug("Throttle assigned {} has been changed, need to notify throttle users", throttle.getLocoAddress() );
957
958            this.throttle = throttle;
959            for (ThrottleListener listener : listeners) {
960                listener.notifyThrottleFound(throttle);
961            }
962            //This handles moving the listeners from the old throttle to the new one
963            LocoAddress la = this.throttle.getLocoAddress();
964            PropertyChangeEvent e = new PropertyChangeEvent(this, "throttleAssignmentChanged", null, la); // NOI18N
965            for (PropertyChangeListener prop : old.getPropertyChangeListeners()) {
966                this.throttle.addPropertyChangeListener(prop);
967                prop.propertyChange(e);
968            }
969        }
970
971        void setRosterEntry(BasicRosterEntry _re) {
972            re = _re;
973        }
974
975        BasicRosterEntry getRosterEntry() {
976            return re;
977        }
978
979        void addListener(ThrottleListener l) {
980            // Check for duplication here
981            if (listeners.contains(l))
982                log.debug("this Addresses listeners already includes listener {}", l);
983            else
984                listeners.add(l);
985        }
986
987        void removeListener(ThrottleListener l) {
988            listeners.remove(l);
989        }
990
991        boolean containsListener(ThrottleListener l) {
992            return listeners.contains(l);
993        }
994    }
995
996    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractThrottleManager.class);
997
998}