001package jmri.jmrit.logixng;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.io.*;
006import java.nio.charset.StandardCharsets;
007import java.util.Collection;
008import java.util.List;
009import java.util.HashMap;
010import java.util.Map;
011import java.util.Collections;
012
013import javax.script.Bindings;
014import javax.script.ScriptException;
015import javax.script.SimpleBindings;
016
017import jmri.*;
018import jmri.JmriException;
019import jmri.jmrit.logixng.util.ReferenceUtil;
020import jmri.jmrit.logixng.util.parser.*;
021import jmri.jmrit.logixng.util.parser.ExpressionNode;
022import jmri.jmrit.logixng.util.parser.LocalVariableExpressionVariable;
023import jmri.script.JmriScriptEngineManager;
024
025import org.slf4j.Logger;
026
027/**
028 * A symbol table
029 *
030 * @author Daniel Bergqvist 2020
031 */
032public interface SymbolTable {
033
034    /**
035     * The list of symbols in the table
036     * @return the symbols
037     */
038    Map<String, Symbol> getSymbols();
039
040    /**
041     * The list of symbols and their values in the table
042     * @return the name of the symbols and their values
043     */
044    Map<String, Object> getSymbolValues();
045
046    /**
047     * Get the value of a symbol
048     * @param name the name
049     * @return the value
050     */
051    Object getValue(String name);
052
053    /**
054     * Is the symbol in the symbol table?
055     * @param name the name
056     * @return true if the symbol exists, false otherwise
057     */
058    boolean hasValue(String name);
059
060    /**
061     * Set the value of a symbol
062     * @param name the name
063     * @param value the value
064     */
065    void setValue(String name, Object value);
066
067    /**
068     * Add new symbols to the symbol table
069     * @param symbolDefinitions the definitions of the new symbols
070     * @throws JmriException if an exception is thrown
071     */
072    void createSymbols(Collection<? extends VariableData> symbolDefinitions)
073            throws JmriException;
074
075    /**
076     * Add new symbols to the symbol table.
077     * This method is used for parameters, when new symbols might be created
078     * that uses symbols from a previous symbol table.
079     *
080     * @param symbolTable the symbol table to get existing symbols from
081     * @param symbolDefinitions the definitions of the new symbols
082     * @throws JmriException if an exception is thrown
083     */
084    void createSymbols(
085            SymbolTable symbolTable,
086            Collection<? extends VariableData> symbolDefinitions)
087            throws JmriException;
088
089    /**
090     * Removes symbols from the symbol table
091     * @param symbolDefinitions the definitions of the symbols to be removed
092     * @throws JmriException if an exception is thrown
093     */
094    void removeSymbols(Collection<? extends VariableData> symbolDefinitions)
095            throws JmriException;
096
097    /**
098     * Print the symbol table on a stream
099     * @param stream the stream
100     */
101    void printSymbolTable(java.io.PrintWriter stream);
102
103    /**
104     * Validates the name of a symbol
105     * @param name the name
106     * @return true if the name is valid, false otherwise
107     */
108    static boolean validateName(String name) {
109        if (name.isEmpty()) return false;
110        if (!Character.isLetter(name.charAt(0))) return false;
111        for (int i=0; i < name.length(); i++) {
112            if (!Character.isLetterOrDigit(name.charAt(i)) && (name.charAt(i) != '_')) {
113                return false;
114            }
115        }
116        return true;
117    }
118
119    /**
120     * Get the stack.
121     * This method is only used internally by DefaultSymbolTable.
122     *
123     * @return the stack
124     */
125    Stack getStack();
126
127
128    /**
129     * An enum that defines the types of initial value.
130     */
131    enum InitialValueType {
132
133        None(Bundle.getMessage("InitialValueType_None"), true),
134        Integer(Bundle.getMessage("InitialValueType_Integer"), true),
135        FloatingNumber(Bundle.getMessage("InitialValueType_FloatingNumber"), true),
136        String(Bundle.getMessage("InitialValueType_String"), true),
137        Array(Bundle.getMessage("InitialValueType_Array"), false),
138        Map(Bundle.getMessage("InitialValueType_Map"), false),
139        LocalVariable(Bundle.getMessage("InitialValueType_LocalVariable"), true),
140        Memory(Bundle.getMessage("InitialValueType_Memory"), true),
141        Reference(Bundle.getMessage("InitialValueType_Reference"), true),
142        Formula(Bundle.getMessage("InitialValueType_Formula"), true),
143        ScriptExpression(Bundle.getMessage("InitialValueType_ScriptExpression"), true),
144        ScriptFile(Bundle.getMessage("InitialValueType_ScriptFile"), true),
145        LogixNG_Table(Bundle.getMessage("InitialValueType_LogixNGTable"), true);
146
147        private final String _descr;
148        private final boolean _isValidAsParameter;
149
150        private InitialValueType(String descr, boolean isValidAsParameter) {
151            _descr = descr;
152            _isValidAsParameter = isValidAsParameter;
153        }
154
155        @Override
156        public String toString() {
157            return _descr;
158        }
159
160        public boolean isValidAsParameter() {
161            return _isValidAsParameter;
162        }
163    }
164
165
166    /**
167     * The definition of the symbol
168     */
169    interface Symbol {
170
171        /**
172         * The name of the symbol
173         * @return the name
174         */
175        String getName();
176
177        /**
178         * The index on the stack for this symbol
179         * @return the index
180         */
181        int getIndex();
182
183    }
184
185
186    /**
187     * Data for a variable.
188     */
189    static class VariableData {
190
191        public String _name;
192        public InitialValueType _initialValueType = InitialValueType.None;
193        public String _initialValueData;
194
195        public VariableData(
196                String name,
197                InitialValueType initialValueType,
198                String initialValueData) {
199
200            _name = name;
201            if (initialValueType != null) {
202                _initialValueType = initialValueType;
203            }
204            _initialValueData = initialValueData;
205        }
206
207        public VariableData(VariableData variableData) {
208            _name = variableData._name;
209            _initialValueType = variableData._initialValueType;
210            _initialValueData = variableData._initialValueData;
211        }
212
213        /**
214         * The name of the variable
215         * @return the name
216         */
217        public String getName() {
218            return _name;
219        }
220
221        public InitialValueType getInitialValueType() {
222            return _initialValueType;
223        }
224
225        public String getInitialValueData() {
226            return _initialValueData;
227        }
228
229    }
230
231    /**
232     * Print a variable
233     * @param log          the logger
234     * @param pad          the padding
235     * @param name         the name
236     * @param value        the value
237     * @param expandArraysAndMaps   true if arrays and maps should be expanded, false otherwise
238     * @param headerName   header for the variable name
239     * @param headerValue  header for the variable value
240     */
241    @SuppressWarnings("unchecked")  // Checked cast is not possible due to type erasure
242    @SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", justification="The code prints a complex variable, like a map, to the log")
243    static void printVariable(
244            Logger log,
245            String pad,
246            String name,
247            Object value,
248            boolean expandArraysAndMaps,
249            String headerName,
250            String headerValue) {
251
252        if (expandArraysAndMaps && (value instanceof Map)) {
253            log.warn("{}{}: {},", pad, headerName, name);
254            var map = ((Map<? extends Object, ? extends Object>)value);
255            for (var entry : map.entrySet()) {
256                log.warn("{}{}{} -> {},", pad, pad, entry.getKey(), entry.getValue());
257            }
258        } else if (expandArraysAndMaps && (value instanceof List)) {
259            log.warn("{}{}: {},", pad, headerName, name);
260            var list = ((List<? extends Object>)value);
261            for (int i=0; i < list.size(); i++) {
262                log.warn("{}{}{}: {},", pad, pad, i, list.get(i));
263            }
264        } else  {
265            log.warn("{}{}: {}, {}: {}", pad, headerName, name, headerValue, value);
266        }
267    }
268
269    private static Object runScriptExpression(String initialData) {
270        String script =
271                "import jmri\n" +
272                "variable.set(" + initialData + ")";
273
274        JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault();
275
276        Bindings bindings = new SimpleBindings();
277        LogixNG_ScriptBindings.addScriptBindings(bindings);
278
279        var variable = new Reference<Object>();
280        bindings.put("variable", variable);
281
282        try {
283            String theScript = String.format("import jmri%n") + script;
284            scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON)
285                    .eval(theScript, bindings);
286        } catch (ScriptException e) {
287            log.warn("cannot execute script", e);
288            return null;
289        }
290        return variable.get();
291    }
292
293    private static Object runScriptFile(String initialData) {
294
295        JmriScriptEngineManager scriptEngineManager = jmri.script.JmriScriptEngineManager.getDefault();
296
297        Bindings bindings = new SimpleBindings();
298        LogixNG_ScriptBindings.addScriptBindings(bindings);
299
300        var variable = new Reference<Object>();
301        bindings.put("variable", variable);
302
303        try (InputStreamReader reader = new InputStreamReader(
304                new FileInputStream(jmri.util.FileUtil.getExternalFilename(initialData)),
305                StandardCharsets.UTF_8)) {
306            scriptEngineManager.getEngineByName(JmriScriptEngineManager.JYTHON)
307                    .eval(reader, bindings);
308        } catch (IOException | ScriptException e) {
309            log.warn("cannot execute script \"{}\"", initialData, e);
310            return null;
311        }
312        return variable.get();
313    }
314
315    private static Object copyLogixNG_Table(String initialData) {
316
317        NamedTable myTable = InstanceManager.getDefault(NamedTableManager.class)
318                .getNamedTable(initialData);
319
320        var myMap = new java.util.concurrent.ConcurrentHashMap<Object, Map<Object, Object>>();
321
322        for (int row=1; row <= myTable.numRows(); row++) {
323            Object rowKey = myTable.getCell(row, 0);
324            var rowMap = new java.util.concurrent.ConcurrentHashMap<Object, Object>();
325
326            for (int col=1; col <= myTable.numColumns(); col++) {
327                var columnKey = myTable.getCell(0, col);
328                var cellValue = myTable.getCell(row, col);
329                rowMap.put(columnKey, cellValue);
330            }
331
332            myMap.put(rowKey, rowMap);
333        }
334
335        return myMap;
336    }
337
338    static Object getInitialValue(
339            InitialValueType initialType,
340            String initialData,
341            SymbolTable symbolTable,
342            Map<String, Symbol> symbols)
343            throws ParserException, JmriException {
344
345        switch (initialType) {
346            case None:
347                return null;
348
349            case Integer:
350                return Long.parseLong(initialData);
351
352            case FloatingNumber:
353                return Double.parseDouble(initialData);
354
355            case String:
356                return initialData;
357
358            case Array:
359                List<Object> array = new java.util.ArrayList<>();
360                Object initialValue = array;
361                String initialValueData = initialData;
362                if ((initialValueData != null) && !initialValueData.isEmpty()) {
363                    Object data = "";
364                    String[] parts = initialValueData.split(":", 2);
365                    if (parts.length > 1) {
366                        initialValueData = parts[0];
367                        if (Character.isDigit(parts[1].charAt(0))) {
368                            try {
369                                data = Long.parseLong(parts[1]);
370                            } catch (NumberFormatException e) {
371                                try {
372                                    data = Double.parseDouble(parts[1]);
373                                } catch (NumberFormatException e2) {
374                                    throw new IllegalArgumentException("Data is not a number", e2);
375                                }
376                            }
377                        } else if ((parts[1].charAt(0) == '"') && (parts[1].charAt(parts[1].length()-1) == '"')) {
378                            data = parts[1].substring(1,parts[1].length()-1);
379                        } else {
380                            // Assume initial value is a local variable
381                            data = symbolTable.getValue(parts[1]).toString();
382                        }
383                    }
384                    try {
385                        int count;
386                        if (Character.isDigit(initialValueData.charAt(0))) {
387                            count = Integer.parseInt(initialValueData);
388                        } else {
389                            // Assume size is a local variable
390                            count = Integer.parseInt(symbolTable.getValue(initialValueData).toString());
391                        }
392                        for (int i=0; i < count; i++) array.add(data);
393                    } catch (NumberFormatException e) {
394                        throw new IllegalArgumentException("Initial capacity is not an integer", e);
395                    }
396                }
397                return initialValue;
398
399            case Map:
400                return new java.util.HashMap<>();
401
402            case LocalVariable:
403                return symbolTable.getValue(initialData);
404
405            case Memory:
406                Memory m = InstanceManager.getDefault(MemoryManager.class).getNamedBean(initialData);
407                if (m != null) return m.getValue();
408                else return null;
409
410            case Reference:
411                if (ReferenceUtil.isReference(initialData)) {
412                    return ReferenceUtil.getReference(
413                            symbolTable, initialData);
414                } else {
415                    log.error("\"{}\" is not a reference", initialData);
416                    return null;
417                }
418
419            case Formula:
420                RecursiveDescentParser parser = createParser(symbols);
421                ExpressionNode expressionNode = parser.parseExpression(
422                        initialData);
423                return expressionNode.calculate(symbolTable);
424
425            case ScriptExpression:
426                return runScriptExpression(initialData);
427
428            case ScriptFile:
429                return runScriptFile(initialData);
430
431            case LogixNG_Table:
432                return copyLogixNG_Table(initialData);
433
434            default:
435                log.error("definition._initialValueType has invalid value: {}", initialType.name());
436                throw new IllegalArgumentException("definition._initialValueType has invalid value: " + initialType.name());
437        }
438    }
439
440    private static RecursiveDescentParser createParser(Map<String, Symbol> symbols)
441            throws ParserException {
442        Map<String, Variable> variables = new HashMap<>();
443
444        for (SymbolTable.Symbol symbol : Collections.unmodifiableMap(symbols).values()) {
445            variables.put(symbol.getName(),
446                    new LocalVariableExpressionVariable(symbol.getName()));
447        }
448
449        return new RecursiveDescentParser(variables);
450    }
451
452
453
454    static class SymbolNotFound extends IllegalArgumentException {
455
456        public SymbolNotFound(String message) {
457            super(message);
458        }
459    }
460
461
462    @SuppressFBWarnings(value="SLF4J_LOGGER_SHOULD_BE_PRIVATE", justification="Interfaces cannot have private fields")
463    org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SymbolTable.class);
464
465}