001package jmri.jmrit.operations.rollingstock;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.time.LocalDateTime;
006import java.util.*;
007
008import javax.annotation.OverridingMethodsMustInvokeSuper;
009
010import jmri.beans.PropertyChangeSupport;
011import jmri.jmrit.operations.locations.Location;
012import jmri.jmrit.operations.locations.Track;
013import jmri.jmrit.operations.trains.Train;
014import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
015
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019/**
020 * Base class for rolling stock managers car and engine.
021 *
022 * @author Daniel Boudreau Copyright (C) 2010, 2011, 2026
023 * @param <T> the type of RollingStock managed by this manager
024 */
025public abstract class RollingStockManager<T extends RollingStock> extends PropertyChangeSupport implements PropertyChangeListener {
026
027    public static final String NONE = "";
028
029    // RollingStock
030    protected Hashtable<String, T> _hashTable = new Hashtable<>();
031
032    public static final String LISTLENGTH_CHANGED_PROPERTY = "RollingStockListLength"; // NOI18N
033    
034    abstract public RollingStock newRS(String road, String number);
035
036    public RollingStockManager() {
037    }
038
039    /**
040     * Get the number of items in the roster
041     *
042     * @return Number of rolling stock in the Roster
043     */
044    public int getNumEntries() {
045        return _hashTable.size();
046    }
047
048    public void dispose() {
049        deleteAll();
050    }
051
052    /**
053     * Get rolling stock by id
054     *
055     * @param id The string id.
056     *
057     * @return requested RollingStock object or null if none exists
058     */
059    public T getById(String id) {
060        return _hashTable.get(id);
061    }
062
063    /**
064     * Get rolling stock by road and number
065     *
066     * @param road   RollingStock road
067     * @param number RollingStock number
068     * @return requested RollingStock object or null if none exists
069     */
070    public T getByRoadAndNumber(String road, String number) {
071        String id = RollingStock.createId(road, number);
072        return getById(id);
073    }
074
075    /**
076     * Get a rolling stock by type and road. Used to test that rolling stock
077     * with a specific type and road exists.
078     *
079     * @param type RollingStock type.
080     * @param road RollingStock road.
081     * @return the first RollingStock found with the specified type and road.
082     */
083    public T getByTypeAndRoad(String type, String road) {
084        Enumeration<String> en = _hashTable.keys();
085        while (en.hasMoreElements()) {
086            T rs = getById(en.nextElement());
087            if (rs.getTypeName().equals(type) && rs.getRoadName().equals(road)) {
088                return rs;
089            }
090        }
091        return null;
092    }
093
094    /**
095     * Get a rolling stock by Radio Frequency Identification (RFID)
096     *
097     * @param rfid RollingStock's RFID.
098     * @return the RollingStock with the specific RFID, or null if not found
099     */
100    public T getByRfid(String rfid) {
101        Enumeration<String> en = _hashTable.keys();
102        while (en.hasMoreElements()) {
103            T rs = getById(en.nextElement());
104            if (rs.getRfid().equals(rfid)) {
105                return rs;
106            }
107        }
108        return null;
109    }
110
111    /**
112     * Load RollingStock.
113     *
114     * @param rs The RollingStock to load.
115     */
116    public void register(T rs) {
117        if (!_hashTable.containsKey(rs.getId())) {
118            int oldSize = _hashTable.size();
119            rs.addPropertyChangeListener(this);
120            _hashTable.put(rs.getId(), rs);
121            firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size());
122        } else {
123            log.error("Duplicate rolling stock id: ({})", rs.getId());
124            rs.dispose();
125        }
126    }
127
128    /**
129     * Unload RollingStock.
130     *
131     * @param rs The RollingStock to delete.
132     */
133    public void deregister(T rs) {
134        rs.removePropertyChangeListener(this);
135        rs.dispose();
136        int oldSize = _hashTable.size();
137        _hashTable.remove(rs.getId());
138        firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size());
139    }
140
141    /**
142     * Remove all RollingStock from roster
143     */
144    public void deleteAll() {
145        int oldSize = _hashTable.size();
146        Enumeration<String> en = _hashTable.keys();
147        while (en.hasMoreElements()) {
148            T rs = getById(en.nextElement());
149            rs.dispose();
150            _hashTable.remove(rs.getId());
151        }
152        firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size());
153    }
154
155    public void resetMoves() {
156        resetMoves(getList());
157    }
158
159    public void resetMoves(List<T> list) {
160        for (RollingStock rs : list) {
161            rs.setMoves(0);
162        }
163    }
164
165    /**
166     * Returns a list (no order) of RollingStock.
167     *
168     * @return list of RollingStock
169     */
170    public List<T> getList() {
171        return new ArrayList<>(_hashTable.values());
172    }
173
174    /**
175     * Sort by rolling stock id
176     *
177     * @return list of RollingStock ordered by id
178     */
179    public List<T> getByIdList() {
180        Enumeration<String> en = _hashTable.keys();
181        String[] arr = new String[_hashTable.size()];
182        List<T> out = new ArrayList<>();
183        int i = 0;
184        while (en.hasMoreElements()) {
185            arr[i++] = en.nextElement();
186        }
187        Arrays.sort(arr);
188        for (i = 0; i < arr.length; i++) {
189            out.add(getById(arr[i]));
190        }
191        return out;
192    }
193
194    /**
195     * Sort by rolling stock road name
196     *
197     * @return list of RollingStock ordered by road name
198     */
199    public List<T> getByRoadNameList() {
200        return getByList(getByIdList(), BY_ROAD);
201    }
202
203    private static final int PAGE_SIZE = 64;
204    private static final int NOT_INTEGER = -999999999; // flag when RS number isn't an Integer
205
206    /**
207     * Sort by rolling stock number, number can be alphanumeric. RollingStock
208     * number can also be in the format of nnnn-N, where the "-N" allows the
209     * user to enter RollingStock with similar numbers.
210     *
211     * @return list of RollingStock ordered by number
212     */
213    public List<T> getByNumberList() {
214        // first get by road list
215        List<T> sortIn = getByRoadNameList();
216        // now re-sort
217        List<T> out = new ArrayList<>();
218        int rsNumber = 0;
219        int outRsNumber = 0;
220
221        for (T rs : sortIn) {
222            boolean rsAdded = false;
223            try {
224                rsNumber = Integer.parseInt(rs.getNumber());
225                rs.number = rsNumber;
226            } catch (NumberFormatException e) {
227                // maybe rolling stock number in the format nnnn-N
228                try {
229                    String[] number = rs.getNumber().split(TrainCommon.HYPHEN);
230                    rsNumber = Integer.parseInt(number[0]);
231                    rs.number = rsNumber;
232                } catch (NumberFormatException e2) {
233                    rs.number = NOT_INTEGER;
234                    // sort alphanumeric numbers at the end of the out list
235                    String numberIn = rs.getNumber();
236                    // log.debug("rolling stock in road number ("+numberIn+") isn't a number");
237                    for (int k = (out.size() - 1); k >= 0; k--) {
238                        String numberOut = out.get(k).getNumber();
239                        try {
240                            Integer.parseInt(numberOut);
241                            // done, place rolling stock with alphanumeric
242                            // number after rolling stocks with real numbers.
243                            out.add(k + 1, rs);
244                            rsAdded = true;
245                            break;
246                        } catch (NumberFormatException e3) {
247                            if (numberIn.compareToIgnoreCase(numberOut) >= 0) {
248                                out.add(k + 1, rs);
249                                rsAdded = true;
250                                break;
251                            }
252                        }
253                    }
254                    if (!rsAdded) {
255                        out.add(0, rs);
256                    }
257                    continue;
258                }
259            }
260
261            int start = 0;
262            // page to improve sort performance.
263            int divisor = out.size() / PAGE_SIZE;
264            for (int k = divisor; k > 0; k--) {
265                outRsNumber = out.get((out.size() - 1) * k / divisor).number;
266                if (outRsNumber == NOT_INTEGER) {
267                    continue;
268                }
269                if (rsNumber >= outRsNumber) {
270                    start = (out.size() - 1) * k / divisor;
271                    break;
272                }
273            }
274            for (int j = start; j < out.size(); j++) {
275                outRsNumber = out.get(j).number;
276                if (outRsNumber == NOT_INTEGER) {
277                    try {
278                        outRsNumber = Integer.parseInt(out.get(j).getNumber());
279                    } catch (NumberFormatException e) {
280                        try {
281                            String[] number = out.get(j).getNumber().split(TrainCommon.HYPHEN);
282                            outRsNumber = Integer.parseInt(number[0]);
283                        } catch (NumberFormatException e2) {
284                            // force add
285                            outRsNumber = rsNumber + 1;
286                        }
287                    }
288                }
289                if (rsNumber < outRsNumber) {
290                    out.add(j, rs);
291                    rsAdded = true;
292                    break;
293                }
294            }
295            if (!rsAdded) {
296                out.add(rs);
297            }
298        }
299        // log.debug("end rolling stock sort by number list");
300        return out;
301    }
302
303    /**
304     * Sort by rolling stock type names
305     *
306     * @return list of RollingStock ordered by RollingStock type
307     */
308    public List<T> getByTypeList() {
309        return getByList(getByRoadNameList(), BY_TYPE);
310    }
311
312    /**
313     * Return rolling stock of a specific type
314     *
315     * @param type type of rolling stock
316     * @return list of RollingStock that are specific type
317     */
318    public List<T> getByTypeList(String type) {
319        List<T> typeList = getByTypeList();
320        List<T> out = new ArrayList<>();
321        for (T rs : typeList) {
322            if (rs.getTypeName().equals(type)) {
323                out.add(rs);
324            }
325        }
326        return out;
327    }
328
329    /**
330     * Sort by rolling stock color names
331     *
332     * @return list of RollingStock ordered by RollingStock color
333     */
334    public List<T> getByColorList() {
335        return getByList(getByTypeList(), BY_COLOR);
336    }
337
338    /**
339     * Sort by rolling stock location
340     *
341     * @return list of RollingStock ordered by RollingStock location
342     */
343    public List<T> getByLocationList() {
344        return getByList(getByNumberList(), BY_LOCATION);
345    }
346
347    /**
348     * Sort by rolling stock destination
349     *
350     * @return list of RollingStock ordered by RollingStock destination
351     */
352    public List<T> getByDestinationList() {
353        return getByList(getByLocationList(), BY_DESTINATION);
354    }
355
356    /**
357     * Sort by rolling stocks in trains
358     *
359     * @return list of RollingStock ordered by trains
360     */
361    public List<T> getByTrainList() {
362        List<T> byDest = getByList(getByIdList(), BY_DESTINATION);
363        List<T> byLoc = getByList(byDest, BY_LOCATION);
364        return getByList(byLoc, BY_TRAIN);
365    }
366
367    /**
368     * Sort by rolling stock moves
369     *
370     * @return list of RollingStock ordered by RollingStock moves
371     */
372    public List<T> getByMovesList() {
373        return getByList(getList(), BY_MOVES);
374    }
375
376    /**
377     * Sort by when rolling stock was built
378     *
379     * @return list of RollingStock ordered by RollingStock built date
380     */
381    public List<T> getByBuiltList() {
382        return getByList(getByIdList(), BY_BUILT);
383    }
384
385    /**
386     * Sort by rolling stock owner
387     *
388     * @return list of RollingStock ordered by RollingStock owner
389     */
390    public List<T> getByOwnerList() {
391        return getByList(getByIdList(), BY_OWNER);
392    }
393
394    /**
395     * Sort by rolling stock value
396     *
397     * @return list of RollingStock ordered by value
398     */
399    public List<T> getByValueList() {
400        return getByList(getByIdList(), BY_VALUE);
401    }
402
403    /**
404     * Sort by rolling stock RFID
405     *
406     * @return list of RollingStock ordered by RFIDs
407     */
408    public List<T> getByRfidList() {
409        return getByList(getByIdList(), BY_RFID);
410    }
411    
412    public List<T> getByPickupList() {
413        return getByList(getByDestinationList(), BY_PICKUP);
414    }
415
416    /**
417     * Get a list of all rolling stock sorted last date used
418     *
419     * @return list of RollingStock ordered by last date
420     */
421    public List<T> getByLastDateList() {
422        return getByList(getByIdList(), BY_LAST);
423    }
424    
425    public List<T> getByLastDateReversedList() {
426        List<T> out = getByLastDateList();
427        Collections.reverse(out);
428        return out;
429    } 
430    
431    public List<T> getByCommentList() {
432        return getByList(getByIdList(), BY_COMMENT);
433    }
434
435    protected List<T> getByList(List<T> sortIn, int attribute) {
436        List<T> out = new ArrayList<>(sortIn);
437        out.sort(getComparator(attribute));
438        return out;
439    }
440
441    // The various sort options for RollingStock
442    // see CarManager and EngineManger for other values
443    protected static final int BY_NUMBER = 0;
444    protected static final int BY_ROAD = 1;
445    protected static final int BY_TYPE = 2;
446    protected static final int BY_COLOR = 3;
447    protected static final int BY_LOCATION = 4;
448    protected static final int BY_DESTINATION = 5;
449    protected static final int BY_TRAIN = 6;
450    protected static final int BY_MOVES = 7;
451    protected static final int BY_BUILT = 8;
452    protected static final int BY_OWNER = 9;
453    protected static final int BY_RFID = 10;
454    protected static final int BY_VALUE = 11;
455    protected static final int BY_LAST = 12;
456    protected static final int BY_BLOCKING = 13;
457    private static final int BY_PICKUP = 14;
458    protected static final int BY_COMMENT = 15;
459
460    protected java.util.Comparator<T> getComparator(int attribute) {
461        switch (attribute) {
462            case BY_NUMBER:
463                return (r1, r2) -> (r1.getNumber().compareToIgnoreCase(r2.getNumber()));
464            case BY_ROAD:
465                return (r1, r2) -> (r1.getRoadName().compareToIgnoreCase(r2.getRoadName()));
466            case BY_TYPE:
467                return (r1, r2) -> (r1.getTypeName().compareToIgnoreCase(r2.getTypeName()));
468            case BY_COLOR:
469                return (r1, r2) -> (r1.getColorName().compareToIgnoreCase(r2.getColorName()));
470            case BY_LOCATION:
471                return (r1, r2) -> (r1.getStatus() + r1.getLocationName() + r1.getTrackName())
472                        .compareToIgnoreCase(r2.getStatus() + r2.getLocationName() + r2.getTrackName());
473            case BY_DESTINATION:
474                return (r1, r2) -> (r1.getDestinationName() + r1.getDestinationTrackName())
475                        .compareToIgnoreCase(r2.getDestinationName() + r2.getDestinationTrackName());
476            case BY_TRAIN:
477                return (r1, r2) -> (r1.getTrainName().compareToIgnoreCase(r2.getTrainName()));
478            case BY_MOVES:
479                return (r1, r2) -> (r1.getMoves() - r2.getMoves());
480            case BY_BUILT:
481                return (r1,
482                        r2) -> (convertBuildDate(r1.getBuilt()).compareToIgnoreCase(convertBuildDate(r2.getBuilt())));
483            case BY_OWNER:
484                return (r1, r2) -> (r1.getOwnerName().compareToIgnoreCase(r2.getOwnerName()));
485            case BY_RFID:
486                return (r1, r2) -> (r1.getRfid().compareToIgnoreCase(r2.getRfid()));
487            case BY_VALUE:
488                return (r1, r2) -> (r1.getValue().compareToIgnoreCase(r2.getValue()));
489            case BY_LAST:
490                return (r1, r2) -> (r1.getLastMoveDate().compareTo(r2.getLastMoveDate()));
491            case BY_BLOCKING:
492                return (r1, r2) -> (r1.getBlocking() - r2.getBlocking());
493            case BY_PICKUP:
494                return (r1, r2) -> (r1.getPickupTime().compareToIgnoreCase(r2.getPickupTime()));
495            case BY_COMMENT:
496                return (r1, r2) -> (r1.getComment().compareToIgnoreCase(r2.getComment()));
497            default:
498                return (r1, r2) -> ((r1.getRoadName() + r1.getNumber())
499                        .compareToIgnoreCase(r2.getRoadName() + r2.getNumber()));
500        }
501    }
502    
503    protected List<T> sortByTrackPriority(List<T> list) {
504        List<T> out = new ArrayList<>();
505        // sort rolling stock by track priority
506        for (T rs : list) {
507            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_HIGH)) {
508                out.add(rs);
509            }
510        }
511        for (T rs : list) {
512            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_MEDIUM)) {
513                out.add(rs);
514            }
515        }
516        for (T rs : list) {
517            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_NORMAL)) {
518                out.add(rs);
519            }
520        }
521        for (T rs : list) {
522            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_LOW)) {
523                out.add(rs);
524            }
525        }
526        // rolling stock without a track assignment
527        for (T rs : list) {
528            if (!out.contains(rs)) {
529                out.add(rs);
530            }
531        }
532        return out;
533    }
534
535    /*
536     * Converts build date into consistent String. Three build date formats; Two
537     * digits YY becomes 19YY. MM-YY becomes 19YY. MM-YYYY becomes YYYY.
538     */
539    public static String convertBuildDate(String date) {
540        String[] built = date.split("-");
541        if (built.length == 2) {
542            try {
543                int d = Integer.parseInt(built[1]);
544                if (d < 100) {
545                    d = d + 1900;
546                }
547                return Integer.toString(d);
548            } catch (NumberFormatException e) {
549                log.debug("Unable to parse built date ({})", date);
550            }
551        } else {
552            try {
553                int d = Integer.parseInt(date);
554                if (d < 100) {
555                    d = d + 1900;
556                }
557                return Integer.toString(d);
558            } catch (NumberFormatException e) {
559                log.debug("Unable to parse built date ({})", date);
560            }
561        }
562        return date;
563    }
564
565    /**
566     * Get a list of rolling stocks assigned to a train ordered by location
567     *
568     * @param train The Train.
569     *
570     * @return List of RollingStock assigned to the train ordered by location
571     */
572    public List<T> getByTrainList(Train train) {
573        return getByList(getList(train), BY_LOCATION);
574    }
575
576    /**
577     * Returns a list (no order) of RollingStock in a train.
578     *
579     * @param train The Train.
580     *
581     * @return list of RollingStock
582     */
583    public List<T> getList(Train train) {
584        List<T> out = new ArrayList<>();
585        getList().stream().filter((rs) -> {
586            return rs.getTrain() == train;
587        }).forEachOrdered((rs) -> {
588            out.add(rs);
589        });
590        return out;
591    }
592
593    /**
594     * Returns a list (no order) of RollingStock at a location.
595     *
596     * @param location location to search for.
597     * @return list of RollingStock
598     */
599    public List<T> getList(Location location) {
600        List<T> out = new ArrayList<>();
601        getList().stream().filter((rs) -> {
602            return rs.getLocation() == location;
603        }).forEachOrdered((rs) -> {
604            out.add(rs);
605        });
606        return out;
607    }
608
609    /**
610     * Returns a list (no order) of RollingStock on a track.
611     *
612     * @param track Track to search for.
613     * @return list of RollingStock
614     */
615    public List<T> getList(Track track) {
616        List<T> out = new ArrayList<>();
617        getList().stream().filter((rs) -> {
618            return rs.getTrack() == track;
619        }).forEachOrdered((rs) -> {
620            out.add(rs);
621        });
622        return out;
623    }
624    
625    /**
626     * Returns the rolling stock's last clone if there's one.
627     * 
628     * @param rs The rolling stock searching for a clone
629     * @return Returns the rolling stock's last clone, null if there isn't a
630     *         clone.
631     */
632    public T getClone(RollingStock rs) {
633        List<T> list = getByLastDateReversedList();
634        // clone with the highest creation number will be first in this list
635        for (T clone : list) {
636            if (clone.isClone() &&
637                    clone.getDestinationTrack() == rs.getTrack() &&
638                    clone.getRoadName().equals(rs.getRoadName()) &&
639                    clone.getNumber().split(RollingStock.CLONE_REGEX)[0].equals(rs.getNumber())) {
640                return clone;
641            }
642        }
643        return null; // no clone for this rolling stock
644    }
645
646    int cloneCreationOrder = 0;
647
648    /**
649     * Returns the highest clone creation order given to a clone.
650     * 
651     * @return 1 if the first clone created, otherwise the highest found plus
652     *         one. Automatically increments.
653     */
654    private int getCloneCreationOrder() {
655        if (cloneCreationOrder == 0) {
656            for (RollingStock rs : getList()) {
657                if (rs.isClone()) {
658                    String[] number = rs.getNumber().split(RollingStock.CLONE_REGEX);
659                    int creationOrder = Integer.parseInt(number[1]);
660                    if (creationOrder > cloneCreationOrder) {
661                        cloneCreationOrder = creationOrder;
662                    }
663                }
664            }
665        }
666        return ++cloneCreationOrder;
667    }
668    
669    /**
670     * Creates a clone of rolling stock and places it at the rolling stocks location and track.
671     * @param rs the rolling stock requesting a clone
672     * @return the clone of the rolling stock
673     */
674    protected T createClone(RollingStock rs) {
675        int cloneCreationOrder = getCloneCreationOrder();
676        return createClone(rs, cloneCreationOrder);
677    }
678    
679    protected T createClone(RollingStock rs, int cloneCreationOrder) {
680        @SuppressWarnings("unchecked")
681        T clone = (T) rs.copy();
682        clone.setNumber(rs.getNumber() + RollingStock.CLONE + padNumber(cloneCreationOrder));
683        clone.setClone(true);
684        clone.setMoves(rs.getMoves());
685        // register car before setting location so the car gets logged
686        register(clone);
687        clone.setLocation(rs.getLocation(), rs.getTrack(), RollingStock.FORCE);
688        clone.addPropertyChangeListeners();
689        rs.setCloneOrder(cloneCreationOrder); // for reset
690        return clone;
691    }
692    
693    /**
694     * Moves the rolling stock to the clone's destination track
695     * @param rs rolling stock to be moved
696     * @param track the destination track for the clone
697     * @param train the train that will transport the clone
698     * @param startTime when the rolling stock was moved
699     * @param clone the clone being transported by the train
700     */
701    protected void finshCreateClone(RollingStock rs, Track track, Train train, LocalDateTime startTime, RollingStock clone) {
702        rs.setMoves(rs.getMoves() + 1); // bump count
703        rs.setLocation(track.getLocation(), track, RollingStock.FORCE);
704        rs.setLastTrain(train);
705        rs.setLastLocationId(clone.getLocationId());
706        rs.setLastTrackId(clone.getTrackId());
707        rs.setLastRouteId(train.getRoute().getId());
708        // this rs was moved during the build process
709        rs.setLastDate(startTime);
710        rs.setDestination(null, null);
711    }
712
713    /**
714     * Pads the number to 4 digits for sorting purposes
715     * 
716     * @param n the number needed leading zeros
717     * @return String "number" with leading zeros if necessary
718     */
719    protected String padNumber(int n) {
720        return String.format("%04d", n);
721    }
722
723    @Override
724    @OverridingMethodsMustInvokeSuper
725    public void propertyChange(PropertyChangeEvent evt) {
726        if (evt.getPropertyName().equals(Xml.ID)) {
727            @SuppressWarnings("unchecked")
728            T rs = (T) evt.getSource(); // unchecked cast to T  
729            _hashTable.remove(evt.getOldValue());
730            if (_hashTable.containsKey(rs.getId())) {
731                log.error("Duplicate rolling stock id: ({})", rs.getId());
732                rs.dispose();
733            } else {
734                _hashTable.put(rs.getId(), rs);
735            }
736            // fire so listeners that rebuild internal lists get signal of change in id, even without change in size
737            firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, _hashTable.size(), _hashTable.size());
738        }
739    }
740
741    private static final Logger log = LoggerFactory.getLogger(RollingStockManager.class);
742
743}