package org.gbcpainter.loaders.level;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
import org.gbcpainter.env.GameSettings;
import org.gbcpainter.game.levels.Level;
import org.gbcpainter.game.model.Monster;
import org.gbcpainter.game.model.Player;
import org.gbcpainter.game.model.grid.Junction;
import org.gbcpainter.game.model.grid.Pipe;
import org.gbcpainter.game.levels.SimpleLevelImpl;
import org.gbcpainter.geom.ImmutableSegment2D;
import org.gbcpainter.geom.Segment;
import org.gbcpainter.loaders.level.parsers.AssertValidGraphParser;
import org.gbcpainter.loaders.level.parsers.FileLevelParser;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jgrapht.EdgeFactory;
import org.jgrapht.graph.SimpleGraph;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.gbcpainter.loaders.level.LOADING_STATUS.*;
/**
* Implementation of the {@link org.gbcpainter.loaders.level.LevelLoader} interface.
* <p/>
* It loads the levels from the folder defined by {@link org.gbcpainter.env.GameSettings.STRING_SETTINGS_TYPE#LEVELS_PATH}. It uses a {@link
* org.gbcpainter.loaders.level.parsers.AssertValidGraphParser} to load the level graph.
* <p/>
* Then initializes a {@link org.gbcpainter.game.levels.SimpleLevelImpl} with elements generated from a {@link org.gbcpainter.loaders.level.IGameElementFactory} Player has {@link
* #DEFAULT_START_LIVES} lives.
*
* @author Lorenzo Pellegrini
*/
@ThreadSafe
public class SimpleLevelLoader<P extends Player, M extends Monster, J extends Junction, E extends Pipe> implements LevelLoader {
public static final int DEFAULT_START_LIVES = 5;
private final IGameElementFactory<P, M, J, E> factory;
@GuardedBy( "acquireMutex()" )
private final Set<LevelLoadObserver> observers = new HashSet<>( 1 );
private final Object mutex = new Object();
@GuardedBy ("acquireMutex()")
private volatile LOADING_STATUS status;
@Nullable
@GuardedBy ("acquireMutex()")
private volatile Level loadResult;
@GuardedBy ("acquireMutex()")
private volatile boolean alreadyStarted = false;
@GuardedBy ("acquireMutex()")
private volatile Exception failException = null;
/**
* Creates a level loaders given a element facotry whose status is {@link LOADING_STATUS#NOT_STARTED}
*
* @param factory The factory that will be used to generate the level elements
*/
public SimpleLevelLoader( @NotNull IGameElementFactory<P, M, J, E> factory ) {
this.factory = factory;
this.status = NOT_STARTED;
this.loadResult = null;
}
@NotNull
@Override
public Level loadLevel( @NotNull @NonNls final String name ) throws IllegalStateException, LevelNotFoundException, IOException, ParsingFailException {
checkStartable();
final Level result;
try {
result = doLoading( name );
} catch ( Exception e ) {
synchronized (acquireMutex()) {
failException = e;
setStatus( FAIL );
}
throw e;
}
synchronized (acquireMutex()) {
loadResult = result;
setStatus( LOADING_STATUS.SUCCESS );
}
return result;
}
@Override
public void startLoading( @NotNull @NonNls final String name,
@Nullable LevelLoadObserver observer ) throws IllegalStateException {
checkStartable();
if ( observer != null ) {
addObserver( observer );
}
new Thread( new Runnable() {
@Override
public void run() {
try {
final Level result = doLoading( name );
synchronized (acquireMutex()) {
loadResult = result;
setStatus( LOADING_STATUS.SUCCESS );
}
} catch ( Exception e ) {
synchronized (acquireMutex()) {
failException = e;
setStatus( FAIL );
}
}
}
} ).start();
}
@Override
public LOADING_STATUS getLoadStatus() {
synchronized (acquireMutex()) {
return this.status;
}
}
@Nullable
@Override
public Exception getLoadingException() {
synchronized (acquireMutex()) {
return this.failException;
}
}
@NotNull
@Override
public Level getResult() throws IllegalStateException {
final Level actualResult;
synchronized (acquireMutex()) {
actualResult = this.loadResult;
}
if ( actualResult == null ) {
throw new IllegalStateException();
}
return actualResult;
}
@Override
public void addObserver( @NotNull final LevelLoadObserver observer ) {
synchronized (acquireMutex()) {
this.observers.add( observer );
}
}
@Override
public void removeObserver( @NotNull final LevelLoadObserver observer ) {
synchronized (acquireMutex()) {
this.observers.remove( observer );
}
}
/**
* Esegue l'effettivo caricamento del livello.
*
* @param name Il nome del livello da caricare.
*
* @return Il livello caricato.
*
* @throws LevelNotFoundException Se il livello desiderato non è stato trovato.
* @throws IOException Se c'è stato un errore di lettura del file.
* @throws ParsingFailException Se è stato impossibile effettuare il parsing del file.
*/
@NotNull
private Level doLoading( @NotNull @NonNls final String name ) throws LevelNotFoundException, IOException, ParsingFailException {
setStatus( SEARCH_FILE );
File levelsPath = GameSettings.getRelativeDirectory( GameSettings.getInstance().getValue( GameSettings.STRING_SETTINGS_TYPE.LEVELS_PATH ) );
File levelFile = new File( levelsPath, name );
if ( ! levelFile.canRead() ) {
throw new LevelNotFoundException( "Couldn't find level: " +
levelFile.getAbsolutePath() );
}
Path path = Paths.get( levelFile.getAbsolutePath() );
setStatus( LOAD_FILE );
ByteBuffer buffer = ByteBuffer.wrap( Files.readAllBytes( path ) );
setStatus( PARSE_DATA );
FileLevelParser parser = new AssertValidGraphParser();
parser.parseData( buffer );
SimpleGraph<Point, Segment> levelMap = parser.getLevelMap();
Map<Integer, Set<Segment>> rawFacesMap = parser.getFacesMap();
Point initialPlayerPosition = parser.getPlayerPosition();
Map<String, java.util.List<Point>> monstersPositions = parser.getMonstersPosition();
/* Creo i dati di gioco */
setStatus( DEFINE_LEVEL );
Player player = factory.createPlayer( initialPlayerPosition );
Set<Monster> initializedMonsters = new HashSet<>();
for (Map.Entry<String, java.util.List<Point>> monstersEntry : monstersPositions.entrySet()) {
for (Point mobPosition : monstersEntry.getValue()) {
initializedMonsters.add( factory.createMonster( monstersEntry.getKey(), mobPosition ) );
}
}
final SimpleGraph<Junction, Pipe> pipeGrid = new SimpleGraph<>( new EdgeFactory<Junction, Pipe>() {
@Override
public E createEdge( final Junction sourceVertex, final Junction targetVertex ) {
return factory.createEdge( new ImmutableSegment2D( sourceVertex.getPosition(), targetVertex.getPosition() ) );
}
} );
final Map<Point, Junction> pointToJunction = new HashMap<>( levelMap.vertexSet().size() );
for (Point vertex : levelMap.vertexSet()) {
final J actualVertex = factory.createJunction( vertex, levelMap.edgesOf( vertex ) );
pipeGrid.addVertex( actualVertex );
pointToJunction.put( vertex, actualVertex );
}
final Map<Segment, Pipe> segmentToPipe = new HashMap<>( levelMap.edgeSet().size() );
for (Segment edge : levelMap.edgeSet()) {
Junction vertexA = pointToJunction.get( levelMap.getEdgeSource( edge ) );
Junction vertexB = pointToJunction.get( levelMap.getEdgeTarget( edge ) );
segmentToPipe.put( edge, pipeGrid.addEdge( vertexA, vertexB ) );
}
final Map<Set<Pipe>, Integer> faceMap = new HashMap<>( rawFacesMap.size() );
for (Map.Entry<Integer, Set<Segment>> faceEntry : rawFacesMap.entrySet()) {
final Set<Pipe> toEdges = new HashSet<>( faceEntry.getValue().size() );
final Integer actualFace = faceEntry.getKey();
for (Segment perimeterSegment : faceEntry.getValue()) {
toEdges.add( segmentToPipe.get( perimeterSegment ) );
}
faceMap.put( toEdges, actualFace );
}
setStatus( INIT_LEVEL );
return new SimpleLevelImpl( player, pipeGrid, faceMap, DEFAULT_START_LIVES, 0, initializedMonsters );
}
@NotNull
private Object acquireMutex() {
return mutex;
}
private void checkStartable() throws IllegalStateException {
synchronized (acquireMutex()) {
if ( alreadyStarted ) {
throw new IllegalStateException();
}
alreadyStarted = true;
}
}
private void setStatus( @NotNull LOADING_STATUS status ) {
synchronized (acquireMutex()) {
this.status = status;
for(LevelLoadObserver observer : this.observers) {
observer.loadStatusUpdated( this );
}
if(status.hasFinished()) {
this.observers.clear();
}
}
}
@NonNls
@Override
public String toString() {
synchronized (acquireMutex()) {
return "SimpleLevelLoader{" +
"factory=" + factory +
", status=" + status +
", loadResult=" + loadResult +
", alreadyStarted=" + alreadyStarted +
", failException=" + failException +
'}';
}
}
}