package org.gbcpainter.loaders.level.parsers;
import net.jcip.annotations.NotThreadSafe;
import org.gbcpainter.geom.ImmutableSegment2D;
import org.gbcpainter.geom.Segment;
import org.gbcpainter.loaders.level.ParsingFailException;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jgrapht.EdgeFactory;
import org.jgrapht.graph.SimpleGraph;
import java.awt.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.InvalidMarkException;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.*;
import java.util.List;
import java.util.regex.Pattern;
/**
* Implementation of the {@link org.gbcpainter.loaders.level.parsers.FileLevelParser}.
* <p/>
* The algorithm asserts that the level described in the file is valid (connected, without overlapping elements, with junctions with at least 2 connections, etc)
* <p/>
* The format of the file is: <ol> <li> Position of the player in the format {@code x1, y1} that is, the coordinates (integers) in base ten separated by a comma </li> <li> Line
* break </li> <li> Definitions of pipes an monsters. They can be mixed and must be of the following formats: <ul> <li> {@code x1, y1, x2, y2, faceA, faceB} Defines a pipe. These
* are the coordinates (integers) in base ten separated by a comma of the 2 extremities followed by faceA and faceB, that is, 2 integers identifying the faces' perimeter this pipe
* belongs
* <p/>
* If the pipe is external faceB can be omitted ({@code x1, y1, x2, y2, faceA}) </li> <li> {@code monster, x1, y1} Defines a monster. "monster" is the name of the monster. It must
* be a valid monster name. x1,y1 are the coordinates (integers) in base ten separated by a comma of the monster </li> </ul> </li> </ol>
*
* @author Lorenzo Pellegrini
*/
@NotThreadSafe
public class AssertValidGraphParser implements FileLevelParser {
/* Start 'format' variables */
@NonNls
private static final String DIGITS_REGEX = "\\d+";
@NonNls
private static final String FACE_REGEX = DIGITS_REGEX;
@NonNls
private static final String X_COORDINATE_REGEX = DIGITS_REGEX;
@NonNls
private static final String Y_COORDINATE_REGEX = DIGITS_REGEX;
@NonNls
private static final String LINE_DELIMITER_REGEX = "[\\r\\n]+";
/* Start inspection generated patterns */
private static final Pattern LINE_DELIMITER_PATTERN = Pattern.compile( LINE_DELIMITER_REGEX );
/* End 'format' variables */
@NonNls
private static final String[] CHARSET_NAMES = { "UTF-8", "UTF-16", "ISO-8859-1", "US-ASCII" };
@NonNls
private static final String DELIMITER = ",";
@NonNls
private static final String VALID_PIPE_LINE_REGEX = "^" + X_COORDINATE_REGEX + DELIMITER +
Y_COORDINATE_REGEX + DELIMITER +
X_COORDINATE_REGEX + DELIMITER +
Y_COORDINATE_REGEX + DELIMITER +
FACE_REGEX + DELIMITER +
FACE_REGEX + "$";
private static final Pattern VALID_PIPE_LINE_PATTERN = Pattern.compile( VALID_PIPE_LINE_REGEX );
@NonNls
private static final String VALID_EXTERNAL_PIPE_LINE_REGEX = "^" + X_COORDINATE_REGEX + DELIMITER +
Y_COORDINATE_REGEX + DELIMITER +
X_COORDINATE_REGEX + DELIMITER +
Y_COORDINATE_REGEX + DELIMITER +
FACE_REGEX + "$";
private static final Pattern VALID_EXTERNAL_PIPE_LINE_PATTERN = Pattern.compile( VALID_EXTERNAL_PIPE_LINE_REGEX );
@NonNls
private static final String VALID_MONSTER_LINE_REGEX = "^[a-zA-Z]+" + DELIMITER +
X_COORDINATE_REGEX + DELIMITER + Y_COORDINATE_REGEX + "$";
private static final Pattern VALID_MONSTER_LINE_PATTERN = Pattern.compile( VALID_MONSTER_LINE_REGEX );
@NonNls
private static final String VALID_PLAYER_POSITION_REGEX = "^" + X_COORDINATE_REGEX + DELIMITER + Y_COORDINATE_REGEX + "$";
private static final Pattern VALID_PLAYER_POSITION_PATTERN = Pattern.compile( VALID_PLAYER_POSITION_REGEX );
private static final Pattern SPACE_PATTERN = Pattern.compile( "\\s" );
/* End inspection generated patterns */
/* Start constants for the format */
/**
* Number of tokens {@code x1, y1, x2, y2, faceA, faceB} (pipe definition line) has
*/
private static final int PIPE_TOKENS = 6;
/**
* Number of tokens {@code x1, y1, x2, y2, faceA} (external pipe definition line) has
*/
private static final int EXTERNAL_PIPE_TOKENS = 5;
/**
* Internal id of the external face
*/
private static final int EXTERNAL_FACE_PIPE_ID = - 1;
/**
* Defines in which position of the line the X coordinate of the player point is.
*
* By the fact that the player position format is {@code x, y}, the index is 0
*/
private static final int PLAYER_X_TOKEN = 0;
/**
* Defines in which position of the line the X coordinate of the player point is.
*
* By the fact that the player position format is {@code x, y}, the index is 1
*/
private static final int PLAYER_Y_TOKEN = 1;
/* Start charset decoder initialization */
private static final List<CharsetDecoder> CHARSET_DECODERS;
static {
Set<CharsetDecoder> result = new LinkedHashSet<>( CHARSET_NAMES.length );
Map<String, Charset> available = Charset.availableCharsets();
for (String cName : CHARSET_NAMES) {
Charset got = available.get( cName );
if ( got != null ) {
result.add( got.newDecoder() );
}
}
CHARSET_DECODERS = new ArrayList<>( result );
}
/* End charset decoder initialization */
/* Start loaded data */
private final SimpleGraph<Point, Segment> lineGraph = new SimpleGraph<>( new EdgeFactory<Point, Segment>() {
@Override
public Segment createEdge( final Point sourceVertex, final Point targetVertex ) {
return new ImmutableSegment2D( sourceVertex, targetVertex );
}
} );
private final Map<Integer, Set<Segment>> facesMap = new HashMap<>( 0 );
private boolean loaded = false;
private Point playerPosition = new Point();
private Map<String, List<Point>> monstersPosition = new HashMap<>( 0 );
/* End loaded data */
/**
* Returns a list of charsets supported by the parser
*
* @return A names list of {@link java.nio.charset.Charset}
*/
@NotNull
public static List<String> getSupportedCharsets() {
return new ArrayList<>( Arrays.asList( CHARSET_NAMES ) );
}
private synchronized static CharBuffer doDecode( @NotNull ByteBuffer buffer ) throws ParsingFailException {
boolean success = false;
CharBuffer parsingResult = null;
Exception lastException = null;
for (CharsetDecoder decoder : CHARSET_DECODERS) {
try {
parsingResult = decoder.decode( buffer );
success = true;
break;
} catch ( IllegalStateException | CharacterCodingException notTheRightDecoder ) {
lastException = notTheRightDecoder;
try {
buffer.reset();
} catch ( InvalidMarkException noMark ) {
buffer.clear();
}
}
}
if ( ! success ) {
throw new ParsingFailException( "Can't find a valid decoder", lastException );
}
return parsingResult;
}
private static Point parsePlayerPositionLine( @NotNull @NonNls String line ) throws NumberFormatException {
String[] values = line.split( DELIMITER );
int parsedY;
int parsedX = Integer.parseInt( values[PLAYER_X_TOKEN] );
if ( parsedX < 0 ) {
throw new NumberFormatException( "Negative number" );
}
parsedY = Integer.parseInt( values[PLAYER_Y_TOKEN] );
if ( parsedY < 0 ) {
throw new NumberFormatException( "Negative number" );
}
return new Point( parsedX, parsedY );
}
private static int[] parsePipeLine( @NotNull @NonNls String line ) throws ParsingFailException {
int parsedInt;
int tokenIndex = 0;
int[] lineParsingRawData = new int[PIPE_TOKENS];
StringTokenizer tokenizer = new StringTokenizer( line, DELIMITER );
while ( tokenizer.hasMoreElements() ) {
if ( tokenIndex == PIPE_TOKENS ) {
throw new ParsingFailException( "Too many tokens" );
}
String token = tokenizer.nextToken();
try {
parsedInt = Integer.parseInt( token );
if ( parsedInt < 0 ) {
throw new NumberFormatException( "Negative number" );
}
} catch ( NumberFormatException e ) {
throw new ParsingFailException( e );
}
lineParsingRawData[tokenIndex] = parsedInt;
tokenIndex++;
}
if ( tokenIndex != PIPE_TOKENS ) {
if ( tokenIndex == EXTERNAL_PIPE_TOKENS ) {
lineParsingRawData[PIPE_TOKENS - 1] = EXTERNAL_FACE_PIPE_ID;
} else {
throw new ParsingFailException( "Not enough tokens: " + line );
}
}
return lineParsingRawData;
}
/**
* Utility method that parses a monster line
*
* @param line The line of file to parse
*
* @return A pair monster name: position
*
* @throws ParsingFailException If the line is not valid
*/
private static Map.Entry<String, Point> parseMonsterLine( @NotNull @NonNls String line ) throws ParsingFailException {
String monsterName;
Point monsterPosition = new Point();
StringTokenizer tokenizer = new StringTokenizer( line, DELIMITER );
monsterName = tokenizer.nextToken();
String token = tokenizer.nextToken();
try {
monsterPosition.x = Integer.parseInt( token );
if ( monsterPosition.x < 0 ) {
throw new NumberFormatException( "Negative number" );
}
} catch ( NumberFormatException e ) {
throw new ParsingFailException( e );
}
token = tokenizer.nextToken();
try {
monsterPosition.y = Integer.parseInt( token );
if ( monsterPosition.y < 0 ) {
throw new NumberFormatException( "Negative number" );
}
} catch ( NumberFormatException e ) {
throw new ParsingFailException( e );
}
return new AbstractMap.SimpleEntry<>( monsterName, monsterPosition );
}
@Override
public void parseData( @NotNull final ByteBuffer buffer ) throws ParsingFailException {
//Decode the byte buffer and convert it to string
CharBuffer parsingResult = doDecode( buffer );
String realData = parsingResult.toString();
//Splits the file in lines
List<String> lines = Arrays.asList( LINE_DELIMITER_PATTERN.split( realData ) );
if ( lines.isEmpty() ) {
throw new ParsingFailException( "Empty file" );
}
//Find player position first
final Iterator<String> skipFirstLineTrick = lines.iterator();
String firstLine;
try {
do {
firstLine = skipFirstLineTrick.next();
//Remove spaces
firstLine = SPACE_PATTERN.matcher( firstLine ).replaceAll( "" );
} while ( ! VALID_PLAYER_POSITION_PATTERN.matcher( firstLine ).matches() );
} catch ( NoSuchElementException e ) {
//If player position line couldn't be found...
throw new ParsingFailException( "Can't find a valid player position", e );
}
try {
final Point validPosition = parsePlayerPositionLine( firstLine );
this.playerPosition.x = validPosition.x;
this.playerPosition.y = validPosition.y;
} catch ( NumberFormatException e ) {
throw new ParsingFailException( e );
}
/* End of the parsing of the initial player position */
/* Parse the other lines */
while ( skipFirstLineTrick.hasNext() ) {
@NonNls String nextLine = skipFirstLineTrick.next();
nextLine = SPACE_PATTERN.matcher( nextLine ).replaceAll( "" );
if ( nextLine.isEmpty() ) {
continue;
}
if ( VALID_PIPE_LINE_PATTERN.matcher( nextLine ).matches() || VALID_EXTERNAL_PIPE_LINE_PATTERN.matcher( nextLine ).matches() ) {
/* The line contains the definition of a Pipe */
int[] lineParsingRawData = parsePipeLine( nextLine );
Point first = new Point( lineParsingRawData[0], lineParsingRawData[1] );
Point second = new Point( lineParsingRawData[2], lineParsingRawData[3] );
int firstFace = lineParsingRawData[4];
int secondFace = lineParsingRawData[5];
this.lineGraph.addVertex( first );
this.lineGraph.addVertex( second );
Segment createdSegment = this.lineGraph.addEdge( first, second );
Set<Segment> faceSet;
/*
If firstFace or secondFace are EXTERNAL_FACE_PIPE_ID,
this pipe is an external pipe.
*/
assert !(firstFace == EXTERNAL_FACE_PIPE_ID && secondFace == EXTERNAL_FACE_PIPE_ID);
if ( firstFace != EXTERNAL_FACE_PIPE_ID ) {
faceSet = this.facesMap.get( firstFace );
if ( faceSet == null ) {
//This face wasn't declared in the file yet
faceSet = new HashSet<>( 1 );
this.facesMap.put( firstFace, faceSet );
}
faceSet.add( createdSegment );
}
if ( secondFace != EXTERNAL_FACE_PIPE_ID ) {
faceSet = this.facesMap.get( secondFace );
if ( faceSet == null ) {
//This face wasn't declared in the file yet
faceSet = new HashSet<>( 1 );
this.facesMap.put( secondFace, faceSet );
}
faceSet.add( createdSegment );
}
} else if ( VALID_MONSTER_LINE_PATTERN.matcher( nextLine ).matches() ) {
/* This line contains data abount the initial position of a monster */
Map.Entry<String, Point> parsed = parseMonsterLine( nextLine );
List<Point> monsterList = this.monstersPosition.get( parsed.getKey() );
if ( monsterList == null ) {
/* A monster with the same name was not added yet */
monsterList = new LinkedList<>();
this.monstersPosition.put( parsed.getKey(), monsterList );
}
monsterList.add( parsed.getValue() );
}
}
this.loaded = true;
}
@NotNull
@Override
public SimpleGraph<Point, Segment> getLevelMap() throws IllegalStateException {
this.checkLoaded();
return this.lineGraph;
}
@NotNull
@Override
public Map<Integer, Set<Segment>> getFacesMap() throws IllegalStateException {
this.checkLoaded();
return this.facesMap;
}
@NotNull
@Override
public Point getPlayerPosition() throws IllegalStateException {
this.checkLoaded();
return this.playerPosition;
}
@NotNull
@Override
public Map<String, List<Point>> getMonstersPosition() throws IllegalStateException {
this.checkLoaded();
return this.monstersPosition;
}
private void checkLoaded() throws IllegalStateException {
if ( ! this.loaded ) {
throw new IllegalStateException( "Level not loaded" );
}
}
@Override
public String toString() {
return "AssertValidGraphParser{" +
"lineGraph=" + this.lineGraph +
", facesMap=" + this.facesMap +
", loaded=" + this.loaded +
", playerPosition=" + this.playerPosition +
", monstersPosition=" + this.monstersPosition +
'}';
}
}