001package jmri.jmrit.z21server;
002
003import java.beans.PropertyChangeListener;
004import java.util.Map;
005import java.util.HashMap;
006import java.util.regex.*;
007
008import jmri.*;
009
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013/**
014 * Handle the mapping from a Z21 turnout number to a JMRI Named Bean. This is not
015 * restricted to just JMRI turnouts, the mapping works for:
016 * - Turnouts
017 * - Lights (thrown = ON, closed = OFF
018 * - Routes (thrown sets the route, closed does nothing)
019 * - Signal Masts and Signal Heads (closed = HELD, thrown = not HELD, the aspect/appearance itself can not be modified)
020 * - Sensors (thrown = ACTIVE, closed = INACTIVE). Used to triggers all actions bound to the sensor
021 * 
022 * If a component should be used by a Z21 turnout number, the number will be stored
023 * in a property of the bean.
024 * 
025 * @author Eckart Meyer Copyright (C) 2025
026 */
027
028public class TurnoutNumberMapHandler implements PropertyChangeListener {
029    
030    private static TurnoutNumberMapHandler instance;
031    public final static String beanProperty = "Z21TurnoutMap";
032
033    // NOTE: This list should match the classes used in NumberMapFrame.java
034    private final static Class<?>[] mgrList = {
035        TurnoutManager.class,
036        RouteManager.class,
037        LightManager.class,
038        SignalMastManager.class,
039        SignalHeadManager.class,
040        SensorManager.class
041    };
042
043    private final Map<Integer, NamedBean> turnoutNumberList = new HashMap<>(); //cache for faster access
044    
045    private TurnoutNumberMapHandler() {
046    }
047    
048    synchronized public static TurnoutNumberMapHandler getInstance() {
049        if (instance == null) {
050            instance = new TurnoutNumberMapHandler();
051            instance.loadNumberList();
052            instance.addPropertyChangeListeners();
053        }
054        return instance;
055    }
056    
057    public static Class<?>[] getManagerClassList() {
058        return mgrList;
059    }
060
061/**
062 * Get the state of a component given by the Z21 Turnout Number. Get the the component from
063 * the hash map. The state is either THROWN, CLOSED or UNKNOWN. For other components than
064 * turnouts, their state will be converted into a turnout state.
065 * 
066 * @param turnoutNumber - the Z21 Turnout Number
067 * @return the current state converted to a turnout state
068 */    
069    public int getStateForNumber(int turnoutNumber) {
070        int state = -1;
071        NamedBean b = turnoutNumberList.get(turnoutNumber);
072        if (b != null) {
073            log.debug("Turnout number {} is {} - {}", turnoutNumber, b.getSystemName(), b.getUserName());
074            log.trace("  class: {}", b.getClass().getName());
075            if (b instanceof Route) {
076                state = Turnout.CLOSED;
077            }
078            else if (b instanceof Light) {
079                Light l = (Light)b;
080                state = (l.getState() == Light.ON) ? Turnout.THROWN : Turnout.CLOSED;
081            }
082            else if (b instanceof SignalMast) {
083                SignalMast s = (SignalMast)b;
084                state = (s.getHeld()) ? Turnout.CLOSED : Turnout.THROWN;
085            }
086            else if (b instanceof SignalHead) {
087                SignalHead s = (SignalHead)b;
088                state = (s.getHeld()) ? Turnout.CLOSED : Turnout.THROWN;
089            }
090            else if (b instanceof Sensor) {
091                Sensor s = (Sensor)b;
092                state = (s.getKnownState() == Sensor.ACTIVE) ? Turnout.THROWN : Turnout.CLOSED;
093            }
094            else {
095                state = b.getState();
096            }
097        }
098        log.debug("state for number {} is {}", turnoutNumber, state);
099        return state;
100    }
101    
102/**
103 * Set the state of a component identified by the mapped number from a turnout state (THROWN or CLOSED)
104 * 
105 * @param turnoutNumber - the Z21 Turnout Number
106 * @param state - a turnout state
107 */
108    public void setStateForNumber(int turnoutNumber, int state) {
109        NamedBean b = turnoutNumberList.get(turnoutNumber);
110        if (b != null) {
111            log.debug("Turnout number {} is {} - {}", turnoutNumber, b.getSystemName(), b.getUserName());
112            if (b instanceof Turnout) {
113                Turnout t = (Turnout)b;
114                t.setCommandedState(state);
115            }
116            else if (b instanceof Route) {
117                Route r = (Route)b;
118                if (state == Turnout.THROWN) {
119                    r.setRoute();
120                }
121            }
122            else if (b instanceof Light) {
123                Light l = (Light)b;
124                l.setState( (state == Turnout.THROWN) ? Light.ON : Light.OFF );
125            }
126            else if (b instanceof SignalMast) {
127                SignalMast s = (SignalMast)b;
128                s.setHeld(state == Turnout.CLOSED);
129            }
130            else if (b instanceof SignalHead) {
131                SignalHead s = (SignalHead)b;
132                s.setHeld(state == Turnout.CLOSED);
133            }
134            else if (b instanceof Sensor) {
135                Sensor s = (Sensor)b;
136                try {
137                    s.setKnownState((state == Turnout.THROWN) ? Sensor.ACTIVE : Sensor.INACTIVE );
138                }
139                catch (JmriException e) {
140                    log.warn("Sensor not set");
141                }
142            }
143        }
144    }
145
146/**
147 * Load our local hash map, so we have a cache
148 */
149    public void loadNumberList() {
150        turnoutNumberList.clear();
151        for (Class<?> clazz : mgrList) {
152            loadNumberTable(clazz);
153        }        
154    }
155    
156/**
157 * Load the mapping information for all components of a given class.
158 * 
159 * @param <T>
160 * @param clazz - the manager class to be used expressed as a classname, e.g. TurnoutManager.class
161 */
162    @SuppressWarnings("unchecked")
163    private <T extends NamedBean> void loadNumberTable(Class<?> clazz) {
164        Pattern pattern = Pattern.compile("^(\\d+)$");
165        Manager<T> mgr = (Manager<T>)InstanceManager.getNullableDefault(clazz);
166        if (mgr != null) {
167            log.trace("mgr: {} {}", mgr, mgr.getClass().getName());
168            for (T t : mgr.getNamedBeanSet()) {
169                Object o = t.getProperty(beanProperty);
170                if (o != null) {
171                    String val = o.toString();
172                    Matcher matcher = pattern.matcher(val);
173                    if (matcher.matches()) {
174                        if (matcher.group(0) != null) {
175                            int num = Integer.parseInt(matcher.group(0)); //mapped turnout number
176                            log.debug("Found number {}: {} - {}", num, t.getSystemName(), t.getUserName());
177                            turnoutNumberList.put(num, t);
178                        }
179                    }
180                }
181            }
182        }
183    }
184    
185/**
186 * Add property change listener to all supported component managers, so we
187 * will be informed, if the tables have changed.
188 */
189    @SuppressWarnings("unchecked")
190    private void addPropertyChangeListeners() {
191        for (Class<?> clazz : mgrList) {
192            Manager<?> mgr = (Manager<?>)InstanceManager.getNullableDefault(clazz);
193            if (mgr != null) {
194                mgr.addPropertyChangeListener(instance);
195            }
196        }
197    }
198
199/**
200 * Remove listener from all managers
201 */    
202    @SuppressWarnings("unchecked")
203    private void removePropertyChangeListeners() {
204        for (Class<?> clazz : mgrList) {
205            Manager<?> mgr = (Manager<?>)InstanceManager.getNullableDefault(clazz);
206            if (mgr != null) {
207                mgr.removePropertyChangeListener(instance);
208            }
209        }
210    }
211    
212/**
213 * on destruction of the instance - would probably never occur...
214 */
215    public void dispose() {
216        removePropertyChangeListeners();
217    }
218    
219/**
220 * Property change listener.
221 * (Re-)loads the cache from all mapped components.
222 * Also called from the UI if the mapping has been changed by the user.
223 * 
224 * @param e is the propery change event
225 */
226    @Override
227    public void propertyChange(java.beans.PropertyChangeEvent e) {
228        log.trace("property changed: {}", e.getPropertyName());
229        loadNumberList(); //reload list
230    }
231
232    
233    private final static Logger log = LoggerFactory.getLogger(TurnoutNumberMapHandler.class);
234
235}