package jalse.entities;
import static jalse.entities.Entities.getTypeAncestry;
import static jalse.entities.Entities.isSubtype;
import static jalse.tags.Tags.getRootContainer;
import static jalse.tags.Tags.getTreeDepth;
import static jalse.tags.Tags.getTreeMember;
import static jalse.tags.Tags.setCreated;
import static jalse.tags.Tags.setOriginContainer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Stream;
import jalse.actions.Action;
import jalse.actions.ActionContext;
import jalse.actions.ActionEngine;
import jalse.actions.DefaultActionScheduler;
import jalse.actions.SchedulableActionContext;
import jalse.attributes.AttributeContainer;
import jalse.attributes.AttributeListener;
import jalse.attributes.DefaultAttributeContainer;
import jalse.attributes.NamedAttributeType;
import jalse.misc.AbstractIdentifiable;
import jalse.misc.ListenerSet;
import jalse.tags.Created;
import jalse.tags.OriginContainer;
import jalse.tags.RootContainer;
import jalse.tags.Tag;
import jalse.tags.TagTypeSet;
import jalse.tags.TreeDepth;
import jalse.tags.TreeMember;
/**
* A simple yet fully featured {@link Entity} implementation.<br>
* <br>
* This entity can be marked as alive ({@link #markAsAlive()}) or dead ({@link #markAsDead()}).
*
* @author Elliot Ford
*
* @see DefaultEntityFactory
* @see DefaultEntityContainer
* @see DefaultAttributeContainer
* @see DefaultActionScheduler
* @see TagTypeSet
*
*/
public class DefaultEntity extends AbstractIdentifiable implements Entity {
/**
* Parent entity container.
*/
protected EntityContainer container;
/**
* Child entities.
*/
protected final DefaultEntityContainer entities;
/**
* Associated attributes.
*/
protected final DefaultAttributeContainer attributes;
/**
* Self action scheduler.
*/
protected final DefaultActionScheduler<Entity> scheduler;
/**
* Current state information.
*/
protected final TagTypeSet tags;
private final ListenerSet<EntityTypeListener> listeners;
private final Set<Class<? extends Entity>> types;
private final AtomicBoolean alive;
private final Lock read;
private final Lock write;
/**
* Creates a new default entity instance.
*
* @param id
* Entity ID.
* @param factory
* Entity factory for creating/killing child entities.
* @param container
* Parent entity container.
*/
protected DefaultEntity(final UUID id, final EntityFactory factory, final EntityContainer container) {
super(id);
this.container = container;
entities = new DefaultEntityContainer(factory, this);
attributes = new DefaultAttributeContainer(this);
tags = new TagTypeSet();
scheduler = new DefaultActionScheduler<>(this);
listeners = new ListenerSet<>(EntityTypeListener.class);
types = new HashSet<>();
alive = new AtomicBoolean();
final ReadWriteLock rwLock = new ReentrantReadWriteLock();
read = rwLock.readLock();
write = rwLock.writeLock();
}
@Override
public <T> boolean addAttributeListener(final NamedAttributeType<T> namedType,
final AttributeListener<T> listener) {
return attributes.addAttributeListener(namedType, listener);
}
/**
* Adds tree based tags for when a non-null container is set.
*
* @see RootContainer
* @see TreeDepth
*/
protected void addContainerTags() {
// Only add root if we aren't it
final RootContainer rc = getRootContainer(container);
if (rc != null) {
tags.add(rc);
}
final TreeDepth parentDepth = getTreeDepth(container);
tags.add(parentDepth != null ? parentDepth.increment() : TreeDepth.ROOT);
}
@Override
public boolean addEntityListener(final EntityListener listener) {
return entities.addEntityListener(listener);
}
@Override
public boolean addEntityTypeListener(final EntityTypeListener listener) {
Objects.requireNonNull(listener);
write.lock();
try {
return listeners.add(listener);
} finally {
write.unlock();
}
}
/**
* Adds the default tags.
*
* @see Created
* @see OriginContainer
* @see #addContainerTags()
*/
protected void addTags() {
setCreated(tags);
setOriginContainer(tags, container);
addContainerTags();
}
protected void addTreeMember() {
/*
* Ensure this is called before read: If this was added after creating an entity the
* listener could try and read this tag and it might not be up to date.
*/
tags.add(getTreeMember(this));
}
@Override
public void cancelAllScheduledForActor() {
scheduler.cancelAllScheduledForActor();
}
private void checkAlive() {
if (!isAlive()) {
throw new IllegalStateException(String.format("Entity %s is no longer alive", id));
}
}
@Override
public <T> void fireAttributeChanged(final NamedAttributeType<T> namedType) {
attributes.fireAttributeChanged(namedType);
}
@Override
public <T> T getAttribute(final NamedAttributeType<T> namedType) {
return attributes.getAttribute(namedType);
}
@Override
public int getAttributeCount() {
return attributes.getAttributeCount();
}
@Override
public <T> Set<? extends AttributeListener<T>> getAttributeListeners(final NamedAttributeType<T> namedType) {
return attributes.getAttributeListeners(namedType);
}
@Override
public Set<NamedAttributeType<?>> getAttributeListenerTypes() {
return attributes.getAttributeListenerTypes();
}
@Override
public Set<NamedAttributeType<?>> getAttributeTypes() {
return attributes.getAttributeTypes();
}
@Override
public EntityContainer getContainer() {
return isAlive() ? container : null;
}
/**
* Gets the associated action engine.
*
* @return Optional containing the engine or else empty optional if there is no engine
* associated.
*/
protected ActionEngine getEngine() {
return scheduler.getEngine();
}
@Override
public Entity getEntity(final UUID id) {
return entities.getEntity(id);
}
@Override
public int getEntityCount() {
return entities.getEntityCount();
}
@Override
public Set<UUID> getEntityIDs() {
return entities.getEntityIDs();
}
@Override
public Set<? extends EntityListener> getEntityListeners() {
return entities.getEntityListeners();
}
@Override
public Set<? extends EntityTypeListener> getEntityTypeListeners() {
read.lock();
try {
return new HashSet<>(listeners);
} finally {
read.unlock();
}
}
@Override
public <T extends Tag> Set<T> getTagsOfType(final Class<T> type) {
addTreeMember();
return tags.getOfType(type);
}
@Override
public boolean isAlive() {
return alive.get();
}
@Override
public boolean isMarkedAsType(final Class<? extends Entity> type) {
read.lock();
try {
return types.contains(type);
} finally {
read.unlock();
}
}
@Override
public boolean kill() {
return container.killEntity(id);
}
@Override
public void killEntities() {
entities.killEntities();
}
@Override
public boolean killEntity(final UUID id) {
return entities.killEntity(id);
}
/**
* Marks the entity as alive.
*
* @return Whether the core was alive.
* @see #addTags()
*/
protected boolean markAsAlive() {
addTags();
return alive.getAndSet(true);
}
/**
* Marks the entity as dead.
*
* @return Whether the core was alive.
*
* @see #removeContainerTags()
*/
protected boolean markAsDead() {
removeContainerTags();
return alive.getAndSet(false);
}
@Override
public boolean markAsType(final Class<? extends Entity> type) {
Objects.requireNonNull(type);
write.lock();
try {
// Add target type
if (!types.add(type)) {
return false;
}
// Add missing ancestors
final Set<Class<? extends Entity>> addedAncestors = new HashSet<>();
for (final Class<? extends Entity> at : getTypeAncestry(type)) {
if (types.add(at)) {
// Missing ancestor
addedAncestors.add(at);
}
}
// Trigger change
listeners.getProxy().entityMarkedAsType(new EntityTypeEvent(this, type, addedAncestors));
return true;
} finally {
write.unlock();
}
}
@Override
public SchedulableActionContext<Entity> newContextForActor(final Action<Entity> action) {
checkAlive();
return scheduler.newContextForActor(action);
}
@Override
public Entity newEntity(final UUID id, final AttributeContainer sourceContainer) {
checkAlive();
return entities.newEntity(id, sourceContainer);
}
@Override
public <T extends Entity> T newEntity(final UUID id, final Class<T> type,
final AttributeContainer sourceContainer) {
checkAlive();
return entities.newEntity(id, type, sourceContainer);
}
@Override
public boolean receiveEntity(final Entity e) {
return entities.receiveEntity(e);
}
@Override
public <T> T removeAttribute(final NamedAttributeType<T> namedType) {
return attributes.removeAttribute(namedType);
}
@Override
public <T> boolean removeAttributeListener(final NamedAttributeType<T> namedType,
final AttributeListener<T> listener) {
return attributes.removeAttributeListener(namedType, listener);
}
@Override
public void removeAttributeListeners() {
attributes.removeAttributeListeners();
}
@Override
public <T> void removeAttributeListeners(final NamedAttributeType<T> namedType) {
attributes.removeAttributeListeners(namedType);
}
@Override
public void removeAttributes() {
attributes.removeAttributes();
}
/**
* Removes tree based tags for when a null container is set.
*
* @see TreeMember
* @see RootContainer
* @see TreeDepth
*/
protected void removeContainerTags() {
tags.removeOfType(TreeMember.class);
tags.removeOfType(RootContainer.class);
tags.removeOfType(TreeDepth.class);
}
@Override
public boolean removeEntityListener(final EntityListener listener) {
return entities.removeEntityListener(listener);
}
@Override
public void removeEntityListeners() {
entities.removeEntityListeners();
}
@Override
public boolean removeEntityTypeListener(final EntityTypeListener listener) {
Objects.requireNonNull(listener);
write.lock();
try {
return listeners.remove(listener);
} finally {
write.unlock();
}
}
@Override
public void removeEntityTypeListeners() {
write.lock();
try {
listeners.clear();
} finally {
write.unlock();
}
}
@Override
public ActionContext<Entity> scheduleForActor(final Action<Entity> action, final long initialDelay,
final long period, final TimeUnit unit) {
checkAlive();
return scheduler.scheduleForActor(action, initialDelay, period, unit);
}
@Override
public <T> T setAttribute(final NamedAttributeType<T> namedType, final T attr) {
return attributes.setAttribute(namedType, attr);
}
/**
* Sets the parent container for the entity.
*
* @param container
* New parent container (can be null);
*/
protected void setContainer(final EntityContainer container) {
if (!Objects.equals(this.container, container)) {
this.container = container;
if (!isAlive()) {
return;
}
// Fix container based tags
if (container == null) {
removeContainerTags();
} else {
addContainerTags();
}
}
}
/**
* Associates an engine to the entity for scheduling actions.
*
* @param engine
* Engine to set.
*/
protected void setEngine(final ActionEngine engine) {
scheduler.setEngine(engine);
}
@Override
public Stream<?> streamAttributes() {
return attributes.streamAttributes();
}
@Override
public Stream<Entity> streamEntities() {
return entities.streamEntities();
}
@Override
public Stream<Class<? extends Entity>> streamMarkedAsTypes() {
read.lock();
try {
return new ArrayList<>(types).stream();
} finally {
read.unlock();
}
}
@Override
public Stream<Tag> streamTags() {
addTreeMember();
return tags.stream();
}
@Override
public boolean transfer(final EntityContainer destination) {
return container.transferEntity(id, destination);
}
@Override
public boolean transferEntity(final UUID id, final EntityContainer destination) {
return entities.transferEntity(id, destination);
}
@Override
public void unmarkAsAllTypes() {
write.lock();
try {
new ArrayList<>(types).forEach(this::unmarkAsType);
} finally {
write.unlock();
}
}
@Override
public boolean unmarkAsType(final Class<? extends Entity> type) {
Objects.requireNonNull(type);
write.lock();
try {
// Remove target type
if (!types.remove(type)) {
return false;
}
// Remove descendants
final Set<Class<? extends Entity>> removedDescendants = new HashSet<>();
final Iterator<Class<? extends Entity>> it = types.iterator();
while (it.hasNext()) {
final Class<? extends Entity> dt = it.next();
if (isSubtype(dt, type)) {
// Removed descendant
removedDescendants.add(dt);
it.remove();
}
}
// Trigger change
listeners.getProxy().entityUnmarkedAsType(new EntityTypeEvent(this, type, removedDescendants));
return true;
} finally {
write.unlock();
}
}
}