001/*
002 * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games.
003 * Copyright (C) 2000-2010 The Gridarta Developers.
004 *
005 * This program is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 2 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License along
016 * with this program; if not, write to the Free Software Foundation, Inc.,
017 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
018 */
019
020package net.sf.gridarta.model.baseobject;
021
022import java.io.IOException;
023import java.io.ObjectInputStream;
024import java.io.Serializable;
025import java.util.ArrayList;
026import java.util.Iterator;
027import java.util.List;
028import java.util.ListIterator;
029import net.sf.gridarta.model.archetype.Archetype;
030import net.sf.gridarta.model.gameobject.GameObject;
031import net.sf.gridarta.model.gameobject.NotInsideContainerException;
032import net.sf.gridarta.model.maparchobject.MapArchObject;
033import net.sf.gridarta.model.mapmodel.MapSquare;
034import org.jetbrains.annotations.NotNull;
035import org.jetbrains.annotations.Nullable;
036
037/**
038 * Base class for classes that contain GameObjects as children in the sense of
039 * containment. The interface serves 2 main purposes: <ul> <li>{@link
040 * GameObject} extends this class for containing other GameObjects, like
041 * inventory contents e.g. of a bag or attached events.</li> <li>{@link
042 * MapSquare} extends this class to list the squares on a MapSquare.</li> </ul>
043 * @author <a href="mailto:cher@riedquat.de">Christian Hujer</a>
044 * @todo In case of MapSquares, this class is most likely bogus regarding
045 * multi-part objects. This needs to be fixed.
046 */
047@SuppressWarnings("ClassReferencesSubclass")
048public abstract class GameObjectContainer<G extends GameObject<G, A, R>, A extends MapArchObject<A>, R extends Archetype<G, A, R>> implements Cloneable, Iterable<G>, Serializable {
049
050    /**
051     * The serial version UID.
052     */
053    private static final long serialVersionUID = 1L;
054
055    /**
056     * The contents of this container.
057     * @note the order of this container is bottom to top.
058     * @serial
059     */
060    @NotNull
061    private List<G> contents;
062
063    /**
064     * {@link Iterable} implementation for recursive traversal.
065     */
066    @NotNull
067    private transient Iterable<G> recursive;
068
069    /**
070     * {@link Iterable} implementation for reverse traversal.
071     */
072    @NotNull
073    private transient Iterable<G> reverse;
074
075    /**
076     * Create a new GameObjectContainer.
077     */
078    protected GameObjectContainer() {
079        initData();
080    }
081
082    /**
083     * Initialize the fields. This is needed because there are two ways of
084     * object construction, <ul> <li>regular object construction via constructor
085     * invocation and</li> <li>cloning of objects via {@link #clone()}.</li>
086     * </ul>
087     */
088    private void initData() {
089        contents = new ArrayList<G>(0);
090        recursive = new Iterable<G>() {
091
092            @Override
093            public Iterator<G> iterator() {
094                return new RecursiveGameObjectIterator<G, A, R>(GameObjectContainer.this);
095            }
096
097        };
098        reverse = new Iterable<G>() {
099
100            @Override
101            public Iterator<G> iterator() {
102                return new ReverseIterator<G>(contents);
103            }
104
105        };
106    }
107
108    /**
109     * {@inheritDoc} The Iterator returned does not recurse, it only contains
110     * objects on the first level. The Iterator returned is transparent, that
111     * means modifying the iterator's collection actually modifies the
112     * underlying GameObjectContainer.
113     */
114    @NotNull
115    @Override
116    public Iterator<G> iterator() {
117        // Do not return contents.iterator() directly because otherwise the remove() operation will not properly unlink the removed GameObject from its container.
118        return new Iterator<G>() {
119
120            /** The basic iterator. */
121            @NotNull
122            private final Iterator<G> delegate = contents.iterator();
123
124            /** Current element (last element returned by {@link #next()}). */
125            @Nullable
126            private G current;
127
128            @Override
129            public boolean hasNext() {
130                return delegate.hasNext();
131            }
132
133            @Override
134            public G next() {
135                current = delegate.next();
136                assert current != null;
137                return current;
138            }
139
140            @Override
141            public void remove() {
142                final G tmp = current;
143                if (tmp == null) {
144                    throw new IllegalStateException();
145                }
146                // keep this in sync with GameObjectContainer#remove(G)
147                // we can't simply invoke GameObjectContainer#remove(current) because that would result in a ConcurrentModificationException.
148                notifyBeginChange();
149                try {
150                    if (contents.size() >= 2 && contents.get(0) == tmp) {
151                        contents.get(1).propagateElevation(tmp);
152                    }
153                    delegate.remove();
154                    tmp.setContainer(null, 0, 0);
155                    current = null;
156                } finally {
157                    notifyEndChange();
158                }
159            }
160        };
161    }
162
163    /**
164     * Return an object that is the reverse representation. Invoke this method
165     * if you want to iterate over the contained GameObjects in reverse order.
166     * @return reverse representation
167     */
168    @NotNull
169    public Iterable<G> reverse() {
170        return reverse;
171    }
172
173    /**
174     * Return an object that is a recursive representation. Invoke this method
175     * if you want to iterate over the contained GameObjects recursively.
176     * @return recursive representation
177     */
178    @NotNull
179    public Iterable<G> recursive() {
180        return recursive;
181    }
182
183    /**
184     * Check whether this square is empty.
185     * @return <code>true</code> if this square is empty, otherwise
186     *         <code>false</code>
187     */
188    public boolean isEmpty() {
189        return contents.isEmpty();
190    }
191
192    /**
193     * Return the first GameObject contained in this container.
194     * @return first GameObject contained or <code>null</code> if {@link
195     *         #isEmpty()} returns <code>true</code>
196     */
197    @Nullable
198    public G getFirst() {
199        return contents.isEmpty() ? null : contents.get(0);
200    }
201
202    /**
203     * Return the {@link GameObject} preceding a given game object.
204     * @param gameObject the given game object
205     * @return the preceding game object or <code>null</code> if no preceding
206     *         game object exists.
207     */
208    @Nullable
209    public G getPrev(@NotNull final G gameObject) {
210        final Iterator<G> it = contents.iterator();
211        while (it.hasNext()) {
212            if (it.next() == gameObject) {
213                return it.hasNext() ? it.next() : null;
214            }
215        }
216        return null;
217    }
218
219    /**
220     * Return the {@link GameObject} succeeding a given game object.
221     * @param gameObject the given game object
222     * @return the successor game object or <code>null</code> if no successor
223     *         game object exists.
224     */
225    @Nullable
226    public G getNext(@NotNull final G gameObject) {
227        G prevGameObject = null;
228        for (final G tmpGameObject : contents) {
229            if (tmpGameObject == gameObject) {
230                return prevGameObject;
231            }
232            prevGameObject = tmpGameObject;
233        }
234        return null;
235    }
236
237    /**
238     * Return the last GameObject contained in this container. You should not
239     * invoke this method to iterate over GameObjects, such invocation is
240     * regarded deprecated.
241     * @return last GameObject contained or <code>null</code> if {@link
242     *         #isEmpty()} returns <code>true</code>
243     * @noinspection TypeMayBeWeakened
244     */
245    @Nullable
246    public G getLast() {
247        return contents.isEmpty() ? null : contents.get(contents.size() - 1);
248    }
249
250    /**
251     * Remove a GameObject from this container.
252     * @param gameObject GameObject to remove
253     * @throws IllegalArgumentException if <var>gameObject</var> isn't in this
254     * container
255     * @fixme this implementation does not take care of multi square objects.
256     */
257    public void remove(@NotNull final G gameObject) {
258        // keep this in sync with iterator()
259        notifyBeginChange();
260        try {
261            if (contents.size() >= 2 && contents.get(0) == gameObject) {
262                contents.get(1).propagateElevation(gameObject);
263            }
264            if (!contents.remove(gameObject)) {
265                throw new NotInsideContainerException(this, gameObject);
266            }
267            gameObject.setContainer(null, 0, 0);
268        } finally {
269            notifyEndChange();
270        }
271    }
272
273    /**
274     * Removes all GameObjects from this container.
275     * @fixme this implementation does not take care of multi square objects.
276     */
277    public void removeAll() {
278        if (contents.size() <= 0) {
279            return;
280        }
281
282        notifyBeginChange();
283        try {
284            for (final GameObject<G, A, R> gameObject : contents) {
285                gameObject.setContainer(null, 0, 0);
286            }
287            contents.clear();
288        } finally {
289            notifyEndChange();
290        }
291    }
292
293    /**
294     * Returns whether this game object is the top-most one.
295     * @param gameObject item to move to top
296     * @return whether this game object is the top-most one
297     * @throws IllegalArgumentException if <var>gameObject</var> isn't in this
298     * container
299     */
300    public boolean isTop(@NotNull final G gameObject) {
301        return !contents.isEmpty() && contents.get(contents.size() - 1) == gameObject;
302    }
303
304    /**
305     * Returns whether this game object is the bottom-most one.
306     * @param gameObject item to move to top
307     * @return whether this game object is the bottom-most one
308     * @throws IllegalArgumentException if <var>gameObject</var> isn't in this
309     * container
310     */
311    public boolean isBottom(@NotNull final G gameObject) {
312        return !contents.isEmpty() && contents.get(0) == gameObject;
313    }
314
315    /**
316     * Move an item to top.
317     * @param gameObject item to move to top
318     * @throws IllegalArgumentException if <var>gameObject</var> isn't in this
319     * container
320     */
321    public void moveTop(@NotNull final G gameObject) {
322        final int oldIndex = contents.indexOf(gameObject);
323        if (oldIndex != contents.size() - 1) {
324            notifyBeginChange();
325            try {
326                if (!contents.remove(gameObject)) {
327                    throw new NotInsideContainerException(this, gameObject);
328                }
329                if (oldIndex == 0) {
330                    assert contents.size() >= 1;
331                    contents.get(0).propagateElevation(gameObject);
332                }
333                contents.add(gameObject);
334            } finally {
335                notifyEndChange();
336            }
337        }
338    }
339
340    /**
341     * Move an item up.
342     * @param gameObject item to move up
343     * @throws IllegalArgumentException if <var>gameObject</var> isn't in this
344     * container
345     */
346    public void moveUp(@NotNull final G gameObject) {
347        final int oldIndex = contents.indexOf(gameObject);
348        if (oldIndex < contents.size() - 1) {
349            notifyBeginChange();
350            try {
351                if (!contents.remove(gameObject)) {
352                    throw new NotInsideContainerException(this, gameObject);
353                }
354                if (oldIndex == 0) {
355                    assert contents.size() >= 1;
356                    contents.get(0).propagateElevation(gameObject);
357                }
358                contents.add(oldIndex + 1, gameObject);
359            } finally {
360                notifyEndChange();
361            }
362        }
363    }
364
365    /**
366     * Move an item down.
367     * @param gameObject item to move down
368     * @throws IllegalArgumentException if <var>gameObject</var> isn't in this
369     * container
370     */
371    public void moveDown(@NotNull final G gameObject) {
372        final int oldIndex = contents.indexOf(gameObject);
373        if (oldIndex > 0) {
374            notifyBeginChange();
375            try {
376                if (!contents.remove(gameObject)) {
377                    throw new NotInsideContainerException(this, gameObject);
378                }
379                if (oldIndex == 1) {
380                    assert contents.size() >= 1;
381                    gameObject.propagateElevation(contents.get(0));
382                }
383                contents.add(oldIndex - 1, gameObject);
384            } finally {
385                notifyEndChange();
386            }
387        }
388    }
389
390    /**
391     * Move an item to bottom.
392     * @param gameObject item to move to bottom
393     * @throws IllegalArgumentException if <var>gameObject</var> isn't in this
394     * container
395     */
396    public void moveBottom(@NotNull final G gameObject) {
397        final int oldIndex = contents.indexOf(gameObject);
398        if (oldIndex != 0) {
399            notifyBeginChange();
400            try {
401                if (!contents.remove(gameObject)) {
402                    throw new NotInsideContainerException(this, gameObject);
403                }
404                assert contents.size() >= 1;
405                gameObject.propagateElevation(contents.get(0));
406                contents.add(0, gameObject);
407            } finally {
408                notifyEndChange();
409            }
410        }
411    }
412
413    /**
414     * Add the given GameObject at the end of this Container.
415     * @param gameObject the free yet unlinked <code>GameObject</code> to be
416     * placed in the inventory
417     * @throws IllegalArgumentException if <var>gameObject</var> already is
418     * inside another container
419     */
420    public void addLast(@NotNull final G gameObject) {
421        if (gameObject.isInContainer()) {
422            throw new IllegalArgumentException("Can't add " + gameObject + " to " + this + " because it's already inside " + gameObject.getContainer());
423        }
424
425        notifyBeginChange();
426        try {
427            contents.add(gameObject);
428            setThisContainer(gameObject);
429            gameObject.markModified();
430        } finally {
431            notifyEndChange();
432        }
433    }
434
435    /**
436     * Add the given GameObject at the end of this Container.
437     * @param gameObject the free yet unlinked <code>GameObject</code> to be
438     * placed in the inventory
439     * @throws IllegalArgumentException if <var>gameObject</var> already is
440     * inside another container
441     */
442    public void addFirst(@NotNull final G gameObject) {
443        if (gameObject.isInContainer()) {
444            throw new IllegalArgumentException("Can't add " + gameObject + " to " + this + " because it's already inside " + gameObject.getContainer());
445        }
446
447        notifyBeginChange();
448        try {
449            if (!contents.isEmpty()) {
450                gameObject.propagateElevation(contents.get(0));
451            }
452            contents.add(0, gameObject);
453            setThisContainer(gameObject);
454            gameObject.markModified();
455        } finally {
456            notifyEndChange();
457        }
458    }
459
460    /**
461     * Add a GameObject after another.
462     * @param previousGameObject previous anchor or <code>null</code> to insert
463     * last
464     * @param gameObject GameObject to insert
465     * @throws IllegalArgumentException if <var>gameObject</var> already is
466     * inside another container or <var>previousGameObject</var> isn't inside
467     * this container
468     * @noinspection TypeMayBeWeakened
469     */
470    public void insertAfter(@Nullable final G previousGameObject, @NotNull final G gameObject) {
471        if (gameObject.isInContainer()) {
472            throw new IllegalArgumentException("Can't add " + gameObject + " to " + this + " because it's already inside " + gameObject.getContainer());
473        }
474
475        if (previousGameObject == null) {
476            addLast(gameObject);
477            return;
478        }
479
480        if (!previousGameObject.isHead()) {
481            throw new IllegalArgumentException();
482        }
483
484        notifyBeginChange();
485        try {
486            boolean added = false;
487            int index = 0;
488            for (final G tmpGameObject : contents) {
489                if (tmpGameObject.getHead() == previousGameObject) {
490                    if (index == 0) {
491                        gameObject.propagateElevation(contents.get(0));
492                    }
493                    contents.add(index, gameObject);
494                    added = true;
495                    break;
496                }
497                index++;
498            }
499            if (!added) {
500                throw new IllegalArgumentException("Can't add " + gameObject + " to " + this + " because " + previousGameObject + " is not inside");
501            }
502            setThisContainer(gameObject);
503            gameObject.markModified();
504        } finally {
505            notifyEndChange();
506        }
507    }
508
509    /**
510     * Add a GameObject before another.
511     * @param gameObject GameObject to insert
512     * @param nextGameObject nextGameObject anchor or <code>null</code> to add
513     * first
514     * @throws IllegalArgumentException if <var>gameObject</var> already is
515     * inside another container or <var>prev</var> isn't inside this container
516     */
517    public void insertBefore(@NotNull final G gameObject, @Nullable final G nextGameObject) {
518        if (gameObject.isInContainer()) {
519            throw new IllegalArgumentException("Can't add " + gameObject + " to " + this + " because it's already inside " + gameObject.getContainer());
520        }
521
522        if (nextGameObject == null) {
523            addFirst(gameObject);
524            return;
525        }
526
527        if (!nextGameObject.isHead()) {
528            throw new IllegalArgumentException();
529        }
530
531        notifyBeginChange();
532        try {
533            final int insertIndex = contents.indexOf(nextGameObject);
534            if (insertIndex == -1) {
535                throw new IllegalArgumentException("Can't insert " + gameObject + " before " + nextGameObject + " because that isn't inside " + this);
536            }
537            contents.add(insertIndex + 1, gameObject);
538            setThisContainer(gameObject);
539            gameObject.markModified();
540        } finally {
541            notifyEndChange();
542        }
543    }
544
545    /**
546     * Replace an GameObject with another one.
547     * @param oldGameObject old GameObject to be replaced
548     * @param newGameObject new GameObject that replaces oldGameObject
549     * @throws IllegalArgumentException if <var>oldGameObject</var> isn't in
550     * this container or is a multi-part object
551     */
552    public void replace(@NotNull final G oldGameObject, @NotNull final G newGameObject) {
553        if (oldGameObject.isMulti()) {
554            throw new IllegalArgumentException();
555        }
556
557        notifyBeginChange();
558        try {
559            final int insertIndex = contents.indexOf(oldGameObject);
560            if (insertIndex == -1) {
561                throw new NotInsideContainerException(this, oldGameObject);
562            }
563            if (insertIndex == 0) {
564                newGameObject.propagateElevation(oldGameObject);
565            }
566            contents.remove(oldGameObject);
567            oldGameObject.setContainer(null, 0, 0);
568            contents.add(insertIndex, newGameObject);
569            setThisContainer(newGameObject);
570            newGameObject.markModified();
571        } finally {
572            notifyEndChange();
573        }
574    }
575
576    /**
577     * Returns the {@link MapSquare} of this container.
578     * @return the map square of this container or <code>null</code> if this
579     *         GameObjectContainer is not (yet?) connected to a map (a {@link
580     *         MapSquare} would return itself)
581     */
582    @Nullable
583    public abstract MapSquare<G, A, R> getMapSquare();
584
585    /**
586     * Notify the map model that this container is about to change.
587     */
588    protected abstract void notifyBeginChange();
589
590    /**
591     * Notify the map model that this container has changed.
592     */
593    protected abstract void notifyEndChange();
594
595    /**
596     * {@inheritDoc}
597     */
598    @NotNull
599    @Override
600    @SuppressWarnings("unchecked")
601    protected Object clone() {
602        try {
603            //noinspection OverriddenMethodCallDuringObjectConstruction
604            final GameObjectContainer<G, A, R> clone = (GameObjectContainer<G, A, R>) super.clone();
605            clone.initData();
606            for (final BaseObject<G, A, R, G> gameObject : contents) {
607                final G clonedGameObject = gameObject.clone();
608                clone.contents.add(clonedGameObject);
609                clone.setThisContainer(clonedGameObject);
610            }
611            return clone;
612        } catch (final CloneNotSupportedException e) {
613            assert false : "This class must be cloneable" + e;
614            throw new AssertionError(e);
615        }
616    }
617
618    // writeObject() is not required because this class doesn't require special handling during serialization.
619
620    private void readObject(@NotNull final ObjectInputStream stream) throws IOException, ClassNotFoundException {
621        stream.defaultReadObject();
622        // initialize transients
623        initData();
624    }
625
626    /**
627     * Compare this object to another game object container.
628     * @param gameObjectContainer the other game object container
629     * @return <code>true</code> if this object equals the other object
630     */
631    public boolean hasSameContents(@NotNull final GameObjectContainer<?, ?, ?> gameObjectContainer) {
632        if (gameObjectContainer.contents.size() != contents.size()) {
633            return false;
634        }
635
636        for (int i = 0; i < contents.size(); i++) {
637            if (!gameObjectContainer.contents.get(i).isEqual(contents.get(i))) {
638                return false;
639            }
640        }
641
642        return true;
643    }
644
645    /**
646     * Sets a {@link GameObject}'s container to this container.
647     * @param gameObject the game object
648     */
649    protected abstract void setThisContainer(@NotNull final G gameObject);
650
651    /**
652     * Returns this instance as a {@link GameObject} or <code>null</code> if
653     * this instance is not a game object.
654     * @return this instance or <code>null</code>
655     */
656    @Nullable
657    public abstract G asGameObject();
658
659    /**
660     * {@inheritDoc}
661     */
662    @NotNull
663    @Override
664    public String toString() {
665        final StringBuilder sb = new StringBuilder();
666        sb.append("(");
667        final Iterator<G> it = contents.iterator();
668        if (it.hasNext()) {
669            sb.append(it.next());
670            while (it.hasNext()) {
671                sb.append(",");
672                sb.append(it.next());
673            }
674        }
675        sb.append(")");
676        return sb.toString();
677    }
678
679    /**
680     * An iterator for iterating over a list in reverse order.
681     * @todo move this class to JAPI
682     */
683    private static class ReverseIterator<T extends GameObject<?, ?, ?>> implements Iterator<T> {
684
685        /**
686         * The iterator used for delegation.
687         */
688        @NotNull
689        private final ListIterator<T> delegate;
690
691        /**
692         * The list being iterated over.
693         */
694        @NotNull
695        private final List<T> list;
696
697        /**
698         * Create a reverse iterator.
699         * @param list to iterate over in reverse order
700         */
701        private ReverseIterator(@NotNull final List<T> list) {
702            this.list = list;
703            delegate = list.listIterator(list.size());
704        }
705
706        @Override
707        public boolean hasNext() {
708            return delegate.hasPrevious();
709        }
710
711        // previous can throw NoSuchElementException but InspectionGadgets doesn't know about that.
712        @SuppressWarnings("IteratorNextCanNotThrowNoSuchElementException")
713        @NotNull
714        @Override
715        public T next() {
716            return delegate.previous();
717        }
718
719        @Override
720        public void remove() {
721            if (delegate.nextIndex() == 0 && list.size() >= 2) {
722                list.get(1).propagateElevation(list.get(0));
723            }
724            delegate.remove();
725        }
726
727    }
728
729}