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