001package jmri.jmrix.can.cbus.node;
002
003import java.util.ArrayList;
004import javax.annotation.CheckForNull;
005import javax.annotation.Nonnull;
006import jmri.jmrix.can.CanSystemConnectionMemo;
007import jmri.util.ThreadingUtil;
008
009/**
010 * Class to represent a Processing of CAN Frames for a CbusNode.
011 *
012 * @author Steve Young Copyright (C) 2019,2020
013 */
014public class CbusNodeEventManager {
015    private final CbusBasicNodeWithManagers _node;
016    private final CanSystemConnectionMemo _memo;
017
018    protected int nextEvInArray;
019    private ArrayList<CbusNodeEvent> _nodeEvents;
020    private boolean _eventIndexValid;
021    private ArrayList<CbusNodeEvent> eventsToTeachArray;
022    private int nextEvVar;
023    protected boolean TEACH_OUTSTANDING_EVS;
024
025    /**
026     * Create a new CbusNodeNVManager
027     *
028     * @param memo System connection
029     * @param node The Node
030     */
031    public CbusNodeEventManager ( CanSystemConnectionMemo memo, CbusBasicNodeWithManagers node ){
032        _node = node;
033        _memo = memo;
034        _nodeEvents = null;
035        _eventIndexValid = false;
036        TEACH_OUTSTANDING_EVS = false;
037    }
038
039    /**
040     * Returns total number of node events,
041     * including those with outstanding event variables.
042     *
043     * @return number of events, -1 if events not set
044     */ 
045    public int getTotalNodeEvents(){
046        if (_nodeEvents == null) {
047            return -1;
048        }
049        return _nodeEvents.size();
050    }
051
052    /**
053     * Returns number of fully loaded events, ie no outstanding event variables.
054     *
055     * @return number of loaded events, -1 if events not set
056     */    
057    public int getLoadedNodeEvents(){
058        if (_nodeEvents == null) {
059            return -1;
060        }
061        int count = 0;
062        for (int i = 0; i < _nodeEvents.size(); i++) {
063            if ( ( _nodeEvents.get(i).getNn() != -1 ) && ( _nodeEvents.get(i).getEn() != -1 )) {
064                count ++;
065            }
066        }
067        return count;
068    }
069
070    /**
071     * Returns outstanding events from initial event fetch.
072     *
073     * @return number of outstanding index events
074     */
075    public int getOutstandingIndexNodeEvents(){
076        return getTotalNodeEvents() - getLoadedNodeEvents();
077    }
078
079    /**
080     * Add an event to the node, will not overwrite an existing event.
081     * Resets Event Index as Invalid for All Node Events
082     *
083     * @param newEvent the new event to be added
084     */
085    public void addNewEvent( @Nonnull CbusNodeEvent newEvent ) {
086        if (_nodeEvents == null) {
087            _nodeEvents = new ArrayList<>();
088        }
089        _nodeEvents.add(newEvent);
090        
091        setEvIndexValid(false); // Also produces Property Change Event
092    }
093
094    /**
095     * Remove an event from the CbusNode, does not update hardware.
096     *
097     * @param nn the event Node Number
098     * @param en the event Event Number
099     */
100    public void removeEvent(int nn, int en){
101        _nodeEvents.remove(getNodeEvent(nn, en));
102        setEvIndexValid(false);  // Also produces Property Change Event
103    }
104
105    /**
106     * Get a Node event from its Event and Node number combination
107     *
108     * @param en the Event event number
109     * @param nn the Event node number
110     * @return the node event else null if no Event / Node number combination.
111     */
112    @CheckForNull
113    public CbusNodeEvent getNodeEvent(int nn, int en) {
114        if (_nodeEvents==null){
115            return null;
116        }
117        for (int i = 0; i < _nodeEvents.size(); i++) {
118            if ( ( _nodeEvents.get(i).getNn() == nn ) && ( _nodeEvents.get(i).getEn() == en )) {
119                return _nodeEvents.get(i);
120            }
121        }
122        return null;
123    }
124
125    /**
126     * Provide a Node event from its Event and Node number combination
127     * <p>
128     * If an event for this number pair does not already exist on the node
129     * one will be created, else the existing will be returned.
130     * <p>
131     * Adds any new CbusNodeEvent to the node event array, 
132     * which will also be created if it doesn't exist.
133     *
134     * @param en the Event event number
135     * @param nn the Event node number
136     * @return the node event
137     */
138    @Nonnull
139    public CbusNodeEvent provideNodeEvent(int nn, int en) {
140        if (_nodeEvents == null) {
141            _nodeEvents = new ArrayList<>();
142        }
143        CbusNodeEvent newev = getNodeEvent(nn,en);
144        if ( newev ==null){
145            newev = new CbusNodeEvent(_memo,nn, en, _node.getNodeNumber(),
146                -1, _node.getNodeParamManager().getParameter(5) );
147            addNewEvent(newev);
148        }
149        setEvIndexValid(false); // Also produces Property Change Event
150        return newev;
151    }
152
153    /**
154     * Update node with new Node Event.
155     * 
156     * @param nn Node Number
157     * @param en Event Number
158     * @param evvarindex Event Variable Index
159     * @param evvarval Event Variable Value
160     */
161    protected void updateNodeFromLearn(int nn, int en, int evvarindex, int evvarval ){
162        CbusNodeEvent nodeEv = provideNodeEvent( nn , en );
163        nodeEv.setEvVar( evvarindex , evvarval );
164        _node.notifyPropertyChangeListener("ALLEVUPDATE",null,null);
165        
166    }
167
168    /**
169     * Get a Node event from its Index Field
170     * <p>
171     * This is NOT the node array index.
172     *
173     * @param index the Node event index, as set by a node from a NERD request
174     * @return the node event, else null if the index is not located
175     */
176    @CheckForNull
177    public CbusNodeEvent getNodeEventByIndex(int index) {
178        
179        if ( _nodeEvents == null ){
180            return null;
181        }
182        for (int i = 0; i < _nodeEvents.size(); i++) {
183            if ( _nodeEvents.get(i).getIndex() == index ) {
184                return _nodeEvents.get(i);
185            }
186        }
187        return null;
188    }
189
190    /**
191     * Get the Node event Array index from its Index Field
192     *
193     * @param index the Node event index, as set by a node from a NERD request
194     * @return the array index, else -1 if event index number not found in array
195     */
196    protected int getEventRowFromIndex(int index ){
197        for (int i = 0; i < _nodeEvents.size(); i++) {
198            if ( _nodeEvents.get(i).getIndex() == index ) {
199                return i;
200            }
201        }
202        return -1;
203    }
204
205    /**
206     * Get the Node event by ArrayList Index.
207     *
208     * @param index the index of the CbusNodeEvent within the ArrayList
209     * @return the Node Event
210     */
211    @CheckForNull
212    public CbusNodeEvent getNodeEventByArrayID(int index) {
213        return _nodeEvents.get(index);
214    }
215
216    /**
217     * Get the Node event ArrayList
218     *
219     * @return the list of Node Events
220     */
221    @CheckForNull
222    public ArrayList<CbusNodeEvent> getEventArray(){
223        return _nodeEvents;
224    }
225
226    /**
227     * Get the Number of Outstanding Event Variables
228     * <p>
229     * Sometimes, the Event Variables have to be initialised with an unknown
230     * status, this returns a count of unknown Event Variables for the whole Node.
231     *
232     * @return -1 if main node events array null, else number Outstanding Ev Vars
233     */
234    public int getOutstandingEvVars(){
235        int count = 0;
236        ArrayList<CbusNodeEvent> evs = getEventArray();
237        if (  evs == null ){
238            return -1;
239        }
240        for (int i = 0; i < evs.size(); i++) {
241            count += evs.get(i).getOutstandingVars();
242        }
243        return count;
244    }
245
246    /**
247     * The last message from the node CMDERR5 indicates that all remaining event variables
248     * for a particular event are not required.
249     * This sets the remaining ev vars to 0 so are not requested
250     */
251    protected void remainingEvVarsNotNeeded(){
252        ArrayList<CbusNodeEvent> evs = getEventArray();
253        if (evs!=null) {
254            for (int i = 0; i < evs.size(); i++) {
255                if ( evs.get(i).getNextOutstanding() > 0 ) {
256                    evs.get(i).allOutstandingEvVarsNotNeeded();
257
258                    // cancel Timer
259                    _node.getNodeTimerManager().clearNextEvVarTimeout();
260                    // update GUI
261                    _node.notifyPropertyChangeListener("SINGLEEVUPDATE",null,i);
262                    return;
263                }
264            }
265        }
266    }
267
268    /**
269     * Send a request for the next unknown Event Variable to the Physical Node
270     * <p>
271     * If events are unknown, sends the NERD request and starts that timer,
272     * else requests the next Ev Var with unknown status ( represented as int value -1 )
273     * Will NOT send if any Node is in Learn Mode or if there are any outstanding requests from the Node.
274     */
275    protected void sendNextEvVarToFetch() {
276        ArrayList<CbusNodeEvent> _evs = getEventArray();
277        // do not request if node is learn mode
278        if (( _node.getTableModel().getAnyNodeInLearnMode() > -1 )
279        || ( _evs == null )
280        || ( _node.getNodeTimerManager().hasActiveTimers()) ){
281            return;
282        }
283        
284        // if events on module, get their event, node and node index
285        // *** This could produce up to 255 responses per node ***
286        if ( ( getTotalNodeEvents() > 0 ) && getOutstandingIndexNodeEvents()>0 ) {
287            _node.send.nERD( _node.getNodeNumber() );
288            // starts timeout 
289            _node.getNodeTimerManager().setAllEvTimeout();
290            return;
291        }
292        
293        for (int i = 0; i < _evs.size(); i++) {
294            if ( _evs.get(i).getOutstandingVars() > 0 ) {
295                int index = _evs.get(i).getIndex();
296                int nextevvar = _evs.get(i).getNextOutstanding();
297                
298                // index from NERD / ENRSP indexing may start at 0
299                if ( index > -1 ) {
300                
301                    // start timer
302                    _node.getNodeTimerManager().setNextEvVarTimeout( nextevvar,_evs.get(i).toString() );
303                    _node.send.rEVAL( _node.getNodeNumber(), index, nextevvar );
304                    return;
305                }
306                else { // if index < 0 event index is invalid so attempt refetch.
307                    // reset events
308                    log.info("Invalid index, resetting events for node {}", _node );
309                    _nodeEvents = null;
310                    return;
311                }
312            }
313        }
314    }
315
316    /**
317     * Used in CBUS_NEVAL response from Node.
318     * Set the value of an event variable by event Index and event Variable Index
319     * @param eventIndex Event Index
320     * @param eventVarIndex Event Variable Index
321     * @param newVal New Value
322     */
323    protected void setEvVarByIndex(int eventIndex, int eventVarIndex, int newVal) {
324        CbusNodeEvent nodeEvByIndex = getNodeEventByIndex(eventIndex);
325        if ( nodeEvByIndex != null ) {
326            nodeEvByIndex.setEvVar(eventVarIndex,newVal);
327            _node.notifyPropertyChangeListener("SINGLEEVUPDATE",null,getEventRowFromIndex(eventIndex));
328        }
329    }
330
331    /**
332     * Used to process a CBUS_ENRSP response from node
333     *
334     * If existing index known, use that slot in the event array,
335     * else if event array has empty slot for that index, use that slot.
336     * @param nn Node Number
337     * @param en Event Number
338     * @param index Index Number
339     */
340    protected void setNextEmptyNodeEvent(int nn, int en, int index){
341        ArrayList<CbusNodeEvent> _evs = getEventArray();
342        if ( _evs == null ){
343            log.error("Indexed events are not expected as total number of events unknown");
344            return;
345        } else {
346            for (int i = 0; i < _evs.size(); i++) {
347                if ( _evs.get(i).getIndex() == index ) {
348                    _evs.get(i).setNn(nn);
349                    _evs.get(i).setEn(en);
350                    _node.notifyPropertyChangeListener("SINGLEEVUPDATE",null,i);
351                    return;
352                }
353            }
354        }
355        
356        for (int i = 0; i < _nodeEvents.size(); i++) {
357            if ( ( _nodeEvents.get(i).getNn() == -1 ) && ( _nodeEvents.get(i).getEn() == -1 ) ) {
358                _nodeEvents.get(i).setNn(nn);
359                _nodeEvents.get(i).setEn(en);
360                _nodeEvents.get(i).setIndex(index);
361                
362                _node.notifyPropertyChangeListener("SINGLEEVUPDATE",null,i);
363                return;
364            }
365        }
366        log.error("Issue setting node event, index {} not valid",index);
367        _nodeEvents = null;
368    }
369
370    /**
371     * Get if the Node event index is valid.
372     * @return true if event index is valid, else false if invalid or no events on node.
373     */
374    protected boolean isEventIndexValid(){
375        return _eventIndexValid;
376    }
377
378    /**
379     * Set the Node event index flag as valid or invalid.
380     * <p>
381     * Resets all Node Event Indexes to -1 if invalid.
382     * @param newval true if Event Index Valid, else false
383     */
384    protected void setEvIndexValid( boolean newval ) {
385        _eventIndexValid = newval;
386        if (!newval){ // event index no longer valid so clear values in individual events
387            for (int i = 0; i < _nodeEvents.size(); i++) {
388                _nodeEvents.get(i).setIndex(-1);
389            }
390        }
391        _node.notifyPropertyChangeListener("ALLEVUPDATE",null,null);
392    }
393
394    /**
395     * Send and teach updated Events to this node
396     *
397     * @param evArray array of CbusNodeEvents to be taught
398     */
399    public void sendNewEvSToNode( @Nonnull ArrayList<CbusNodeEvent> evArray ){
400        eventsToTeachArray = evArray;
401        
402        if (eventsToTeachArray==null){
403            _node.getNodeTimerManager().sendEvErrorCount=1;
404            teachEventsComplete();
405            return;
406        }
407        
408        if (eventsToTeachArray.isEmpty()){
409            teachEventsComplete();
410            return;
411        }
412        
413        // check other nodes in learn mode
414        if ( _node.getTableModel().getAnyNodeInLearnMode() > -1 ) {
415            String err = "Cancelling teach event.  Node " + _node.getTableModel().getAnyNodeInLearnMode() + " is already in Learn Mode";
416            log.warn("Unable to teach: {}",err);
417            _node.notifyPropertyChangeListener("ADDEVCOMPLETE", null, err);
418            return;
419        }
420        
421        TEACH_OUTSTANDING_EVS = true;
422        nextEvInArray = 0;
423        nextEvVar = 1; // start at 1 as 0 is used for total ev vars
424        _node.getNodeTimerManager().sendEvErrorCount = 0;
425        
426        _node.send.nodeEnterLearnEvMode( _node.getNodeNumber() ); // no response expected but we add a mini delay for other traffic
427        
428        log.debug("sendNewEvSToNode {}",evArray);
429        
430        ThreadingUtil.runOnLayoutDelayed( () -> {
431            teachNewEvLoop();
432        }, 50 );
433        
434    }
435
436    /**
437     * Send a message to delete an event stored on this node
438     *
439     * @param nn event node number
440     * @param en event event number
441     */
442    public void deleteEvOnNode( int nn, int en){
443        
444        // check other nodes in learn mode
445        if ( _node.getTableModel().getAnyNodeInLearnMode() > -1 ) {
446            String err = "Cancelling delete event.  Node " + _node.getTableModel().getAnyNodeInLearnMode() + " is already in Learn Mode";
447            log.warn("Unable to teach: {}",err);
448            _node.notifyPropertyChangeListener("DELETEEVCOMPLETE", null, err);
449            return;
450        }
451        
452        _node.send.nodeEnterLearnEvMode( _node.getNodeNumber() ); 
453        // no response expected but we add a mini delay for other traffic
454        ThreadingUtil.runOnLayoutDelayed( () -> {
455            _node.send.nodeUnlearnEvent( nn, en );
456            setEvIndexValid(false);
457        }, 50 );
458        log.info("Deleted Event {} on Node {}",
459            new jmri.jmrix.can.cbus.CbusEvent(nn,en),_node.getNodeNumber());
460        ThreadingUtil.runOnGUIDelayed( () -> {
461            _node.send.nodeExitLearnEvMode( _node.getNodeNumber() );
462            // notify ui
463            _node.notifyPropertyChangeListener("DELETEEVCOMPLETE", null, "");
464        }, 100 );
465    }
466
467    private void teachEventsComplete(){
468    
469        TEACH_OUTSTANDING_EVS = false;
470            _node.send.nodeExitLearnEvMode( _node.getNodeNumber() );
471            String err;
472            if ( _node.getNodeTimerManager().sendEvErrorCount==0 ) {
473                log.info("Completed Event Write with No errors, node {}.", _node );
474                err = "";
475            }
476            else {
477                err = "Event Write Failed with "+ _node.getNodeTimerManager().sendEvErrorCount +" errors.";
478                log.error("{} Node {}.", err, _node);
479                
480            }
481            // notify ui's
482            ThreadingUtil.runOnGUIDelayed( () -> {
483                _node.notifyPropertyChangeListener("ADDEVCOMPLETE", null, err);
484                 _node.notifyPropertyChangeListener("ADDALLEVCOMPLETE", null, _node.getNodeTimerManager().sendEvErrorCount);
485                _node.getNodeTimerManager().sendEvErrorCount=0;
486            },50 );
487    }
488
489    /**
490     * Loop for event teaching, triggered from various places
491     */
492    protected void teachNewEvLoop(){
493        
494        if ( nextEvVar > _node.getNodeParamManager().getParameter(5) ) {
495            nextEvVar = 1;
496            nextEvInArray++;
497        }
498        
499        if ( nextEvInArray >= eventsToTeachArray.size() ) {
500            log.debug("all done");
501            teachEventsComplete();
502            return;
503        }
504
505        CbusNodeEvent wholeEvent = eventsToTeachArray.get(nextEvInArray);
506        log.debug("event variable array length: {}", wholeEvent.getEvVarArray().length);
507        log.debug("Number of event vars from node parameters: {}",_node.getNodeParamManager().getParameter(CbusNodeConstants.EV_PER_EN_IDX) );
508        if (wholeEvent.getEvVarArray().length < nextEvVar ){
509            log.warn("Incorrect number of event variables ({}) for {}",
510                wholeEvent.getEvVarArray().length ,wholeEvent );
511            _node.getNodeTimerManager().sendEvErrorCount++;
512            nextEvVar = 1;
513            nextEvInArray++;
514            teachNewEvLoop();
515            return;
516        }
517
518        log.debug("teach event var {}  val {} ",nextEvVar,wholeEvent.getEvVar(nextEvVar));
519        CbusNodeEvent existingEvent = getNodeEvent( wholeEvent.getNn(), wholeEvent.getEn() );
520        
521        log.debug("teach event {} with existing event {}",wholeEvent,existingEvent);
522        
523        // increment and restart loop if no change
524        if ((existingEvent!=null) && (existingEvent.getEvVar(nextEvVar)==wholeEvent.getEvVar(nextEvVar))){
525            nextEvVar++;
526            teachNewEvLoop();
527            return;
528        }
529        
530        // start timeout , send message, increment for next run, only sent when WRACK received
531        _node.getNodeTimerManager().setsendEditEvTimeout();
532        _node.send.nodeTeachEventLearnMode(wholeEvent.getNn(),wholeEvent.getEn(),nextEvVar,wholeEvent.getEvVar(nextEvVar));
533        nextEvVar++;
534    }
535
536    /**
537     * Resets Node Events with null array.
538     * For when a CbusNode is reset to unknown events.
539     */
540    public void resetNodeEvents() {
541        _nodeEvents = null;
542        _node.notifyPropertyChangeListener("ALLEVUPDATE",null,null);
543    }
544
545    /**
546     * Resets Node Events with zero length array.
547     * For when a CbusNode is reset to 0 events
548     *
549     */
550    public void resetNodeEventsToZero() {
551        _nodeEvents = null;
552        _nodeEvents = new ArrayList<>();
553        _node.notifyPropertyChangeListener("ALLEVUPDATE",null,null);
554    }
555
556    /**
557     * the next event index for a CbusDummyNode NODE to allocate, 
558     * NOT a software tool.
559     * @return next available event index
560     */
561    public int getNextFreeIndex(){
562        int newIndex = 1;
563        for (int i = 0; i < getTotalNodeEvents(); i++) {
564            CbusNodeEvent a = getNodeEventByArrayID(i);
565            if ( (a!=null) && newIndex <= a.getIndex() ) {
566                newIndex = a.getIndex()+1;
567            }
568        }
569        log.debug("dummy node sets index {}",newIndex);
570        return newIndex;
571    }
572
573    /**
574     * @return descriptive string
575     */
576    @Override
577    public String toString() {
578        return "Node Events";
579    }
580
581    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusNodeEventManager.class);
582
583}