001package jmri.jmrit.withrottle;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.List;
006import java.util.concurrent.ConcurrentHashMap;
007
008import org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011/**
012 * Keeps track of what locos are being controlled by a throttle, and passes the
013 * control messages on to them. Creates a new MultiThrottleController for each
014 * loco requested on this throttle. Each loco will then be able to be controlled
015 * individually. '*' is a wildcard loco key. Forwards to all locos on this
016 * MultiThrottle.
017 * <p>
018 * Sample messages:<ul>
019 * <li> {@literal MT+L757<;>L757} On T throttle, add loco L757.
020 * <li> {@literal MT+L1234<;>L1234} On T throttle, add loco L1234.
021 * <li> {@literal MTSL1234<;>L1234} On T throttle, steal loco L1234.
022 * <li> {@literal MTAL757<;>R1} On T throttle, loco L757, set direction to
023 * forward.
024 * <li> {@literal MTAL1234<;>R0} On T throttle, loco L1234, set direction to
025 * reverse.
026 * <li> {@literal MTAL757<;>V42} On T throttle, loco L757, set speed to 42.
027 * <li> {@literal MTAL1234<;>V42} On T throttle, loco L1234, set speed to 42.
028 * <li> {@literal MTA*<;>V16} On T throttle, all locos, set speed to 16.
029 * <li> {@literal MT-L757<;>L757} On T throttle, remove loco L757. (Still has
030 * L1234)
031 * </ul>
032 *
033 * @author Brett Hoffman Copyright (C) 2011
034 */
035public class MultiThrottle {
036
037    private ThrottleControllerListener parentTCL = null;
038    private ControllerInterface parentController = null;
039    char whichThrottle;
040    ConcurrentHashMap<String, MultiThrottleController> throttles;
041
042    public MultiThrottle(char id, ThrottleControllerListener tcl, ControllerInterface ci) {
043        if (log.isDebugEnabled()) {
044            log.debug("Creating new MultiThrottle for id: {}", id);
045        }
046        whichThrottle = id;
047        parentTCL = tcl;
048        parentController = ci;
049    }
050
051    /**
052     * Handle a message sent from the device. A key is used to send an action to
053     * the correct loco. '*' is a wildcard key, sends action to all locos in
054     * this MultiThrottle.
055     *
056     * @param message Consists of a control character, the loco's key, a
057     *                separator {@literal "<;>"}, and the action to forward to
058     *                the MultiThrottleController.
059     */
060    public void handleMessage(String message) {
061        log.debug("MT handleMessage: {}", message);
062        List<String> unit = Arrays.asList(message.substring(1).split("<;>"));
063        String key = unit.get(0);
064        String action = unit.get(1);
065        if ((key == null) || (action == null)) {
066            return;
067        }
068
069        switch (message.charAt(0)) {
070            case 'A':  //  'A'ction
071                passActionsToControllers(key, action);
072                break;
073            case '+':  //  add loco
074                addThrottleController(key, action);
075                break;
076            case '-':  //  remove loco
077                removeThrottleController(key, action);
078                break;
079            case 'S':   //  Steal loco
080                stealThrottleController(key, action);
081                break;
082            default:
083                log.warn("Unhandled code: {}", message.charAt(0));
084                break;
085        }   //  end switch
086
087    }
088
089    private MultiThrottleController createThrottleController(String key) {
090        if (!isValidAddr(key) ) { //make sure address is acceptable before proceeding
091            return null;
092        }
093        if (throttles == null) {
094            throttles = new ConcurrentHashMap<>(1);
095        }
096
097        if (throttles.containsKey(key)) {
098            log.debug("Throttle: {} already in MultiThrottle consist.", key);
099            return null;
100        }
101        MultiThrottleController mtc = new MultiThrottleController(whichThrottle, key, parentTCL, parentController);
102        throttles.put(key, mtc);
103        log.debug("Throttle: {} added to MultiThrottle consist.", key);
104        return mtc;
105    }
106    
107    protected void addThrottleController(String key, String action) {   //  key is address format L#### or S##
108        MultiThrottleController mtc = createThrottleController(key);
109        if (mtc != null) {
110            //  This will request the loco as a DccTrottle
111            mtc.sort(action);
112        }
113    }
114    
115    protected void stealThrottleController(String key, String action) {
116        MultiThrottleController mtc = createThrottleController(key);
117        if (mtc != null) {
118            //  This will request the loco as a DccTrottle
119            mtc.isStealAddress = true;
120            mtc.sort(action);
121        }
122        log.debug("Throttle: {} stolen to MultiThrottle consist.", key);
123    }
124
125    /**
126     * Validate that address is going to be allowed by throttle controller. 
127     *   If not, send an error string to client.
128     *
129     * @param key address to be validated, of form Lnnnn or Snnn
130     */
131    private boolean isValidAddr(String key) {
132        if (key.length() < 2) {
133            String msg = Bundle.getMessage("ErrorInvalidAddressFormat", key);
134            log.warn(msg);
135            parentController.sendAlertMessage(msg);
136            return false;
137        }
138        try {
139            int addr = Integer.parseInt(key.substring(1));
140            if (key.charAt(0) == 'L') {
141                if (jmri.InstanceManager.throttleManagerInstance().canBeLongAddress(addr)) {
142                    return true;
143                } else {
144                    String msg = Bundle.getMessage("ErrorLongAddress", key);
145                    log.warn(msg);
146                    parentController.sendAlertMessage(msg);
147                    return false;
148                }
149            } else if (key.charAt(0) == 'S') {
150                if (jmri.InstanceManager.throttleManagerInstance().canBeShortAddress(addr)) {
151                    return true;
152                } else {
153                    String msg = Bundle.getMessage("ErrorShortAddress", key);
154                    log.warn(msg);
155                    parentController.sendAlertMessage(msg);
156                    return false;
157                }
158            }
159            String msg = Bundle.getMessage("ErrorInvalidAddressFormat", key);
160            parentController.sendAlertMessage(msg);
161            log.warn(msg);
162            return false;
163        } catch (NumberFormatException e) {
164            String msg = Bundle.getMessage("ErrorInvalidAddressFormat", key);
165            parentController.sendAlertMessage(msg);
166            log.warn(msg);
167            return false;
168        }
169    }
170
171    protected boolean removeThrottleController(String key, String action) {
172
173        if (throttles == null) {
174            log.debug("No MultiThrottle to remove {} from.", key);
175            return false;
176        }
177        if (key.equals("*")) {
178            ArrayList<String> throttleKeys = new ArrayList<String>(throttles.keySet());  //copy to avoid concurrentModificationException
179            throttleKeys.forEach((throttle) -> {
180                removeThrottleController(throttle, action);
181                //  Runs each loco through this method individually
182            });
183            return true;
184        }
185        if (!throttles.containsKey(key)) {
186            if (log.isDebugEnabled()) {
187                log.debug("Throttle: {} not in MultiThrottle.", key);
188            }
189            return false;
190        }
191        MultiThrottleController mtc = throttles.get(key);
192        mtc.sort(action);
193        mtc.shutdownThrottle();
194        mtc.removeControllerListener(parentController);
195        mtc.removeThrottleControllerListener(parentTCL);
196        throttles.remove(key);
197        if (log.isDebugEnabled()) {
198            log.debug("Throttle: {} removed from MultiThrottle.", key);
199        }
200        return true;
201    }
202
203    protected void passActionsToControllers(String key, String action) {
204        if (throttles == null) {
205            log.debug("No throttles in MultiThrottle to receive action.");
206            return;
207        }
208        if (log.isDebugEnabled()) {
209            log.debug("MultiThrottle key: {}, action: {}", key, action);
210        }
211
212        if (key.equals("*")) {
213            ArrayList<String> throttleKeys = new ArrayList<String>(throttles.keySet());  //copy to avoid concurrentModificationException
214            throttleKeys.forEach((throttle) -> {
215                passActionsToControllers(throttle, action);
216                //  Runs each loco through this method individually
217            });
218            return;
219        }
220        if (throttles.containsKey(key)) {
221            throttles.get(key).sort(action);
222        }
223    }
224
225    public void dispose() {
226        if (throttles == null) {
227            return;
228        }
229        ArrayList<String> throttleKeys = new ArrayList<String>(throttles.keySet());  //copy to avoid concurrentModificationException
230        throttleKeys.forEach((throttle) -> {
231            removeThrottleController(throttle, "r");
232        });
233    }
234
235    public void eStop() {
236        if (throttles == null) {
237            return;
238        }
239        ArrayList<String> throttleKeys = new ArrayList<String>(throttles.keySet());  //copy to avoid concurrentModificationException
240        throttleKeys.forEach((throttle) -> {
241            passActionsToControllers(throttle, "X");
242        });
243    }
244
245    /**
246     * A request for a this address has been cancelled, clean up the waiting
247     * MultiThrottleController. If the MTC is marked as a steal, this cancel needs 
248     * to not happen.
249     *
250     * @param key The string to use as a key to remove the proper
251     *            MultiThrottleController
252     */
253    public void canceledThrottleRequest(String key) {
254        if (throttles == null) {
255            log.warn("No MultiThrottle to remove {} from.", key);
256            return;
257        }
258        if (!throttles.containsKey(key)) {
259            if (log.isDebugEnabled()) {
260                log.debug("Throttle: {} not in MultiThrottle.", key);
261            }
262            return;
263        }
264        MultiThrottleController mtc = throttles.get(key);
265        if (!mtc.isStealAddress) {
266            mtc.removeControllerListener(parentController);
267            throttles.remove(key);
268            log.debug("Throttle: {} cancelled from MultiThrottle.", key);
269        }
270    }
271
272    private final static Logger log = LoggerFactory.getLogger(MultiThrottle.class);
273}