package com.jcwhatever.nucleus.internal.managed.signs;
import com.jcwhatever.nucleus.Nucleus;
import com.jcwhatever.nucleus.events.signs.SignInteractEvent;
import com.jcwhatever.nucleus.internal.NucMsg;
import com.jcwhatever.nucleus.managed.scheduler.Scheduler;
import com.jcwhatever.nucleus.managed.signs.ISignContainer;
import com.jcwhatever.nucleus.managed.signs.ISignManager;
import com.jcwhatever.nucleus.managed.signs.SignHandler;
import com.jcwhatever.nucleus.managed.signs.SignHandler.SignBreakResult;
import com.jcwhatever.nucleus.managed.signs.SignHandler.SignChangeResult;
import com.jcwhatever.nucleus.managed.signs.SignHandler.SignClickResult;
import com.jcwhatever.nucleus.managed.signs.SignHandler.SignHandlerRegistration;
import com.jcwhatever.nucleus.storage.IDataNode;
import com.jcwhatever.nucleus.utils.PreCon;
import com.jcwhatever.nucleus.utils.SignUtils;
import com.jcwhatever.nucleus.utils.text.TextColor;
import com.jcwhatever.nucleus.utils.text.TextFormat;
import com.jcwhatever.nucleus.utils.text.TextUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.Sign;
import org.bukkit.event.Event;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.SignChangeEvent;
import javax.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Manages handled signs.
*/
public final class InternalSignManager implements ISignManager {
private static final Pattern PATTERN_HEADER_STRIPPER = Pattern.compile("[\\[\\]]");
private static final SignHandlerRegistration REGISTRATION = new SignHandlerRegistration();
/**
* Get the data node name of a sign based on its location.
*
* @param signLocation The location of the sign.
*/
public static String getSignNodeName(Location signLocation) {
PreCon.notNull(signLocation);
String worldName = signLocation.getWorld() != null
? '_' + signLocation.getWorld().getName()
: "";
return "sign" +
signLocation.getBlockX() + '_' +
signLocation.getBlockY() + '_' +
signLocation.getBlockZ() +
worldName;
}
// keyed to sign name
private final Map<String, SignHandler> _handlerMap = new HashMap<>(30);
/**
* Constructor.
*/
public InternalSignManager() {
Bukkit.getPluginManager().registerEvents(new BukkitListener(this), Nucleus.getPlugin());
}
@Override
public boolean registerHandler(SignHandler signHandler) {
PreCon.notNull(signHandler);
SignHandler current = _handlerMap.get(signHandler.getSearchName());
if (current != null) {
NucMsg.warning(
"Failed to register sign handler. A sign named '{0}' is already " +
"registered by plugin '{1}'.",
current.getName(), current.getPlugin().getName());
return false;
}
if (signHandler.getName() == null || signHandler.getName().isEmpty()) {
throw new IllegalArgumentException("Failed to register sign handler because it has " +
"no name.");
}
if (!TextUtils.isValidName(signHandler.getName())) {
throw new IllegalArgumentException("Failed to register sign handler because it has " +
"an invalid name: " + signHandler.getName());
}
if (signHandler.getPlugin() == null) {
throw new IllegalArgumentException("Failed to register sign handler because it " +
"did not return an owning plugin.");
}
if (signHandler.getUsage() == null) {
throw new IllegalArgumentException("Failed to register sign because it's usage " +
"returns null.");
}
if (signHandler.getUsage().length != 4) {
throw new IllegalArgumentException("Failed to register sign because it's usage " +
"array has an incorrect number of elements.");
}
signHandler.onRegister(REGISTRATION);
_handlerMap.put(signHandler.getSearchName(), signHandler);
loadSigns(signHandler);
return true;
}
@Override
public boolean unregisterHandler(String name) {
PreCon.notNull(name);
SignHandler current = _handlerMap.remove(name.toLowerCase());
if (current == null)
return false;
return true;
}
@Override
public SignHandler getSignHandler(String name) {
PreCon.notNull(name);
return _handlerMap.get(name.toLowerCase());
}
@Override
public Collection<SignHandler> getSignHandlers() {
return Collections.unmodifiableCollection(_handlerMap.values());
}
@Override
public <T extends Collection<SignHandler>> T getSignHandlers(T output) {
PreCon.notNull(output);
output.addAll(_handlerMap.values());
return output;
}
@Override
public Collection<ISignContainer> getSigns(String signHandlerName) {
return getSigns(signHandlerName, new ArrayList<ISignContainer>(20));
}
@Override
public <T extends Collection<ISignContainer>> T getSigns(String signHandlerName, T output) {
PreCon.notNullOrEmpty(signHandlerName);
SignHandler handler = _handlerMap.get(signHandlerName.toLowerCase());
if (handler == null)
return output;
IDataNode handlerNode = getHandlerNode(handler);
for (IDataNode signNode : handlerNode) {
Location location = signNode.getLocation("location");
if (location == null)
continue;
ISignContainer container = new SignContainer(location, signNode);
output.add(container);
}
return output;
}
@Override
@Nullable
public String[] getSavedLines(Sign sign) {
PreCon.notNull(sign);
String handlerName = getSignHandlerName(sign.getLine(0));
SignHandler handler = _handlerMap.get(handlerName);
if (handler == null)
return null;
IDataNode signNode = getSignNode(handler, sign.getLocation());
return new String[] {
signNode.getString("line0", ""),
signNode.getString("line1", ""),
signNode.getString("line2", ""),
signNode.getString("line3", "")
};
}
@Override
public boolean restoreSign (String signHandlerName, Location location) {
PreCon.notNullOrEmpty(signHandlerName);
PreCon.notNull(location);
SignHandler handler = _handlerMap.get(signHandlerName.toLowerCase());
if (handler == null) {
NucMsg.warning(
"Failed to restore sign because a sign handler named '{0}' was not found for it.",
signHandlerName);
return false;
}
String signName = getSignNodeName(location);
IDataNode handlerNode = getHandlerNode(handler);
if (!handlerNode.hasNode(signName))
return false;
IDataNode signNode = handlerNode.getNode(signName);
final Location loc = signNode.getLocation("location");
if (loc == null) {
NucMsg.warning(handler.getPlugin(),
"Failed to restore sign because it's missing its location config property.");
return false;
}
final Material type = signNode.getEnum("type", null, Material.class);
if (type == null) {
NucMsg.warning(handler.getPlugin(),
"Failed to restore sign because it's missing its type config property.");
return false;
}
final BlockFace facing = signNode.getEnum("direction", null, BlockFace.class);
if (facing == null) {
NucMsg.warning(handler.getPlugin(),
"Failed to restore sign because it's missing its direction config property.");
return false;
}
final String line0 = handler.getHeaderPrefix() + handler.getDisplayName();
final String line1 = signNode.getString("line1");
final String line2 = signNode.getString("line2");
final String line3 = signNode.getString("line3");
loc.getBlock().setType(type);
Scheduler.runTaskLater(Nucleus.getPlugin(), new Runnable() {
@Override
public void run() {
final BlockState blockState = loc.getBlock().getState();
blockState.setType(type);
blockState.setData(SignUtils.createData(type, facing));
Sign sign = (Sign) blockState;
sign.setLine(0, line0);
sign.setLine(1, line1);
sign.setLine(2, line2);
sign.setLine(3, line3);
sign.update(true);
}
});
return true;
}
@Override
public boolean restoreSigns (String signHandlerName) {
PreCon.notNullOrEmpty(signHandlerName);
SignHandler handler = _handlerMap.get(signHandlerName.toLowerCase());
if (handler == null) {
NucMsg.warning(
"Failed to restore signs because a sign handler named '{0}' was not found.",
signHandlerName);
return false;
}
IDataNode handlerNode = getHandlerNode(handler);
final Deque<SignInfo> signInfo = new ArrayDeque<>(10);
for (IDataNode signNode : handlerNode) {
Location loc = signNode.getLocation("location");
if (loc == null) {
NucMsg.warning(handler.getPlugin(),
"Failed to restore sign because it's missing its location config property.");
continue;
}
Material type = signNode.getEnum("type", null, Material.class);
if (type == null) {
NucMsg.warning(handler.getPlugin(),
"Failed to restore sign because it's missing its type config property.");
continue;
}
BlockFace facing = signNode.getEnum("direction", null, BlockFace.class);
if (facing == null) {
NucMsg.warning(handler.getPlugin(),
"Failed to restore sign because it's missing its direction config property.");
continue;
}
String line0 = signNode.getString("line0");
String line1 = signNode.getString("line1");
String line2 = signNode.getString("line2");
String line3 = signNode.getString("line3");
BlockState blockState = loc.getBlock().getState();
blockState.setType(type);
blockState.setData(SignUtils.createData(type, facing));
blockState.update(true);
signInfo.push(new SignInfo(loc, line0, line1, line2, line3));
}
Scheduler.runTaskLater(Nucleus.getPlugin(), new Runnable() {
@Override
public void run() {
while (!signInfo.isEmpty()) {
SignInfo s = signInfo.pop();
BlockState state = s.location.getBlock().getState();
Sign sign = (Sign) state;
SignUtils.setLines(sign, s.lines);
sign.update(true);
}
}
});
return true;
}
/**
* Invoke when a sign is changed/created.
*
* @param sign The changed sign.
* @param event The sign change event.
*
* @return True if a handler is found for the sign and the event was handled.
*/
boolean signChange(Sign sign, SignChangeEvent event) {
String signName = getSignHandlerName(event.getLine(0));
SignHandler handler = _handlerMap.get(signName);
if (handler == null)
return false;
event.setLine(0, TextFormat.translateFormatChars(event.getLine(0)));
event.setLine(1, TextFormat.translateFormatChars(event.getLine(1)));
event.setLine(2, TextFormat.translateFormatChars(event.getLine(2)));
event.setLine(3, TextFormat.translateFormatChars(event.getLine(3)));
IDataNode signNode = getSignNode(handler, sign.getLocation());
SignChangeResult result = REGISTRATION.signChange(
handler,
event.getPlayer(),
new SignContainer(sign.getLocation(), signNode, event));
if (result == null)
throw new RuntimeException("A SignEventResult must be returned from SignHandler#onSignChange.");
boolean isAllowed = result == SignChangeResult.VALID;
String prefix = isAllowed ? handler.getHeaderPrefix() : "#" + TextColor.RED;
String header = prefix + handler.getDisplayName();
event.setLine(0, header);
if (isAllowed) {
Location loc = sign.getLocation();
signNode.set("location", loc);
signNode.set("line0", header.trim());
signNode.set("line1", event.getLine(1));
signNode.set("line2", event.getLine(2));
signNode.set("line3", event.getLine(3));
signNode.set("type", sign.getType().name());
signNode.set("direction", SignUtils.getFacing(sign).name());
signNode.save();
}
return true;
}
/**
* Invoke when a sign is clicked.
*
* @param event The sign interact event.
*
* @return True if a handler is found for the sign.
*/
boolean signClick(SignInteractEvent event) {
String signName = getSignHandlerName(event.getSign().getLine(0));
SignHandler handler = _handlerMap.get(signName);
if (handler == null)
return false;
IDataNode signNode = getSignNode(handler, event.getSign().getLocation());
SignClickResult result = REGISTRATION.signClick(
handler,
event.getPlayer(),
new SignContainer(event.getSign().getLocation(), signNode));
if (result == null)
throw new RuntimeException("A SignClickResult must be returned from SignHandler#onSignClick.");
if (result == SignClickResult.HANDLED) {
event.setUseItemInHand(Event.Result.DENY);
event.setCancelled(true);
}
return true;
}
/**
* Invoke when a sign is broken.
*
* @param sign The sign that was broken.
* @param event The sign break event.
*
* @return True if a handler is found for the sign.
*/
boolean signBreak(Sign sign, BlockBreakEvent event) {
String signName = getSignHandlerName(sign.getLine(0));
SignHandler handler = _handlerMap.get(signName);
if (handler == null)
return false;
IDataNode signNode = getSignNode(handler, sign.getLocation());
SignBreakResult result = REGISTRATION.signBreak(
handler,
event.getPlayer(),
new SignContainer(sign.getLocation(), signNode));
if (result == null)
throw new RuntimeException("A SignEventResult must be returned from SignHandler#onSignBreak.");
if (result == SignBreakResult.ALLOW) {
signNode.remove();
signNode.save();
}
else {
event.setCancelled(true);
}
return true;
}
// load signs for the specified handler from
// config and pass into handler.
private void loadSigns(SignHandler handler) {
IDataNode handlerNode = getHandlerNode(handler);
for (IDataNode signNode : handlerNode) {
Location location = signNode.getLocation("location");
if (location == null)
continue;
Sign sign = SignUtils.getSign(location.getBlock());
if (sign == null)
continue;
REGISTRATION.signLoad(handler, new SignContainer(sign.getLocation(), signNode));
}
}
// get a data node for a sign, assumes sign is
// already validated to the specified handler
private IDataNode getSignNode(SignHandler handler, Location signLocation) {
IDataNode handlerNode = getHandlerNode(handler);
return handlerNode.getNode(getSignNodeName(signLocation));
}
private IDataNode getHandlerNode(SignHandler handler) {
IDataNode pluginNode = REGISTRATION.getDataNode(handler);
return pluginNode.getNode(handler.getName());
}
// Get the sign handler name from the
// first line of a sign.
@Nullable
private String getSignHandlerName(String line0) {
String header = TextFormat.remove(line0);
if (header.isEmpty())
return null;
Matcher stripMatcher = PATTERN_HEADER_STRIPPER.matcher(header);
header = stripMatcher.replaceAll("").trim().toLowerCase();
Matcher spaceMatcher = TextUtils.PATTERN_SPACE.matcher(header);
return spaceMatcher.replaceAll("_");
}
static class SignInfo {
final String[] lines;
final Location location;
SignInfo(Location location, String...lines) {
this.location = location;
this.lines = lines;
}
}
}