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