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