001package jmri.jmrit.logixng.expressions;
002
003import java.util.List;
004import java.util.ArrayList;
005import java.util.HashMap;
006import java.util.Locale;
007import java.util.Map;
008
009import javax.annotation.Nonnull;
010import javax.annotation.CheckForNull;
011
012import jmri.InstanceManager;
013import jmri.JmriException;
014import jmri.Manager;
015import jmri.NamedBean;
016import jmri.NamedBeanUsageReport;
017import jmri.jmrit.logixng.*;
018import jmri.jmrit.logixng.implementation.DefaultFemaleGenericExpressionSocket;
019import jmri.jmrit.logixng.util.parser.ParserException;
020import jmri.jmrit.logixng.util.parser.RecursiveDescentParser;
021import jmri.jmrit.logixng.util.parser.Variable;
022import jmri.jmrit.logixng.util.parser.GenericExpressionVariable;
023import jmri.jmrit.logixng.util.parser.ExpressionNode;
024import jmri.util.TypeConversionUtil;
025
026/**
027 * Evaluates to True if the formula evaluates to true
028 *
029 * @author Daniel Bergqvist Copyright 2019
030 */
031public class AnalogFormula extends AbstractAnalogExpression implements FemaleSocketListener {
032
033    private String _formula = "";
034    private ExpressionNode _expressionNode;
035    private final List<ExpressionEntry> _expressionEntries = new ArrayList<>();
036    private boolean _disableCheckForUnconnectedSocket = false;
037
038    /**
039     * Create a new instance of Formula with system name and user name.
040     * @param sys the system name
041     * @param user the user name
042     */
043    public AnalogFormula(@Nonnull String sys, @CheckForNull String user) {
044        super(sys, user);
045        _expressionEntries
046                .add(new ExpressionEntry(createFemaleSocket(this, this, getNewSocketName())));
047    }
048
049    /**
050     * Create a new instance of Formula with system name and user name.
051     * @param sys the system name
052     * @param user the user name
053     * @param expressionSystemNames a list of system names for the expressions
054     * this formula uses
055     */
056    public AnalogFormula(@Nonnull String sys, @CheckForNull String user,
057            List<SocketData> expressionSystemNames) {
058        super(sys, user);
059        setExpressionSystemNames(expressionSystemNames);
060    }
061
062    @Override
063    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
064        AnalogExpressionManager manager = InstanceManager.getDefault(AnalogExpressionManager.class);
065        String sysName = systemNames.get(getSystemName());
066        String userName = userNames.get(getSystemName());
067        if (sysName == null) sysName = manager.getAutoSystemName();
068        AnalogFormula copy = new AnalogFormula(sysName, userName);
069        copy.setComment(getComment());
070        copy.setNumSockets(getChildCount());
071        copy.setFormula(_formula);
072        return manager.registerExpression(copy).deepCopyChildren(this, systemNames, userNames);
073    }
074
075    private void setExpressionSystemNames(List<SocketData> systemNames) {
076        if (!_expressionEntries.isEmpty()) {
077            throw new RuntimeException("expression system names cannot be set more than once");
078        }
079
080        for (SocketData socketData : systemNames) {
081            FemaleGenericExpressionSocket socket =
082                    createFemaleSocket(this, this, socketData._socketName);
083//            FemaleGenericExpressionSocket socket =
084//                    InstanceManager.getDefault(AnalogExpressionManager.class)
085//                            .createFemaleSocket(this, this, entry.getKey());
086
087            _expressionEntries.add(new ExpressionEntry(socket, socketData._socketSystemName, socketData._manager));
088        }
089    }
090
091    public String getExpressionSystemName(int index) {
092        return _expressionEntries.get(index)._socketSystemName;
093    }
094
095    public String getExpressionManager(int index) {
096        return _expressionEntries.get(index)._manager;
097    }
098
099    private FemaleGenericExpressionSocket createFemaleSocket(
100            Base parent, FemaleSocketListener listener, String socketName) {
101
102        return new DefaultFemaleGenericExpressionSocket(
103                FemaleGenericExpressionSocket.SocketType.GENERIC, parent, listener, socketName);
104    }
105
106    public final void setFormula(String formula) throws ParserException {
107        Map<String, Variable> variables = new HashMap<>();
108        RecursiveDescentParser parser = new RecursiveDescentParser(variables);
109        for (int i=0; i < getChildCount(); i++) {
110            Variable v = new GenericExpressionVariable((FemaleGenericExpressionSocket)getChild(i));
111            variables.put(v.getName(), v);
112        }
113        _expressionNode = parser.parseExpression(formula);
114        // parseExpression() may throw an exception and we don't want to set
115        // the field _formula until we now parseExpression() has succeeded.
116        _formula = formula;
117    }
118
119    public String getFormula() {
120        return _formula;
121    }
122
123    private void parseFormula() {
124        try {
125            setFormula(_formula);
126        } catch (ParserException e) {
127            log.error("Unexpected exception when parsing the formula", e);
128        }
129    }
130
131    /** {@inheritDoc} */
132    @Override
133    public Category getCategory() {
134        return Category.COMMON;
135    }
136
137    /** {@inheritDoc} */
138    @Override
139    public double evaluate() throws JmriException {
140
141        if (_formula.isEmpty()) {
142            return 0.0;
143        }
144
145        return TypeConversionUtil.convertToDouble(_expressionNode.calculate(
146                getConditionalNG().getSymbolTable()), false);
147    }
148
149    @Override
150    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
151        return _expressionEntries.get(index)._socket;
152    }
153
154    @Override
155    public int getChildCount() {
156        return _expressionEntries.size();
157    }
158
159    public void setChildCount(int count) {
160        List<FemaleSocket> addList = new ArrayList<>();
161        List<FemaleSocket> removeList = new ArrayList<>();
162
163        // Is there too many children?
164        while (_expressionEntries.size() > count) {
165            int childNo = _expressionEntries.size()-1;
166            FemaleSocket socket = _expressionEntries.get(childNo)._socket;
167            if (socket.isConnected()) {
168                socket.disconnect();
169            }
170            removeList.add(_expressionEntries.get(childNo)._socket);
171            _expressionEntries.remove(childNo);
172        }
173
174        // Is there not enough children?
175        while (_expressionEntries.size() < count) {
176            FemaleGenericExpressionSocket socket =
177                    createFemaleSocket(this, this, getNewSocketName());
178            _expressionEntries.add(new ExpressionEntry(socket));
179            addList.add(socket);
180        }
181        parseFormula();
182        firePropertyChange(Base.PROPERTY_CHILD_COUNT, removeList, addList);
183    }
184
185    @Override
186    public String getShortDescription(Locale locale) {
187        return Bundle.getMessage(locale, "AnalogFormula_Short");
188    }
189
190    @Override
191    public String getLongDescription(Locale locale) {
192        if (_formula.isEmpty()) {
193            return Bundle.getMessage(locale, "AnalogFormula_Long_Empty");
194        } else {
195            return Bundle.getMessage(locale, "AnalogFormula_Long", _formula);
196        }
197    }
198
199    // This method ensures that we have enough of children
200    private void setNumSockets(int num) {
201        List<FemaleSocket> addList = new ArrayList<>();
202
203        // Is there not enough children?
204        while (_expressionEntries.size() < num) {
205            FemaleGenericExpressionSocket socket =
206                    createFemaleSocket(this, this, getNewSocketName());
207            _expressionEntries.add(new ExpressionEntry(socket));
208            addList.add(socket);
209        }
210        parseFormula();
211        firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, addList);
212    }
213
214    private void checkFreeSocket() {
215        boolean hasFreeSocket = false;
216
217        for (ExpressionEntry entry : _expressionEntries) {
218            hasFreeSocket |= !entry._socket.isConnected();
219        }
220        if (!hasFreeSocket) {
221            FemaleGenericExpressionSocket socket =
222                    createFemaleSocket(this, this, getNewSocketName());
223            _expressionEntries.add(new ExpressionEntry(socket));
224
225            List<FemaleSocket> list = new ArrayList<>();
226            list.add(socket);
227            parseFormula();
228            firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, list);
229        }
230    }
231
232    /** {@inheritDoc} */
233    @Override
234    public boolean isSocketOperationAllowed(int index, FemaleSocketOperation oper) {
235        switch (oper) {
236            case Remove:        // Possible if socket is not connected
237                return ! getChild(index).isConnected();
238            case InsertBefore:
239                return true;    // Always possible
240            case InsertAfter:
241                return true;    // Always possible
242            case MoveUp:
243                return index > 0;   // Possible if not first socket
244            case MoveDown:
245                return index+1 < getChildCount();   // Possible if not last socket
246            default:
247                throw new UnsupportedOperationException("Oper is unknown" + oper.name());
248        }
249    }
250
251    private void insertNewSocket(int index) {
252        FemaleGenericExpressionSocket socket =
253                createFemaleSocket(this, this, getNewSocketName());
254        _expressionEntries.add(index, new ExpressionEntry(socket));
255
256        List<FemaleSocket> addList = new ArrayList<>();
257        addList.add(socket);
258        parseFormula();
259        firePropertyChange(Base.PROPERTY_CHILD_COUNT, null, addList);
260    }
261
262    private void removeSocket(int index) {
263        List<FemaleSocket> removeList = new ArrayList<>();
264        removeList.add(_expressionEntries.remove(index)._socket);
265        parseFormula();
266        firePropertyChange(Base.PROPERTY_CHILD_COUNT, removeList, null);
267    }
268
269    private void moveSocketDown(int index) {
270        ExpressionEntry temp = _expressionEntries.get(index);
271        _expressionEntries.set(index, _expressionEntries.get(index+1));
272        _expressionEntries.set(index+1, temp);
273
274        List<FemaleSocket> list = new ArrayList<>();
275        list.add(_expressionEntries.get(index)._socket);
276        list.add(_expressionEntries.get(index)._socket);
277        parseFormula();
278        firePropertyChange(Base.PROPERTY_CHILD_REORDER, null, list);
279    }
280
281    /** {@inheritDoc} */
282    @Override
283    public void doSocketOperation(int index, FemaleSocketOperation oper) {
284        switch (oper) {
285            case Remove:
286                if (getChild(index).isConnected()) throw new UnsupportedOperationException("Socket is connected");
287                removeSocket(index);
288                break;
289            case InsertBefore:
290                insertNewSocket(index);
291                break;
292            case InsertAfter:
293                insertNewSocket(index+1);
294                break;
295            case MoveUp:
296                if (index == 0) throw new UnsupportedOperationException("cannot move up first child");
297                moveSocketDown(index-1);
298                break;
299            case MoveDown:
300                if (index+1 == getChildCount()) throw new UnsupportedOperationException("cannot move down last child");
301                moveSocketDown(index);
302                break;
303            default:
304                throw new UnsupportedOperationException("Oper is unknown" + oper.name());
305        }
306    }
307
308    @Override
309    public void connected(FemaleSocket socket) {
310        if (_disableCheckForUnconnectedSocket) return;
311
312        for (ExpressionEntry entry : _expressionEntries) {
313            if (socket == entry._socket) {
314                entry._socketSystemName =
315                        socket.getConnectedSocket().getSystemName();
316                entry._manager =
317                        socket.getConnectedSocket().getManager().getClass().getName();
318            }
319        }
320
321        checkFreeSocket();
322    }
323
324    @Override
325    public void disconnected(FemaleSocket socket) {
326        for (ExpressionEntry entry : _expressionEntries) {
327            if (socket == entry._socket) {
328                entry._socketSystemName = null;
329                entry._manager = null;
330                break;
331            }
332        }
333    }
334
335    /** {@inheritDoc} */
336    @Override
337    public void socketNameChanged(FemaleSocket socket) {
338        parseFormula();
339    }
340
341    /** {@inheritDoc} */
342    @Override
343    public void setup() {
344        // We don't want to check for unconnected sockets while setup sockets
345        _disableCheckForUnconnectedSocket = true;
346
347        for (ExpressionEntry ee : _expressionEntries) {
348            try {
349                if ( !ee._socket.isConnected()
350                        || !ee._socket.getConnectedSocket().getSystemName()
351                                .equals(ee._socketSystemName)) {
352
353                    String socketSystemName = ee._socketSystemName;
354                    String manager = ee._manager;
355                    ee._socket.disconnect();
356                    if (socketSystemName != null) {
357                        Manager<? extends MaleSocket> m =
358                                InstanceManager.getDefault(LogixNG_Manager.class)
359                                        .getManager(manager);
360                        MaleSocket maleSocket = m.getBySystemName(socketSystemName);
361                        if (maleSocket != null) {
362                            ee._socket.connect(maleSocket);
363                            maleSocket.setup();
364                        } else {
365                            log.error("cannot load analog expression {}", socketSystemName);
366                        }
367                    }
368                } else {
369                    ee._socket.getConnectedSocket().setup();
370                }
371            } catch (SocketAlreadyConnectedException ex) {
372                // This shouldn't happen and is a runtime error if it does.
373                throw new RuntimeException("socket is already connected");
374            }
375        }
376
377        parseFormula();
378        checkFreeSocket();
379
380        _disableCheckForUnconnectedSocket = false;
381    }
382
383    /** {@inheritDoc} */
384    @Override
385    public void registerListenersForThisClass() {
386        // Do nothing
387    }
388
389    /** {@inheritDoc} */
390    @Override
391    public void unregisterListenersForThisClass() {
392        // Do nothing
393    }
394
395    /** {@inheritDoc} */
396    @Override
397    public void disposeMe() {
398    }
399
400    /** {@inheritDoc} */
401    @Override
402    public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) {
403        log.debug("getUsageReport :: Expression AnalogFormula: bean = {}, f = \"{}\", cdl = {}", bean, getFormula(), cdl);
404        if (bean != null) {
405            // Search for possible bean references using system and user names.  False positives are possible.
406            var formula = getFormula();
407            var uname = bean.getUserName();
408            if (formula.contains(bean.getSystemName()) || (uname != null && formula.contains(uname))) {
409                report.add(new NamedBeanUsageReport("LogixNGExpression", cdl, getLongDescription()));
410            }
411        }
412    }
413
414    public static class SocketData {
415        public final String _socketName;
416        public final String _socketSystemName;
417        public final String _manager;
418
419        public SocketData(String socketName, String socketSystemName, String manager) {
420            _socketName = socketName;
421            _socketSystemName = socketSystemName;
422            _manager = manager;
423        }
424    }
425
426
427    /* This class is public since ExpressionFormulaXml needs to access it. */
428    public static class ExpressionEntry {
429        private final FemaleGenericExpressionSocket _socket;
430        private String _socketSystemName;
431        public String _manager;
432
433        public ExpressionEntry(FemaleGenericExpressionSocket socket, String socketSystemName, String manager) {
434            _socket = socket;
435            _socketSystemName = socketSystemName;
436            _manager = manager;
437        }
438
439        private ExpressionEntry(FemaleGenericExpressionSocket socket) {
440            this._socket = socket;
441        }
442
443    }
444
445
446    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AnalogFormula.class);
447}