001package jmri.jmrit.operations.trains.manualtrainbuilder;
002
003import java.util.*;
004
005import org.jdom2.Element;
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009import jmri.InstanceManager;
010import jmri.beans.PropertyChangeSupport;
011import jmri.jmrit.operations.locations.LocationManagerXml;
012import jmri.jmrit.operations.setup.Control;
013import jmri.jmrit.operations.trains.Train;
014import jmri.jmrit.operations.trains.TrainManager;
015
016/**
017 * Train Manual Build. Allows a user to manually assign cars to a train.
018 *
019 * @author Daniel Boudreau Copyright (C) 2026
020 */
021public class TrainManualBuild extends PropertyChangeSupport implements java.beans.PropertyChangeListener {
022
023    protected String _id = "";
024    protected String _trainId = "";
025    protected String _comment = "";
026
027    // stores manual build items for this manual build
028    protected Hashtable<String, TrainManualBuildItem> _manualBuildHashTable =
029            new Hashtable<String, TrainManualBuildItem>();
030    protected int _IdNumber = 0; // each item in a manual build gets its own id
031    protected int _sequenceNum = 0; // each item has a unique sequence number
032
033    public static final String LISTCHANGE_CHANGED_PROPERTY = "manualBuildListChange"; // NOI18N
034    public static final String DISPOSE = "manualBuildDispose"; // NOI18N
035
036    public TrainManualBuild(String id, String trainId) {
037        log.debug("New manual build ({}) id: {}", trainId, id);
038        _trainId = trainId;
039        _id = id;
040    }
041
042    public String getId() {
043        return _id;
044    }
045
046    public void setTrainId(String trainId) {
047        String old = _trainId;
048        _trainId = trainId;
049        if (!old.equals(trainId)) {
050            setDirtyAndFirePropertyChange("ManualBuildId", old, trainId); // NOI18N
051        }
052    }
053
054    public String getTrainId() {
055        return _trainId;
056    }
057
058    public String getTrainName() {
059        TrainManager trainManager = InstanceManager.getDefault(TrainManager.class);
060        Train train = trainManager.getTrainById(getTrainId());
061        if (train != null) {
062            return train.getName();
063        }
064        return "";
065    }
066
067    public int getSize() {
068        return _manualBuildHashTable.size();
069    }
070
071    public void setComment(String comment) {
072        String old = _comment;
073        _comment = comment;
074        if (!old.equals(comment)) {
075            setDirtyAndFirePropertyChange("ManualBuildComment", old, comment); // NOI18N
076        }
077    }
078
079    public String getComment() {
080        return _comment;
081    }
082
083    public void dispose() {
084        setDirtyAndFirePropertyChange(DISPOSE, null, DISPOSE);
085    }
086
087    /**
088     * Adds build item to the end of this manual build
089     * 
090     * @return ManualBuildItem created
091     */
092    public TrainManualBuildItem addItem() {
093        _IdNumber++;
094        _sequenceNum++;
095        String id = _id + "m" + Integer.toString(_IdNumber);
096        log.debug("Adding new item to ({}) id: {}", getTrainId(), id);
097        TrainManualBuildItem mbi = new TrainManualBuildItem(id);
098        mbi.setSequenceId(_sequenceNum);
099        int old = _manualBuildHashTable.size();
100        _manualBuildHashTable.put(mbi.getId(), mbi);
101
102        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, _manualBuildHashTable.size());
103        // listen for set out and pick up changes to forward
104        mbi.addPropertyChangeListener(this);
105        return mbi;
106    }
107
108    /**
109     * Add a manual build item at a specific place (sequence) in the manual
110     * build. Allowable sequence numbers are 0 to max size of manual build. 0 =
111     * start of list.
112     * 
113     * @param sequence Where in the Manual Build to add the item.
114     * @return manual build item
115     */
116    public TrainManualBuildItem addItem(int sequence) {
117        TrainManualBuildItem mbi = addItem();
118        if (sequence < 0 || sequence > _manualBuildHashTable.size()) {
119            return mbi;
120        }
121        for (int i = 0; i < _manualBuildHashTable.size() - sequence - 1; i++) {
122            moveItemUp(mbi);
123        }
124        return mbi;
125    }
126
127    /**
128     * Remember a NamedBean Object created outside the manager.
129     * 
130     * @param mbi The manual build item to add.
131     */
132    public void register(TrainManualBuildItem mbi) {
133        int old = _manualBuildHashTable.size();
134        _manualBuildHashTable.put(mbi.getId(), mbi);
135
136        // find last id created
137        String[] getId = mbi.getId().split("m");
138        int id = Integer.parseInt(getId[1]);
139        if (id > _IdNumber) {
140            _IdNumber = id;
141        }
142        // find highest sequence number
143        if (mbi.getSequenceId() > _sequenceNum) {
144            _sequenceNum = mbi.getSequenceId();
145        }
146        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, _manualBuildHashTable.size());
147        // listen for set out and pick up changes to forward
148        mbi.addPropertyChangeListener(this);
149    }
150
151    /**
152     * Delete a manual build item
153     * 
154     * @param mbi The manual build item to delete.
155     */
156    public void deleteItem(TrainManualBuildItem mbi) {
157        if (mbi != null) {
158            mbi.removePropertyChangeListener(this);
159            // subtract from the items's available track length
160            String id = mbi.getId();
161            mbi.dispose();
162            int old = _manualBuildHashTable.size();
163            _manualBuildHashTable.remove(id);
164            resequenceIds();
165            setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, _manualBuildHashTable.size());
166        }
167    }
168
169    /**
170     * Reorder the item sequence numbers for this manual build
171     */
172    private void resequenceIds() {
173        List<TrainManualBuildItem> manualBuildItems = getItemsBySequenceList();
174        for (int i = 0; i < manualBuildItems.size(); i++) {
175            // start sequence numbers at 1
176            manualBuildItems.get(i).setSequenceId(i + 1);
177            _sequenceNum = i + 1;
178        }
179    }
180
181    /**
182     * Get a ManualBuildItem by id
183     * 
184     * @param id The string id of the ManualBuildItem.
185     * @return manual build item
186     */
187    public TrainManualBuildItem getItemById(String id) {
188        return _manualBuildHashTable.get(id);
189    }
190
191    private List<TrainManualBuildItem> getItemsByIdList() {
192        String[] arr = new String[_manualBuildHashTable.size()];
193        List<TrainManualBuildItem> out = new ArrayList<TrainManualBuildItem>();
194        Enumeration<String> en = _manualBuildHashTable.keys();
195        int i = 0;
196        while (en.hasMoreElements()) {
197            arr[i++] = en.nextElement();
198        }
199        Arrays.sort(arr);
200        for (i = 0; i < arr.length; i++) {
201            out.add(getItemById(arr[i]));
202        }
203        return out;
204    }
205
206    /**
207     * Get a list of ManualBuildItems sorted by sequence order
208     *
209     * @return list of ManualBuildItems ordered by sequence
210     */
211    public List<TrainManualBuildItem> getItemsBySequenceList() {
212        // first get id list
213        List<TrainManualBuildItem> sortList = getItemsByIdList();
214        // now re-sort
215        List<TrainManualBuildItem> out = new ArrayList<TrainManualBuildItem>();
216
217        for (TrainManualBuildItem mbi : sortList) {
218            for (int j = 0; j < out.size(); j++) {
219                if (mbi.getSequenceId() < out.get(j).getSequenceId()) {
220                    out.add(j, mbi);
221                    break;
222                }
223            }
224            if (!out.contains(mbi)) {
225                out.add(mbi);
226            }
227        }
228        return out;
229    }
230
231    /**
232     * Places a ManualBuildItem earlier in the manual build
233     * 
234     * @param mbi The ManualBuildItem to move.
235     */
236    public void moveItemUp(TrainManualBuildItem mbi) {
237        int sequenceId = mbi.getSequenceId();
238        if (sequenceId - 1 <= 0) {
239            mbi.setSequenceId(_sequenceNum + 1); // move to the end of the list
240            resequenceIds();
241        } else {
242            // adjust the other item taken by this one
243            TrainManualBuildItem replaceSi = getItemBySequenceId(sequenceId - 1);
244            if (replaceSi != null) {
245                replaceSi.setSequenceId(sequenceId);
246                mbi.setSequenceId(sequenceId - 1);
247            } else {
248                resequenceIds(); // error the sequence number is missing
249            }
250        }
251        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, sequenceId);
252    }
253
254    /**
255     * Places a ManualBuildItem later in the manualBuild
256     * 
257     * @param mbi The ManualBuildItem to move.
258     */
259    public void moveItemDown(TrainManualBuildItem mbi) {
260        int sequenceId = mbi.getSequenceId();
261        if (sequenceId + 1 > _sequenceNum) {
262            mbi.setSequenceId(0); // move to the start of the list
263            resequenceIds();
264        } else {
265            // adjust the other item taken by this one
266            TrainManualBuildItem replaceSi = getItemBySequenceId(sequenceId + 1);
267            if (replaceSi != null) {
268                replaceSi.setSequenceId(sequenceId);
269                mbi.setSequenceId(sequenceId + 1);
270            } else {
271                resequenceIds(); // error the sequence number is missing
272            }
273        }
274        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, sequenceId);
275    }
276
277    public TrainManualBuildItem getItemBySequenceId(int sequenceId) {
278        for (TrainManualBuildItem mbi : getItemsByIdList()) {
279            if (mbi.getSequenceId() == sequenceId) {
280                return mbi;
281            }
282        }
283        return null;
284    }
285
286    /**
287     * Construct this Entry from XML. This member has to remain synchronized
288     * with the detailed DTD in operations-config.xml
289     *
290     * @param e Consist XML element
291     */
292    public TrainManualBuild(Element e) {
293        org.jdom2.Attribute a;
294        if ((a = e.getAttribute(Xml.ID)) != null) {
295            _id = a.getValue();
296        } else {
297            log.warn("no id attribute in manualBuild element when reading operations");
298        }
299        if ((a = e.getAttribute(Xml.TRAIN_ID)) != null) {
300            _trainId = a.getValue();
301        }
302        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
303            _comment = a.getValue();
304        }
305        if (e.getChildren(Xml.MANUAL_BUILD_ITEM) != null) {
306            List<Element> eManualBuildItems = e.getChildren(Xml.MANUAL_BUILD_ITEM);
307            log.debug("manualBuild: {} has {} items", getTrainId(), eManualBuildItems.size());
308            for (Element eManualBuildItem : eManualBuildItems) {
309                register(new TrainManualBuildItem(eManualBuildItem));
310            }
311        }
312    }
313
314    /**
315     * Create an XML element to represent this Entry. This member has to remain
316     * synchronized with the detailed DTD in operations-config.xml.
317     *
318     * @return Contents in a JDOM Element
319     */
320    public org.jdom2.Element store() {
321        Element e = new org.jdom2.Element(Xml.MANUAL_BUILD);
322        e.setAttribute(Xml.ID, getId());
323        e.setAttribute(Xml.TRAIN_ID, getTrainId());
324        e.setAttribute(Xml.NAME, getTrainName());
325        e.setAttribute(Xml.COMMENT, getComment());
326        for (TrainManualBuildItem mbi : getItemsBySequenceList()) {
327            e.addContent(mbi.store());
328        }
329
330        return e;
331    }
332
333    @Override
334    public void propertyChange(java.beans.PropertyChangeEvent e) {
335        if (Control.SHOW_PROPERTY) {
336            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e
337                    .getNewValue());
338        }
339        // forward all manualBuild item changes
340        setDirtyAndFirePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue());
341    }
342
343    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
344        // set dirty
345        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
346        firePropertyChange(p, old, n);
347    }
348
349    private static final Logger log = LoggerFactory.getLogger(TrainManualBuild.class);
350
351}