001package jmri.jmrix.can.cbus.node;
002
003import java.beans.PropertyChangeListener;
004import java.beans.PropertyChangeEvent;
005import java.io.File;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.List;
009import java.util.TimerTask;
010import javax.annotation.Nonnull;
011import jmri.jmrix.can.*;
012import jmri.jmrix.can.cbus.CbusConstants;
013import jmri.jmrix.can.cbus.CbusMessage;
014import jmri.jmrix.can.cbus.CbusPreferences;
015import jmri.jmrix.can.cbus.CbusSend;
016import jmri.jmrix.can.cbus.swing.nodeconfig.NodeConfigToolPane;
017import jmri.util.*;
018
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021
022/**
023 * Table data model for display of CBUS Nodes
024 *
025 * @author Steve Young (c) 2019
026 * 
027 */
028public class CbusNodeTableDataModel extends CbusBasicNodeTableFetch
029    implements CanListener, PropertyChangeListener, jmri.Disposable {
030
031    private final CbusSend send;
032    private ArrayList<Integer> _nodesFound;
033    private CbusAllocateNodeNumber allocate;
034    protected CbusPreferences preferences;
035
036    public CbusNodeTableDataModel(@Nonnull CanSystemConnectionMemo memo, int row, int column) {
037        this(memo,row);
038    }
039
040    /**
041     * Create a new CbusNodeTableDataModel.
042     * @param memo system connection.
043     * @param initialArraySize initial Array Size.
044     */
045    public CbusNodeTableDataModel(@Nonnull CanSystemConnectionMemo memo, int initialArraySize ) {
046        super(memo, initialArraySize, 0);
047        log.debug("Starting MERG CBUS Node Table memo \"{}\" ",memo);
048        _nodesFound = new ArrayList<>(initialArraySize);
049        // connect to the CanInterface
050        addTc(memo);
051        
052        send = new CbusSend(memo);
053
054        startup();
055    }
056    
057    private void startup(){
058
059        preferences = _memo.get(CbusPreferences.class);
060        if (preferences == null ) {
061            log.error("no prefs");
062            return;
063        }
064        
065        setBackgroundAllocateListener( preferences.getAllocateNNListener() );
066        if ( preferences.getStartupSearchForCs() ) {
067            send.searchForCommandStations();
068        }
069        if ( preferences.getStartupSearchForNodes() ) {
070            send.searchForNodes();
071            setSearchForNodesTimeout( 5000 );
072        } else
073        if ( preferences.getSearchForNodesBackupXmlOnStartup() ) {
074            // it's preferable to do this AFTER the network search timeout, 
075            // however we also test here in case there is no timeout
076            startupSearchNodeXmlFile();
077        }
078    }
079    
080    // start listener for nodes requesting a new node number
081    public void setBackgroundAllocateListener( boolean newState ){
082        if (newState  && !java.awt.GraphicsEnvironment.isHeadless() ) {
083            if (allocate == null) {
084                allocate = new CbusAllocateNodeNumber( _memo, this );
085            } else {
086            }
087        } else {
088            if ( allocate != null ) {
089                allocate.dispose();
090            }
091            allocate = null;
092        }
093    }
094
095    /**
096     * Unused, even simulated nodes / command stations normally respond with CanReply
097     * @param m canmessage
098     */
099    @Override
100    public void message(CanMessage m) { // outgoing cbus message
101    }
102    
103    private int csFound=0;
104    private int ndFound = 0;
105    
106    /**
107     * Listen on the network for incoming STAT and PNN OPC's
108     * @param m incoming CanReply
109     */
110    @Override
111    public void reply(CanReply m) { // incoming cbus message
112        if ( m.extendedOrRtr() ) {
113            return;
114        }
115        int nodenum = ( m.getElement(1) * 256 ) + m.getElement(2);
116        switch (CbusMessage.getOpcode(m)) {
117            case CbusConstants.CBUS_STAT:
118                // log.debug("Command Station Updates Status {}",m);
119                
120                if ( preferences.getAddCommandStations() ) {
121                    
122                    int csnum = m.getElement(3);
123                    // provides a command station by cs number, NOT node number
124                    CbusNode cs = provideCsByNum(csnum,nodenum);
125                    cs.setFW(m.getElement(5),m.getElement(6),m.getElement(7));
126                    cs.setCsFlags(m.getElement(4));
127                    cs.setCanId(CbusMessage.getId(m));
128                    
129                }   _nodesFound.add(nodenum);
130                csFound++;
131                break;
132            case CbusConstants.CBUS_PNN:
133                log.debug("Node Report message {}",m);
134                if ( searchForNodesTask != null && preferences.getAddNodes() ) {
135                    // provides a node by node number
136                    CbusNode nd = provideNodeByNodeNum(nodenum);
137                    nd.setManuModule(m.getElement(3),m.getElement(4));
138                    nd.setNodeFlags(m.getElement(5));
139                    nd.setCanId(CbusMessage.getId(m));
140                }   _nodesFound.add(nodenum);
141                ndFound++;
142                break;
143            case CbusConstants.CBUS_NNREL:
144                // from node advising releasing node number
145                if ( getNodeRowFromNodeNum(nodenum) >-1 ) {
146                    log.info("{} : NNREL",Bundle.getMessage("NdRelease", getNodeName(nodenum), nodenum ) );
147                    removeRow( getNodeRowFromNodeNum(nodenum),false );
148                }
149                break;
150            default:
151                break;
152        }
153    }
154    
155    /** {@inheritDoc} */
156    @Override
157    public void propertyChange(PropertyChangeEvent ev){
158        if (!(ev.getSource() instanceof CbusNode)) {
159            return;
160        }
161        
162        int evRow = getNodeRowFromNodeNum((( CbusNode ) ev.getSource()).getNodeNumber());
163        if (evRow<0){
164            return;
165        }
166        ThreadingUtil.runOnGUIEventually( ()->{
167            switch (ev.getPropertyName()) {
168                case "SINGLENVUPDATE":
169                case "ALLNVUPDATE":
170                    log.debug("Table data model recieves property change row: {}", evRow);
171                    fireTableCellUpdated(evRow, BYTES_REMAINING_COLUMN);
172                    fireTableCellUpdated(evRow, NODE_TOTAL_BYTES_COLUMN);
173                    break;
174                case "ALLEVUPDATE":
175                case "SINGLEEVUPDATE":
176                    fireTableCellUpdated(evRow, NODE_EVENT_INDEX_VALID_COLUMN);
177                    fireTableCellUpdated(evRow, NODE_EVENTS_COLUMN);
178                    fireTableCellUpdated(evRow, BYTES_REMAINING_COLUMN);
179                    fireTableCellUpdated(evRow, NODE_TOTAL_BYTES_COLUMN);
180                    break;
181                case "BACKUPS":
182                    fireTableCellUpdated(evRow, SESSION_BACKUP_STATUS_COLUMN);
183                    fireTableCellUpdated(evRow, NUMBER_BACKUPS_COLUMN);
184                    fireTableCellUpdated(evRow, LAST_BACKUP_COLUMN);
185                    break;
186                case "PARAMETER":
187                    fireTableRowsUpdated(evRow,evRow);
188                    break;
189                case "LEARNMODE":
190                    fireTableCellUpdated(evRow,NODE_IN_LEARN_MODE_COLUMN);
191                    break;
192                case "NAMECHANGE":
193                    fireTableCellUpdated(evRow,NODE_USER_NAME_COLUMN);
194                    break;
195                case "CANID":
196                    fireTableCellUpdated(evRow,CANID_COLUMN);
197                    break;
198                default:
199                    break;
200            }
201        });
202    }
203    
204    private NodeConfigToolPane searchFeedbackPanel;
205    
206    /**
207     * Sends a search for Nodes with timeout
208     * @param panel Feedback pane, can be null
209     * @param timeout in ms
210     */ 
211    public void startASearchForNodes( NodeConfigToolPane panel, int timeout ){
212        searchFeedbackPanel = panel;
213        csFound=0;
214        ndFound=0;
215        setSearchForNodesTimeout( timeout );
216        send.searchForCommandStations();
217        send.searchForNodes();
218    }
219
220    private TimerTask searchForNodesTask;
221    
222    /**
223     * Loop through main table, add a not found note to any nodes
224     * which are on the table but not on this list.
225     */
226    private void checkOnlineNodesVsTable(){
227        log.debug("{} Nodes found, {}",_nodesFound.size(),_nodesFound);
228        for (int i = 0; i < getRowCount(); i++) {
229            if ( ! _nodesFound.contains(_mainArray.get(i).getNodeNumber() )) {
230                log.debug("No network response from Node {}",_mainArray.get(i));
231                _mainArray.get(i).nodeOnNetwork(false);
232            }
233        }
234        // if node heard but flagged as off-network, reset
235        _nodesFound.stream().map((foundNodeNum) -> getNodeByNodeNum(foundNodeNum)).filter((foundNode) 
236                -> ( foundNode != null && foundNode.getNodeBackupManager().getSessionBackupStatus() == CbusNodeConstants.BackupType.NOTONNETWORK )).map((foundNode) -> {
237            foundNode.resetNodeAll();
238            return foundNode;
239        }).forEachOrdered((_item) -> {
240            startBackgroundFetch();
241        });
242    }
243    
244    /**
245     * Clears Node Search Timer
246     */
247    private void clearSearchForNodesTimeout(){
248        if (searchForNodesTask != null ) {
249            searchForNodesTask.cancel();
250            searchForNodesTask = null;
251        }
252    }
253    
254    /**
255     * Starts Search for Nodes Timer
256     * @param timeout value in msec to wait for responses
257     */
258    private void setSearchForNodesTimeout( int timeout) {
259        _nodesFound = new ArrayList<>(5);
260        searchForNodesTask = new TimerTask() {
261            @Override
262            public void run() {
263                // searchForNodesTask = null;
264                // log.info("Node search complete " );
265                if ( searchFeedbackPanel !=null ) {
266                    searchFeedbackPanel.notifyNodeSearchComplete(csFound,ndFound);
267                }
268                
269                // it's preferable to perform this check here, AFTER the network search timeout
270                // as JMRI may be starting up and this is not time sensitive.
271                if ( preferences.getSearchForNodesBackupXmlOnStartup() ) {
272                    startupSearchNodeXmlFile();
273                }
274                
275                checkOnlineNodesVsTable();
276                clearSearchForNodesTimeout();
277            }
278        };
279        TimerUtil.schedule(searchForNodesTask, timeout);
280    }
281    
282    private boolean searchXmlComplete = false;
283    
284    /**
285     * Search the directory for nodes, ie userPref/cbus/123.xml
286     * Add any found to the Node Manager Table
287     * (Modelled after a method in jmri.jmrit.dispatcher.TrainInfoFile )
288     */
289    public void startupSearchNodeXmlFile() {
290        // ensure preferences will be found for read
291        FileUtil.createDirectory(new CbusNodeBackupFile(_memo).getFileLocation());
292        // create an array of file names from node dir in preferences, then loop
293        List<String> names = new ArrayList<>(5);
294        File fp = new File(new CbusNodeBackupFile(_memo).getFileLocation());
295        if (fp.exists()) {
296            String[] fpList = fp.list(new XmlFilenameFilter());
297            if (fpList !=null ) {
298                names.addAll(Arrays.asList(fpList));
299            }
300        }
301        names.forEach((nb) -> {
302            log.debug("Node: {}",nb);
303            int nodeNum =  jmri.util.StringUtil.getFirstIntFromString(nb);
304            CbusNode nd = provideNodeByNodeNum(nodeNum);
305            nd.getNodeBackupManager().doLoad();
306            log.debug("CbusNode {} added to table",nd);
307        });
308        searchXmlComplete = true;
309    }
310    
311    public boolean startupComplete(){
312        return !(!searchXmlComplete && searchForNodesTask != null);
313    }
314    
315    /**
316     * Disconnect from the network
317     * <p>
318     * Close down any background listeners
319     * <p>
320     * Cancel outstanding Timers
321     */
322    @Override
323    public void dispose() {
324        
325        clearSearchForNodesTimeout();
326        if ( trickleFetch != null ) {
327            trickleFetch.dispose();
328            trickleFetch = null;
329        }
330        
331        setBackgroundAllocateListener(false); // stop listening for node number requests
332        
333        removeTc(_memo);
334        
335        for (int i = 0; i < getRowCount(); i++) {
336            _mainArray.get(i).removePropertyChangeListener(this);
337            _mainArray.get(i).dispose();
338        }
339        // _mainArray = null;
340        
341    }
342
343    private final static Logger log = LoggerFactory.getLogger(CbusNodeTableDataModel.class);
344}