001package jmri.jmrix.openlcb;
002
003import jmri.BooleanPropertyDescriptor;
004import jmri.JmriException;
005import jmri.NamedBean;
006import jmri.NamedBeanPropertyDescriptor;
007import jmri.Reporter;
008import jmri.jmrix.can.CanSystemConnectionMemo;
009import org.openlcb.OlcbInterface;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013import javax.annotation.Nonnull;
014import java.util.ArrayList;
015import java.util.List;
016import java.util.Locale;
017
018/**
019 * Manage the OpenLCB-specific Reporter implementation.
020 *
021 * System names are "MRaa.aa.aa.aa.aa.aa.00.00", where M is the user configurable system prefix,
022 * aa.aa....aa is an OpenLCB Event ID with the last two bytes as zero.
023 *
024 * Typical event IDs for reporters come out of the range 06.4* and 06.5*.
025 *
026 * @author Bob Jacobsen Copyright (C) 2008, 2010
027 * @author Balazs Racz Copyright (C) 2023
028 * @since 5.3.5
029 */
030public class OlcbReporterManager extends jmri.managers.AbstractReporterManager {
031
032    // Whether we accumulate loaded objects in pendingReporters.
033    private boolean isLoading = false;
034    // Turnouts that are being loaded from XML.
035    private final ArrayList<OlcbReporter> pendingReporters = new ArrayList<>();
036
037    /**
038     * {@inheritDoc}
039     */
040    @Override
041    @Nonnull
042    public CanSystemConnectionMemo getMemo() {
043        return (CanSystemConnectionMemo) memo;
044    }
045
046    @Override
047    @Nonnull
048    public List<NamedBeanPropertyDescriptor<?>> getKnownBeanProperties() {
049        List<NamedBeanPropertyDescriptor<?>> l = new ArrayList<>();
050        l.add(new BooleanPropertyDescriptor(OlcbUtils.PROPERTY_IS_AUTHORITATIVE, OlcbTurnout
051                .DEFAULT_IS_AUTHORITATIVE) {
052            @Override
053            public String getColumnHeaderText() {
054                return Bundle.getMessage("OlcbStateAuthHeader");
055            }
056
057            @Override
058            public boolean isEditable(NamedBean bean) {
059                return OlcbUtils.isOlcbBean(bean);
060            }
061        });
062        l.add(new BooleanPropertyDescriptor(OlcbUtils.PROPERTY_LISTEN, OlcbTurnout
063                .DEFAULT_LISTEN) {
064            @Override
065            public String getColumnHeaderText() {
066                return Bundle.getMessage("OlcbStateListenHeader");
067            }
068
069            @Override
070            public boolean isEditable(NamedBean bean) {
071                return OlcbUtils.isOlcbBean(bean);
072            }
073        });
074        return l;
075    }
076
077    // to free resources when no longer used
078    @Override
079    public void dispose() {
080        super.dispose();
081    }
082
083    // Implemented ready for new system connection memo
084    public OlcbReporterManager(CanSystemConnectionMemo memo) {
085        super(memo);
086    }
087
088    /**
089     * {@inheritDoc}
090     *
091     * @throws IllegalArgumentException when SystemName can't be converted
092     */
093    @Override
094    @Nonnull
095    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = "SLF4J_FORMAT_SHOULD_BE_CONST",
096        justification = "passing exception text")
097    protected Reporter createNewReporter(@Nonnull String systemName, String userName) throws IllegalArgumentException {
098        String addr = systemName.substring(getSystemNamePrefix().length());
099        // first, check validity
100        try {
101            validateSystemNameFormat(systemName,Locale.getDefault());
102        } catch (NamedBean.BadSystemNameException e) {
103            log.error(e.getMessage());
104            throw e;
105        }
106        // OK, make
107        OlcbReporter s = new OlcbReporter(getSystemPrefix(), addr, memo.get(OlcbInterface.class));
108        s.setUserName(userName);
109
110        synchronized (pendingReporters) {
111            if (isLoading) {
112                pendingReporters.add(s);
113            } else {
114                s.finishLoad();
115            }
116        }
117        return s;
118    }
119
120    /**
121     * This function is invoked before an XML load is started. We defer initialization of the
122     * newly created Reporters until finishLoad. This avoids certain quadratic run-time
123     * operations due to update listeners.
124     */
125    public void startLoad() {
126        log.debug("Reporter manager : start load");
127        synchronized (pendingReporters) {
128            isLoading = true;
129        }
130    }
131
132    /**
133     * This function is invoked after the XML load is complete and all Reporters are instantiated
134     * and their feedback type is read in. We use this hook to finalize the construction of the
135     * OpenLCB objects whose instantiation was deferred until the feedback type was known.
136     */
137    public void finishLoad() {
138        log.debug("Reporter manager : finish load");
139        synchronized (pendingReporters) {
140            pendingReporters.forEach(OlcbReporter::finishLoad);
141            pendingReporters.clear();
142            isLoading = false;
143        }
144    }
145
146    @Override
147    @Nonnull
148    public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
149        String tmpPrefix = prefix + typeLetter();
150        String tmpSName  = tmpPrefix + curAddress;
151        try {
152            OlcbAddress.validateSystemNameFormat(tmpSName,Locale.getDefault(),tmpPrefix);
153        }
154        catch ( NamedBean.BadSystemNameException ex ){
155            throw new JmriException(ex.getMessage());
156        }
157        // don't check for integer; should check for validity here
158        return prefix + typeLetter() + curAddress;
159    }
160
161    @Override
162    public boolean allowMultipleAdditions(@Nonnull String systemName) {
163        return true;
164    }
165
166    @Override
167    @Nonnull
168    @javax.annotation.CheckReturnValue
169    public String getNextValidSystemName(@Nonnull NamedBean currentBean) throws JmriException {
170        String currentName = currentBean.getSystemName();
171        return incrementSystemName(currentName);
172    }
173
174    /**
175     * Computes the system name for the next block sensor. This increments the unique ID
176     * of the manufacturer-assigned range (bytes 4-5-6) by one.
177     * @param currentName system name for a reporter of a given block
178     * @return next block's system name.
179     */
180    public String incrementSystemName(String currentName) {
181        String oAddr = currentName.substring(getSystemNamePrefix().length());
182        OlcbAddress a = new OlcbAddress(oAddr);
183        // Increments address elements 4-5-6 with overflow.
184        int[] e = a.elements();
185        int idx = 5;
186        while(idx > 2) {
187            e[idx]++;
188            if (e[idx] > 255) {
189                e[idx] = 0;
190                --idx;
191            } else {
192                break;
193            }
194        }
195        // Render new value.
196        String newValue = a.toDottedString();
197        return getSystemNamePrefix() + newValue;
198    }
199
200    /**
201     * {@inheritDoc}
202     */
203    @Override
204    public String getEntryToolTip() {
205        return Bundle.getMessage("AddReporterEntryToolTip");
206    }
207
208    /**
209     * Validates to OpenLCB format.
210     * {@inheritDoc}
211     */
212    @Override
213    @Nonnull
214    public String validateSystemNameFormat(@Nonnull String name, @Nonnull Locale locale) throws NamedBean.BadSystemNameException {
215        name = super.validateSystemNameFormat(name,locale);
216        name = OlcbAddress.validateSystemNameFormat(name,locale,getSystemNamePrefix());
217        return name;
218    }
219
220    private static final Logger log = LoggerFactory.getLogger(OlcbReporterManager.class);
221
222}
223
224