001package jmri.jmrit.logixng.expressions;
002
003import static jmri.Conditional.*;
004
005import java.util.*;
006
007import javax.annotation.CheckForNull;
008import javax.annotation.Nonnull;
009
010import jmri.InstanceManager;
011import jmri.JmriException;
012import jmri.jmrit.logixng.*;
013
014/**
015 * Evaluates to True if the antecedent evaluates to true
016 * 
017 * @author Daniel Bergqvist Copyright 2018
018 */
019public class Antecedent extends AbstractDigitalExpression implements FemaleSocketListener {
020
021    static final java.util.ResourceBundle rbx = java.util.ResourceBundle.getBundle("jmri.jmrit.conditional.ConditionalBundle");  // NOI18N
022    
023    private String _antecedent = "";
024    private final List<ExpressionEntry> _expressionEntries = new ArrayList<>();
025    private boolean disableCheckForUnconnectedSocket = false;
026    
027    /**
028     * Create a new instance of Antecedent with system name and user name.
029     * @param sys the system name
030     * @param user the user name
031     */
032    public Antecedent(@Nonnull String sys, @CheckForNull String user) {
033        super(sys, user);
034        _expressionEntries
035                .add(new ExpressionEntry(InstanceManager.getDefault(DigitalExpressionManager.class)
036                        .createFemaleSocket(this, this, getNewSocketName())));
037    }
038
039    /**
040     * Create a new instance of Antecedent with system name and user name.
041     * @param sys the system name
042     * @param user the user name
043     * @param expressionSystemNames a list of system names for the expressions
044     * this antecedent uses
045     * @throws BadUserNameException when needed
046     * @throws BadSystemNameException when needed
047     */
048    public Antecedent(@Nonnull String sys, @CheckForNull String user,
049            List<Map.Entry<String, String>> expressionSystemNames)
050            throws BadUserNameException, BadSystemNameException {
051        super(sys, user);
052        setExpressionSystemNames(expressionSystemNames);
053    }
054    
055    @Override
056    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
057        DigitalExpressionManager manager = InstanceManager.getDefault(DigitalExpressionManager.class);
058        String sysName = systemNames.get(getSystemName());
059        String userName = userNames.get(getSystemName());
060        if (sysName == null) sysName = manager.getAutoSystemName();
061        Antecedent copy = new Antecedent(sysName, userName);
062        copy.setComment(getComment());
063        copy.setNumSockets(getChildCount());
064        copy.setAntecedent(_antecedent);
065        return manager.registerExpression(copy).deepCopyChildren(this, systemNames, userNames);
066    }
067
068    private void setExpressionSystemNames(List<Map.Entry<String, String>> systemNames) {
069        if (!_expressionEntries.isEmpty()) {
070            throw new RuntimeException("expression system names cannot be set more than once");
071        }
072        
073        for (Map.Entry<String, String> entry : systemNames) {
074            FemaleDigitalExpressionSocket socket =
075                    InstanceManager.getDefault(DigitalExpressionManager.class)
076                            .createFemaleSocket(this, this, entry.getKey());
077            
078            _expressionEntries.add(new ExpressionEntry(socket, entry.getValue()));
079        }
080    }
081    
082    public String getExpressionSystemName(int index) {
083        return _expressionEntries.get(index)._socketSystemName;
084    }
085
086    /** {@inheritDoc} */
087    @Override
088    public LogixNG_Category getCategory() {
089        return LogixNG_Category.COMMON;
090    }
091    
092    /** {@inheritDoc} */
093    @Override
094    public boolean evaluate() throws JmriException {
095        
096        if (_antecedent.isEmpty()) {
097            return false;
098        }
099        
100        boolean result;
101        
102        char[] ch = _antecedent.toCharArray();
103        int n = 0;
104        for (int j = 0; j < ch.length; j++) {
105            if (ch[j] != ' ') {
106                if (ch[j] == '{' || ch[j] == '[') {
107                    ch[j] = '(';
108                } else if (ch[j] == '}' || ch[j] == ']') {
109                    ch[j] = ')';
110                }
111                ch[n++] = ch[j];
112            }
113        }
114        try {
115            List<ExpressionEntry> list = new ArrayList<>();
116            for (ExpressionEntry e : _expressionEntries) {
117                list.add(e);
118            }
119            DataPair dp = parseCalculate(new String(ch, 0, n), list);
120            result = dp.result;
121        } catch (NumberFormatException | IndexOutOfBoundsException | JmriException nfe) {
122            result = false;
123            log.error("{} parseCalculation error antecedent= {}, ex= {}: {}",
124                getDisplayName(), _antecedent, nfe.getClass().getName(), nfe.getMessage());  // NOI18N
125        }
126        
127        return result;
128    }
129    
130    @Override
131    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
132        return _expressionEntries.get(index)._socket;
133    }
134    
135    @Override
136    public int getChildCount() {
137        return _expressionEntries.size();
138    }
139    
140    public void setChildCount(int count) {
141        List<FemaleSocket> addList = new ArrayList<>();
142        List<FemaleSocket> removeList = new ArrayList<>();
143        
144        // Is there too many children?
145        while (_expressionEntries.size() > count) {
146            int childNo = _expressionEntries.size()-1;
147            FemaleSocket socket = _expressionEntries.get(childNo)._socket;
148            if (socket.isConnected()) {
149                socket.disconnect();
150            }
151            removeList.add(_expressionEntries.get(childNo)._socket);
152            _expressionEntries.remove(childNo);
153        }
154        
155        // Is there not enough children?
156        while (_expressionEntries.size() < count) {
157            FemaleDigitalExpressionSocket socket =
158                    InstanceManager.getDefault(DigitalExpressionManager.class)
159                            .createFemaleSocket(this, this, getNewSocketName());
160            _expressionEntries.add(new ExpressionEntry(socket));
161            addList.add(socket);
162        }
163        firePropertyChange(Base.PROPERTY_CHILD_COUNT, removeList, addList);
164    }
165    
166    @Override
167    public String getShortDescription(Locale locale) {
168        return Bundle.getMessage(locale, "Antecedent_Short");
169    }
170    
171    @Override
172    public String getLongDescription(Locale locale) {
173        if (_antecedent.isEmpty()) {
174            return Bundle.getMessage(locale, "Antecedent_Long_Empty");
175        } else {
176            return Bundle.getMessage(locale, "Antecedent_Long", _antecedent);
177        }
178    }
179
180    public String getAntecedent() {
181        return _antecedent;
182    }
183
184    public final void setAntecedent(String antecedent) throws JmriException {
185//        String result = validateAntecedent(antecedent, _expressionEntries);
186//        if (result != null) System.out.format("DANIEL: Exception: %s%n", result);
187//        if (result != null) throw new IllegalArgumentException(result);
188        _antecedent = antecedent;
189    }
190    
191    // This method ensures that we have enough of children
192    private void setNumSockets(int num) {
193        List<FemaleSocket> addList = new ArrayList<>();
194        
195        // Is there not enough children?
196        while (_expressionEntries.size() < num) {
197            FemaleDigitalExpressionSocket socket =
198                    InstanceManager.getDefault(DigitalExpressionManager.class)
199                            .createFemaleSocket(this, this, getNewSocketName());
200            _expressionEntries.add(new ExpressionEntry(socket));
201            addList.add(socket);
202        }
203        firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, addList);
204    }
205    
206    private void checkFreeSocket() {
207        boolean hasFreeSocket = false;
208        
209        for (ExpressionEntry entry : _expressionEntries) {
210            hasFreeSocket |= !entry._socket.isConnected();
211        }
212        if (!hasFreeSocket) {
213            FemaleDigitalExpressionSocket socket =
214                    InstanceManager.getDefault(DigitalExpressionManager.class)
215                                    .createFemaleSocket(this, this, getNewSocketName());
216            _expressionEntries.add(new ExpressionEntry(socket));
217            
218            List<FemaleSocket> list = new ArrayList<>();
219            list.add(socket);
220            firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, list);
221        }
222    }
223    
224    /** {@inheritDoc} */
225    @Override
226    public boolean isSocketOperationAllowed(int index, FemaleSocketOperation oper) {
227        switch (oper) {
228            case Remove:        // Possible if socket is not connected
229                return ! getChild(index).isConnected();
230            case InsertBefore:
231                return true;    // Always possible
232            case InsertAfter:
233                return true;    // Always possible
234            case MoveUp:
235                return index > 0;   // Possible if not first socket
236            case MoveDown:
237                return index+1 < getChildCount();   // Possible if not last socket
238            default:
239                throw new UnsupportedOperationException("Oper is unknown" + oper.name());
240        }
241    }
242    
243    private void insertNewSocket(int index) {
244        FemaleDigitalExpressionSocket socket =
245                InstanceManager.getDefault(DigitalExpressionManager.class)
246                        .createFemaleSocket(this, this, getNewSocketName());
247        _expressionEntries.add(index, new ExpressionEntry(socket));
248        
249        List<FemaleSocket> addList = new ArrayList<>();
250        addList.add(socket);
251        firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, addList);
252    }
253    
254    private void removeSocket(int index) {
255        List<FemaleSocket> removeList = new ArrayList<>();
256        removeList.add(_expressionEntries.remove(index)._socket);
257        firePropertyChange(Base.PROPERTY_CHILD_COUNT, removeList, null);
258    }
259    
260    private void moveSocketDown(int index) {
261        ExpressionEntry temp = _expressionEntries.get(index);
262        _expressionEntries.set(index, _expressionEntries.get(index+1));
263        _expressionEntries.set(index+1, temp);
264        
265        List<FemaleSocket> list = new ArrayList<>();
266        list.add(_expressionEntries.get(index)._socket);
267        list.add(_expressionEntries.get(index)._socket);
268        firePropertyChange(Base.PROPERTY_CHILD_REORDER, null, list);
269    }
270    
271    /** {@inheritDoc} */
272    @Override
273    public void doSocketOperation(int index, FemaleSocketOperation oper) {
274        switch (oper) {
275            case Remove:
276                if (getChild(index).isConnected()) throw new UnsupportedOperationException("Socket is connected");
277                removeSocket(index);
278                break;
279            case InsertBefore:
280                insertNewSocket(index);
281                break;
282            case InsertAfter:
283                insertNewSocket(index+1);
284                break;
285            case MoveUp:
286                if (index == 0) throw new UnsupportedOperationException("cannot move up first child");
287                moveSocketDown(index-1);
288                break;
289            case MoveDown:
290                if (index+1 == getChildCount()) throw new UnsupportedOperationException("cannot move down last child");
291                moveSocketDown(index);
292                break;
293            default:
294                throw new UnsupportedOperationException("Oper is unknown" + oper.name());
295        }
296    }
297    
298    @Override
299    public void connected(FemaleSocket socket) {
300        if (disableCheckForUnconnectedSocket) return;
301        
302        for (ExpressionEntry entry : _expressionEntries) {
303            if (socket == entry._socket) {
304                entry._socketSystemName =
305                        socket.getConnectedSocket().getSystemName();
306            }
307        }
308        
309        checkFreeSocket();
310    }
311
312    @Override
313    public void disconnected(FemaleSocket socket) {
314        for (ExpressionEntry entry : _expressionEntries) {
315            if (socket == entry._socket) {
316                entry._socketSystemName = null;
317                break;
318            }
319        }
320    }
321
322    /** {@inheritDoc} */
323    @Override
324    public void setup() {
325        // We don't want to check for unconnected sockets while setup sockets
326        disableCheckForUnconnectedSocket = true;
327        
328        for (ExpressionEntry ee : _expressionEntries) {
329            try {
330                if ( !ee._socket.isConnected()
331                        || !ee._socket.getConnectedSocket().getSystemName()
332                                .equals(ee._socketSystemName)) {
333
334                    String socketSystemName = ee._socketSystemName;
335                    ee._socket.disconnect();
336                    if (socketSystemName != null) {
337                        MaleSocket maleSocket =
338                                InstanceManager.getDefault(DigitalExpressionManager.class)
339                                        .getBySystemName(socketSystemName);
340                        if (maleSocket != null) {
341                            ee._socket.connect(maleSocket);
342                            maleSocket.setup();
343                        } else {
344                            log.error("cannot load digital expression {}", socketSystemName);
345                        }
346                    }
347                } else {
348                    ee._socket.getConnectedSocket().setup();
349                }
350            } catch (SocketAlreadyConnectedException ex) {
351                // This shouldn't happen and is a runtime error if it does.
352                throw new RuntimeException("socket is already connected");
353            }
354        }
355        
356        checkFreeSocket();
357        
358        disableCheckForUnconnectedSocket = false;
359    }
360
361
362
363    /**
364     * Check that an antecedent is well formed.
365     *
366     * @param ant the antecedent string description
367     * @param expressionEntryList arraylist of existing ExpressionEntries
368     * @return error message string if not well formed
369     * @throws jmri.JmriException when an exception occurs
370     */
371    public String validateAntecedent(String ant, List<ExpressionEntry> expressionEntryList) throws JmriException {
372        char[] ch = ant.toCharArray();
373        int n = 0;
374        for (int j = 0; j < ch.length; j++) {
375            if (ch[j] != ' ') {
376                if (ch[j] == '{' || ch[j] == '[') {
377                    ch[j] = '(';
378                } else if (ch[j] == '}' || ch[j] == ']') {
379                    ch[j] = ')';
380                }
381                ch[n++] = ch[j];
382            }
383        }
384        int count = 0;
385        for (int j = 0; j < n; j++) {
386            if (ch[j] == '(') {
387                count++;
388            }
389            if (ch[j] == ')') {
390                count--;
391            }
392        }
393        if (count > 0) {
394            return java.text.MessageFormat.format(
395                    rbx.getString("ParseError7"), new Object[]{')'});  // NOI18N
396        }
397        if (count < 0) {
398            return java.text.MessageFormat.format(
399                    rbx.getString("ParseError7"), new Object[]{'('});  // NOI18N
400        }
401        try {
402            DataPair dp = parseCalculate(new String(ch, 0, n), expressionEntryList);
403            if (n != dp.indexCount) {
404                return java.text.MessageFormat.format(
405                        rbx.getString("ParseError4"), new Object[]{ch[dp.indexCount - 1]});  // NOI18N
406            }
407            int index = dp.argsUsed.nextClearBit(0);
408            if (index >= 0 && index < expressionEntryList.size()) {
409//                System.out.format("Daniel: ant: %s%n", ant);
410                return java.text.MessageFormat.format(
411                        rbx.getString("ParseError5"),  // NOI18N
412                        new Object[]{expressionEntryList.size(), index + 1});
413            }
414        } catch (NumberFormatException | IndexOutOfBoundsException | JmriException nfe) {
415            return rbx.getString("ParseError6") + nfe.getMessage();  // NOI18N
416        }
417        return null;
418    }
419
420    /**
421     * Parses and computes one parenthesis level of a boolean statement.
422     * <p>
423     * Recursively calls inner parentheses levels. Note that all logic operators
424     * are detected by the parsing, therefore the internal negation of a
425     * variable is washed.
426     *
427     * @param s            The expression to be parsed
428     * @param expressionEntryList ExpressionEntries for R1, R2, etc
429     * @return a data pair consisting of the truth value of the level a count of
430     *         the indices consumed to parse the level and a bitmap of the
431     *         variable indices used.
432     * @throws jmri.JmriException if unable to compute the logic
433     */
434    DataPair parseCalculate(String s, List<ExpressionEntry> expressionEntryList)
435            throws JmriException {
436
437        // for simplicity, we force the string to upper case before scanning
438        s = s.toUpperCase();
439
440        BitSet argsUsed = new BitSet(expressionEntryList.size());
441        DataPair dp = null;
442        boolean leftArg = false;
443        boolean rightArg = false;
444        int oper = OPERATOR_NONE;
445        int k = -1;
446        int i = 0;      // index of String s
447        //int numArgs = 0;
448        if (s.charAt(i) == '(') {
449            dp = parseCalculate(s.substring(++i), expressionEntryList);
450            leftArg = dp.result;
451            i += dp.indexCount;
452            argsUsed.or(dp.argsUsed);
453        } else // cannot be '('.  must be either leftArg or notleftArg
454        {
455            if (s.charAt(i) == 'R') {  // NOI18N
456                try {
457                    k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
458                    i += 2;
459                } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
460                    k = Integer.parseInt(String.valueOf(s.charAt(++i)));
461                }
462                leftArg = expressionEntryList.get(k - 1)._socket.evaluate();
463                i++;
464                argsUsed.set(k - 1);
465            } else if ("NOT".equals(s.substring(i, i + 3))) {  // NOI18N
466                i += 3;
467
468                // not leftArg
469                if (s.charAt(i) == '(') {
470                    dp = parseCalculate(s.substring(++i), expressionEntryList);
471                    leftArg = dp.result;
472                    i += dp.indexCount;
473                    argsUsed.or(dp.argsUsed);
474                } else if (s.charAt(i) == 'R') {  // NOI18N
475                    try {
476                        k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
477                        i += 2;
478                    } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
479                        k = Integer.parseInt(String.valueOf(s.charAt(++i)));
480                    }
481                    leftArg = expressionEntryList.get(k - 1)._socket.evaluate();
482                    i++;
483                    argsUsed.set(k - 1);
484                } else {
485                    throw new JmriException(java.text.MessageFormat.format(
486                            rbx.getString("ParseError1"), new Object[]{s.substring(i)}));  // NOI18N
487                }
488                leftArg = !leftArg;
489            } else {
490                throw new JmriException(java.text.MessageFormat.format(
491                        rbx.getString("ParseError9"), new Object[]{s}));  // NOI18N
492            }
493        }
494        // crank away to the right until a matching parent is reached
495        while (i < s.length()) {
496            if (s.charAt(i) != ')') {
497                // must be either AND or OR
498                if ("AND".equals(s.substring(i, i + 3))) {  // NOI18N
499                    i += 3;
500                    oper = OPERATOR_AND;
501                } else if ("OR".equals(s.substring(i, i + 2))) {  // NOI18N
502                    i += 2;
503                    oper = OPERATOR_OR;
504                } else {
505                    throw new JmriException(java.text.MessageFormat.format(
506                            rbx.getString("ParseError2"), new Object[]{s.substring(i)}));  // NOI18N
507                }
508                if (s.charAt(i) == '(') {
509                    dp = parseCalculate(s.substring(++i), expressionEntryList);
510                    rightArg = dp.result;
511                    i += dp.indexCount;
512                    argsUsed.or(dp.argsUsed);
513                } else // cannot be '('.  must be either rightArg or notRightArg
514                {
515                    if (s.charAt(i) == 'R') {  // NOI18N
516                        try {
517                            k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
518                            i += 2;
519                        } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
520                            k = Integer.parseInt(String.valueOf(s.charAt(++i)));
521                        }
522                        rightArg = expressionEntryList.get(k - 1)._socket.evaluate();
523                        i++;
524                        argsUsed.set(k - 1);
525                    } else if ("NOT".equals(s.substring(i, i + 3))) {  // NOI18N
526                        i += 3;
527                        // not rightArg
528                        if (s.charAt(i) == '(') {
529                            dp = parseCalculate(s.substring(++i), expressionEntryList);
530                            rightArg = dp.result;
531                            i += dp.indexCount;
532                            argsUsed.or(dp.argsUsed);
533                        } else if (s.charAt(i) == 'R') {  // NOI18N
534                            try {
535                                k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
536                                i += 2;
537                            } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
538                                k = Integer.parseInt(String.valueOf(s.charAt(++i)));
539                            }
540                            rightArg = expressionEntryList.get(k - 1)._socket.evaluate();
541                            i++;
542                            argsUsed.set(k - 1);
543                        } else {
544                            throw new JmriException(java.text.MessageFormat.format(
545                                    rbx.getString("ParseError3"), new Object[]{s.substring(i)}));  // NOI18N
546                        }
547                        rightArg = !rightArg;
548                    } else {
549                        throw new JmriException(java.text.MessageFormat.format(
550                                rbx.getString("ParseError9"), new Object[]{s.substring(i)}));  // NOI18N
551                    }
552                }
553                if (oper == OPERATOR_AND) {
554                    leftArg = (leftArg && rightArg);
555                } else if (oper == OPERATOR_OR) {
556                    leftArg = (leftArg || rightArg);
557                }
558            } else {  // This level done, pop recursion
559                i++;
560                break;
561            }
562        }
563        dp = new DataPair();
564        dp.result = leftArg;
565        dp.indexCount = i;
566        dp.argsUsed = argsUsed;
567        return dp;
568    }
569
570
571    static class DataPair {
572        boolean result = false;
573        int indexCount = 0;         // index reached when parsing completed
574        BitSet argsUsed = null;     // error detection for missing arguments
575    }
576
577    /* This class is public since ExpressionAntecedentXml needs to access it. */
578    public static class ExpressionEntry {
579        private String _socketSystemName;
580        private final FemaleDigitalExpressionSocket _socket;
581        
582        public ExpressionEntry(FemaleDigitalExpressionSocket socket, String socketSystemName) {
583            _socketSystemName = socketSystemName;
584            _socket = socket;
585        }
586        
587        private ExpressionEntry(FemaleDigitalExpressionSocket socket) {
588            this._socket = socket;
589        }
590        
591    }
592
593    /** {@inheritDoc} */
594    @Override
595    public void registerListenersForThisClass() {
596        // Do nothing
597    }
598
599    /** {@inheritDoc} */
600    @Override
601    public void unregisterListenersForThisClass() {
602        // Do nothing
603    }
604    
605    /** {@inheritDoc} */
606    @Override
607    public void disposeMe() {
608    }
609    
610    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Antecedent.class);
611}