001package jmri.jmrit.logixng.expressions;
002
003import jmri.util.TimerUnit;
004
005import java.util.*;
006
007import javax.annotation.Nonnull;
008
009import jmri.InstanceManager;
010import jmri.JmriException;
011import jmri.jmrit.logixng.*;
012import jmri.jmrit.logixng.util.*;
013import jmri.jmrit.logixng.util.parser.*;
014import jmri.util.TimerUtil;
015import jmri.util.TypeConversionUtil;
016
017/**
018 * An expression that waits some time before returning True.
019 *
020 * This expression returns False until some time has elapsed. Then it returns
021 * True once. After that, it returns False again until some time has elapsed.
022 *
023 * @author Daniel Bergqvist Copyright 2023
024 */
025public class Timer extends AbstractDigitalExpression {
026
027    private static class StateAndTimerTask{
028        ProtectedTimerTask _timerTask;
029        State _currentState = State.IDLE;
030    }
031
032    private enum State { IDLE, RUNNING, COMPLETED }
033
034    private final Map<ConditionalNG, StateAndTimerTask> _stateAndTimerTask = new HashMap<>();
035    private int _delay;
036    private NamedBeanAddressing _stateAddressing = NamedBeanAddressing.Direct;
037    private TimerUnit _unit = TimerUnit.MilliSeconds;
038    private String _stateReference = "";
039    private String _stateLocalVariable = "";
040    private String _stateFormula = "";
041    private ExpressionNode _stateExpressionNode;
042
043
044    public Timer(String sys, String user)
045            throws BadUserNameException, BadSystemNameException {
046        super(sys, user);
047    }
048
049    @Override
050    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException {
051        DigitalExpressionManager manager = InstanceManager.getDefault(DigitalExpressionManager.class);
052        String sysName = systemNames.get(getSystemName());
053        String userName = userNames.get(getSystemName());
054        if (sysName == null) sysName = manager.getAutoSystemName();
055        Timer copy = new Timer(sysName, userName);
056        copy.setComment(getComment());
057        copy.setDelayAddressing(_stateAddressing);
058        copy.setDelay(_delay);
059        copy.setDelayFormula(_stateFormula);
060        copy.setDelayLocalVariable(_stateLocalVariable);
061        copy.setDelayReference(_stateReference);
062        copy.setUnit(_unit);
063        return manager.registerExpression(copy).deepCopyChildren(this, systemNames, userNames);
064    }
065
066    /** {@inheritDoc} */
067    @Override
068    public LogixNG_Category getCategory() {
069        return LogixNG_Category.COMMON;
070    }
071
072    /**
073     * Get a new timer task.
074     * @param conditionalNG  the ConditionalNG
075     * @param timerDelay     the time the timer should wait
076     * @param timerStart     the time when the timer was started
077     */
078    private ProtectedTimerTask getNewTimerTask(ConditionalNG conditionalNG, long timerDelay, long timerStart) throws JmriException {
079
080        return new ProtectedTimerTask() {
081            @Override
082            public void execute() {
083                try {
084                    synchronized(Timer.this) {
085                        StateAndTimerTask stateAndTimerTask = _stateAndTimerTask.get(conditionalNG);
086                        stateAndTimerTask._timerTask = null;
087
088                        long currentTime = System.currentTimeMillis();
089                        long currentTimerTime = currentTime - timerStart;
090                        if (currentTimerTime < timerDelay) {
091                            scheduleTimer(conditionalNG, timerDelay, timerStart);
092                        } else {
093                            stateAndTimerTask._currentState = State.COMPLETED;
094                            if (conditionalNG.isListenersRegistered()) {
095                                conditionalNG.execute();
096                            }
097                        }
098                    }
099                } catch (RuntimeException | JmriException e) {
100                    log.error("Exception thrown", e);
101                }
102            }
103        };
104    }
105
106    private void scheduleTimer(ConditionalNG conditionalNG, long timerDelay, long timerStart) throws JmriException {
107        synchronized(Timer.this) {
108            StateAndTimerTask stateAndTimerTask = _stateAndTimerTask.get(conditionalNG);
109            if (stateAndTimerTask._timerTask != null) {
110                stateAndTimerTask._timerTask.stopTimer();
111            }
112            long currentTime = System.currentTimeMillis();
113            long currentTimerTime = currentTime - timerStart;
114            stateAndTimerTask._timerTask = getNewTimerTask(conditionalNG, timerDelay, timerStart);
115            TimerUtil.schedule(stateAndTimerTask._timerTask, timerDelay - currentTimerTime);
116        }
117    }
118
119    private long getNewDelay(ConditionalNG conditionalNG) throws JmriException {
120
121        switch (_stateAddressing) {
122            case Direct:
123                return _delay;
124
125            case Reference:
126                return TypeConversionUtil.convertToLong(ReferenceUtil.getReference(
127                        conditionalNG.getSymbolTable(), _stateReference));
128
129            case LocalVariable:
130                SymbolTable symbolTable = conditionalNG.getSymbolTable();
131                return TypeConversionUtil
132                        .convertToLong(symbolTable.getValue(_stateLocalVariable));
133
134            case Formula:
135                return _stateExpressionNode != null
136                        ? TypeConversionUtil.convertToLong(
137                                _stateExpressionNode.calculate(
138                                        conditionalNG.getSymbolTable()))
139                        : 0;
140
141            default:
142                throw new IllegalArgumentException("invalid _addressing state: " + _stateAddressing.name());
143        }
144    }
145
146    /** {@inheritDoc} */
147    @Override
148    public boolean evaluate() throws JmriException {
149        synchronized(this) {
150            ConditionalNG conditionalNG = getConditionalNG();
151            StateAndTimerTask stateAndTimerTask = _stateAndTimerTask
152                    .computeIfAbsent(conditionalNG, o -> new StateAndTimerTask());
153
154            switch (stateAndTimerTask._currentState) {
155                case RUNNING:
156                    return false;
157                case COMPLETED:
158                    stateAndTimerTask._currentState = State.IDLE;
159                    return true;
160                case IDLE:
161                    stateAndTimerTask._currentState = State.RUNNING;
162                    if (stateAndTimerTask._timerTask != null) {
163                        stateAndTimerTask._timerTask.stopTimer();
164                    }
165                    long timerStart = System.currentTimeMillis();
166                    long timerDelay = getNewDelay(conditionalNG) * _unit.getMultiply();
167                    scheduleTimer(conditionalNG, timerDelay, timerStart);
168                    return false;
169                default:
170                    throw new UnsupportedOperationException("currentState has invalid state: "+stateAndTimerTask._currentState.name());
171            }
172        }
173    }
174
175    public void setDelayAddressing(NamedBeanAddressing addressing) throws ParserException {
176        _stateAddressing = addressing;
177        parseDelayFormula();
178    }
179
180    public NamedBeanAddressing getDelayAddressing() {
181        return _stateAddressing;
182    }
183
184    /**
185     * Get the delay.
186     * @return the delay
187     */
188    public int getDelay() {
189        return _delay;
190    }
191
192    /**
193     * Set the delay.
194     * @param delay the delay
195     */
196    public void setDelay(int delay) {
197        _delay = delay;
198    }
199
200    public void setDelayReference(@Nonnull String reference) {
201        if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) {
202            throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference");
203        }
204        _stateReference = reference;
205    }
206
207    public String getDelayReference() {
208        return _stateReference;
209    }
210
211    public void setDelayLocalVariable(@Nonnull String localVariable) {
212        _stateLocalVariable = localVariable;
213    }
214
215    public String getDelayLocalVariable() {
216        return _stateLocalVariable;
217    }
218
219    public void setDelayFormula(@Nonnull String formula) throws ParserException {
220        _stateFormula = formula;
221        parseDelayFormula();
222    }
223
224    public String getDelayFormula() {
225        return _stateFormula;
226    }
227
228    private void parseDelayFormula() throws ParserException {
229        if (_stateAddressing == NamedBeanAddressing.Formula) {
230            Map<String, Variable> variables = new HashMap<>();
231
232            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
233            _stateExpressionNode = parser.parseExpression(_stateFormula);
234        } else {
235            _stateExpressionNode = null;
236        }
237    }
238
239    /**
240     * Get the unit
241     * @return the unit
242     */
243    public TimerUnit getUnit() {
244        return _unit;
245    }
246
247    /**
248     * Set the unit
249     * @param unit the unit
250     */
251    public void setUnit(TimerUnit unit) {
252        _unit = unit;
253    }
254
255    @Override
256    public String getShortDescription(Locale locale) {
257        return Bundle.getMessage(locale, "Timer_Short");
258    }
259
260    @Override
261    public String getLongDescription(Locale locale) {
262        String delay;
263
264        switch (_stateAddressing) {
265            case Direct:
266                delay = Bundle.getMessage(locale, "Timer_DelayByDirect", _unit.getTimeWithUnit(_delay));
267                break;
268
269            case Reference:
270                delay = Bundle.getMessage(locale, "Timer_DelayByReference", _stateReference, _unit.toString());
271                break;
272
273            case LocalVariable:
274                delay = Bundle.getMessage(locale, "Timer_DelayByLocalVariable", _stateLocalVariable, _unit.toString());
275                break;
276
277            case Formula:
278                delay = Bundle.getMessage(locale, "Timer_DelayByFormula", _stateFormula, _unit.toString());
279                break;
280
281            default:
282                throw new IllegalArgumentException("invalid _stateAddressing state: " + _stateAddressing.name());
283        }
284
285        return Bundle.getMessage(locale, "Timer_Long", delay);
286    }
287
288    /** {@inheritDoc} */
289    @Override
290    public void setup() {
291        // Do nothing
292    }
293
294    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Timer.class);
295
296}