package org.drooms.impl;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.UndirectedSparseGraph;
import edu.uci.ics.jung.graph.util.Graphs;
import org.drooms.api.Edge;
import org.drooms.api.Node;
import org.drooms.api.Node.Type;
import org.drooms.api.Playground;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.*;
import java.util.stream.Collectors;
class DefaultPlayground implements Playground {
private static final char WALL_SIGN = '#';
private static final char PLAYER_SIGN = '@';
private final Map<Node, Character> portals = new HashMap<>();
private final List<Node[]> nodeLocations = new ArrayList<>();
private final Graph<Node, Edge> graph = new UndirectedSparseGraph<>();
private final List<Node> startingNodes = new ArrayList<>();
private final int width;
private final String name;
DefaultPlayground(final String name, final List<String> lines) {
this.name = name;
// portal data
final Map<Character, Node> portalEntries = new TreeMap<>();
final Map<Character, Node> portalExits = new TreeMap<>();
// assemble nodes
int maxX = Integer.MIN_VALUE;
final Collection<Node> identifiedNodes = new HashSet<>();
for (final String line : lines) {
int y = this.nodeLocations.size();
final Node[] locations = new Node[line.length()];
for (int x = 0; x < line.length(); x++) {
final char nodeLabel = line.charAt(x);
Node n;
switch (nodeLabel) {
case WALL_SIGN: // wall node
n = new DefaultNode(Type.WALL, x, y);
break;
case PLAYER_SIGN: // player starting position
n = new DefaultNode(Type.STARTING_POSITION, x, y);
this.startingNodes.add(n);
break;
case ' ': // regular node
n = new DefaultNode(x, y);
break;
default: // any other character is a portal
n = new DefaultNode(Type.PORTAL, x, y);
if (portalEntries.containsKey(nodeLabel)) {
if (portalExits.containsKey(nodeLabel)) {
throw new IllegalStateException("Portal " + nodeLabel + " appears more than twice!");
} else {
portalExits.put(nodeLabel, n);
}
} else {
portalEntries.put(nodeLabel, n);
}
}
identifiedNodes.add(n);
locations[x] = n;
maxX = Math.max(maxX, x);
}
this.nodeLocations.add(locations);
y++;
}
this.width = maxX + 1;
// link nodes
for (final Node n : identifiedNodes) {
if (n.getType() == Type.WALL) {
// don't link wall node to any other node
continue;
}
final int y = n.getY();
final int x = n.getX();
// link upwards
if (y > 0) {
this.link(x, y, x, y - 1);
}
// link downwards
if (y < this.nodeLocations.size() - 1) {
this.link(x, y, x, y + 1);
}
final Node[] nodes = this.nodeLocations.get(y);
// link to the left
if (x > 0) {
this.link(x, y, x - 1, y);
}
// link to the right
if (x < nodes.length - 1) {
this.link(x, y, x + 1, y);
}
}
// link portals
for (final Map.Entry<Character, Node> entries : portalEntries.entrySet()) {
final Character key = entries.getKey();
if (!portalExits.containsKey(key)) {
throw new IllegalStateException("Portal " + key + " has no opposite end.");
}
final Node entry = entries.getValue();
final Node exit = portalExits.get(key);
this.link(entry.getX(), entry.getY(), exit.getX(), exit.getY());
this.portals.put(entry, key);
this.portals.put(exit, key);
}
}
@Override
public Graph<Node, Edge> getGraph() {
return Graphs.unmodifiableGraph(this.graph);
}
@Override
public int getHeight() {
return this.nodeLocations.size();
}
@Override
public String getName() {
return this.name;
}
@Override
public List<Node> getStartingPositions() {
return Collections.unmodifiableList(this.startingNodes);
}
@Override
public int getWidth() {
return this.width;
}
@Override
public boolean isAvailable(final int x, final int y) {
return (this.getNodeAt(x, y).getType() == Type.WALL) ? false : true;
}
private Edge link(final int x, final int y, final int otherX, final int otherY) {
if (!this.isAvailable(x, y) || !this.isAvailable(otherX, otherY)) {
return null;
}
final Node node1 = this.getNodeAt(x, y);
final Node node2 = this.getNodeAt(otherX, otherY);
Edge e = this.graph.findEdge(node1, node2);
if (e == null) {
e = new DefaultEdge(node1, node2);
this.graph.addEdge(e, node1, node2);
}
return e;
}
/**
* Write out the playground into a stream.
*
* @param s
* The stream
* @throws IOException
* In case the stream cannot be written.
*/
public void write(final OutputStream s) throws IOException {
try (final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s, "UTF-8"))) {
for (final Node[] line : this.nodeLocations) {
for (final Node n : line) {
switch (n.getType()) {
case WALL:
bw.append(DefaultPlayground.WALL_SIGN);
break;
case STARTING_POSITION:
bw.append(DefaultPlayground.PLAYER_SIGN);
break;
case PORTAL:
bw.append(this.portals.get(n));
break;
default:
bw.append(' ');
}
}
bw.newLine();
}
}
}
@Override
public Node getNodeAt(final int x, final int y) {
if (y < 0 || y >= this.nodeLocations.size()) {
return new DefaultNode(Type.WALL, x, y);
}
final Node[] nodes = this.nodeLocations.get(y);
if (x < 0 || x >= nodes.length) {
return new DefaultNode(Type.WALL, x, y);
}
return this.nodeLocations.get(y)[x];
}
@Override
public Node getOtherEndOfPortal(final Node portal) {
if (portal.getType() != Type.PORTAL) {
throw new IllegalArgumentException("Node not a portal: " + portal);
} else if (!this.portals.containsKey(portal)) {
throw new IllegalArgumentException("Unknown portal:" + portal);
}
final Character portalId = this.portals.get(portal);
final List<Node> possiblePortalEnds = this.portals.keySet().stream().filter(p -> portalId.equals(this.portals.get(p)))
.filter(p -> !p.equals(portal)).collect(Collectors.toList());
if (possiblePortalEnds.size() != 1) {
throw new IllegalStateException("Cannot find portal. This should not be possible at this point.");
} else {
return possiblePortalEnds.get(0);
}
}
}