package me.desht.scrollingmenusign.views;
import me.desht.dhutils.Debugger;
import me.desht.dhutils.MiscUtil;
import me.desht.dhutils.PersistableLocation;
import me.desht.dhutils.Str;
import me.desht.dhutils.block.BlockUtil;
import me.desht.scrollingmenusign.SMSException;
import me.desht.scrollingmenusign.SMSMenu;
import me.desht.scrollingmenusign.ScrollingMenuSign;
import me.desht.scrollingmenusign.enums.ViewJustification;
import org.apache.commons.lang.StringUtils;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.material.Sign;
import java.util.*;
import java.util.Map.Entry;
/**
* This view draws menus on a rectangular array of signs.
*/
public class SMSMultiSignView extends SMSGlobalScrollableView {
private BlockFace facing;
private PersistableLocation topLeft;
private PersistableLocation bottomRight;
private int height; // in blocks
private int width; // in blocks
private final Map<Location, String[]> updates = new HashMap<Location, String[]>();
/**
* Create a new multi-sign view object with no registered location. A location
* which contains a sign must be added with @see #addLocation(Location) before
* this view is useful.
*
* @param name Unique name for this view.
* @param menu The SMSMenu object to attach this view to.
*/
public SMSMultiSignView(String name, SMSMenu menu) {
super(name, menu);
this.setMaxLocations(100); // arbitrary maximum
}
/**
* Create a new multi-sign view at loc. The wall signs around loc will be scanned to work out just
* what signs comprise this view.
*
* @param name name of the new view
* @param menu menu for the new view
* @param loc location of the new view
* @throws SMSException if there was any problem creating the view
*/
public SMSMultiSignView(String name, SMSMenu menu, Location loc) throws SMSException {
this(name, menu);
scanForSigns(loc);
for (Block b : getBlocks()) {
addLocation(b.getLocation());
}
}
/**
* Create a new sign view object. Equivalent to calling SMSSignView(null, menu, loc). The
* view's name will be automatically generated, based on the menu name.
*
* @param menu The SMSMenu object to attach this view to.
* @param loc The location of this view's sign
* @throws SMSException if the given location is not suitable for this view
*/
public SMSMultiSignView(SMSMenu menu, Location loc) throws SMSException {
this(null, menu, loc);
}
/* (non-Javadoc)
* @see me.desht.scrollingmenusign.views.SMSGlobalScrollableView#thaw(org.bukkit.configuration.ConfigurationSection)
*/
@Override
public void thaw(ConfigurationSection node) throws SMSException {
super.thaw(node);
scanForSigns(getLocationsArray()[0]);
}
/* (non-Javadoc)
* @see me.desht.scrollingmenusign.views.SMSScrollableView#update(java.util.Observable, java.lang.Object)
*/
@Override
public void update(Observable menu, Object arg1) {
super.update(menu, arg1);
repaintAll();
}
@Override
public void onDeleted(boolean permanent) {
super.onDeleted(permanent);
if (permanent) {
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
org.bukkit.block.Sign s = getSign(x, y);
if (s != null) {
for (int i = 0; i < SIGN_LINES; i++) {
s.setLine(i, "");
}
s.update();
}
}
}
}
}
private void repaintAll() {
String prefixNotSel = ScrollingMenuSign.getInstance().getConfigCache().getPrefixNotSelected();
String prefixSel = ScrollingMenuSign.getInstance().getConfigCache().getPrefixSelected();
List<String> titleLines = formatTitle();
int nTitleLines = titleLines.size();
for (int i = 0; i < nTitleLines; i++) {
drawText(i, titleLines.get(i));
}
for (int i = nTitleLines; i < height * SIGN_LINES; i++) {
drawText(i, "");
}
int scrollPos = getScrollPos();
int menuSize = getActiveMenuItemCount(null);
int pageSize = height * SIGN_LINES - nTitleLines;
switch (getScrollType()) {
case SCROLL:
for (int j = 0, pos = scrollPos; j < pageSize && j < menuSize; j++) {
String lineText = getActiveItemLabel(null, pos);
drawText(j + nTitleLines, formatItem(j == 0 ? prefixSel : prefixNotSel, lineText));
if (++pos > menuSize) {
pos = 1;
}
}
break;
case PAGE:
int pageNum = (scrollPos - 1) / pageSize;
for (int j = 0, pos = (pageNum * pageSize) + 1; j < pageSize && pos <= menuSize; j++, pos++) {
String lineText = getActiveItemLabel(null, pos);
drawText(j + nTitleLines, formatItem(pos == scrollPos ? prefixSel : prefixNotSel, lineText));
}
break;
}
applyUpdates();
}
/**
* Draw a line of text at the given line, which will potentially span multiple signs.
* Colour/markup codes are preserved across signs, which may lead to unexpectedly few
* printable characters appearing on each sign if a lot of markup is used!
*
* @param line The line number on which to draw the text
* @param text The text to draw
*/
public void drawText(int line, String text) {
int y = line / SIGN_LINES;
Debugger.getInstance().debug(2, "drawText: view=" + getName() + ", line=" + line + ", text=[" + text + "]");
int begin = 0;
if (width == 1) {
// optimised case; avoid line-splitting calculations
pendingUpdate(getSignLocation(0, y), line % SIGN_LINES, text);
} else {
// multiple horizontal signs; we have some line-splitting to do...
String ctrlColour = "", ctrlOther = "";
for (int x = 0; x < width; x++) {
String ctrl = ctrlColour + ctrlOther;
int end = Math.min(begin + (SIGN_WIDTH - ctrl.length()), text.length());
String sub = ctrl + text.substring(begin, end);
if (sub.endsWith("\u00a7")) {
// we can't have a control char split over 2 signs
sub = StringUtils.chop(sub);
}
ctrlColour = ctrlOther = "";
for (int i = 0; i < sub.length() - 1; i++) {
char c = sub.charAt(i), c1 = Character.toLowerCase(sub.charAt(i + 1));
if (c == '\u00a7') {
if (c1 == 'r') {
ctrlColour = ctrlOther = "";
} else if (isHexDigit(c1)) {
ctrlColour = "\u00a7" + c1;
ctrlOther = ""; // colour code disables any previous formatting code
} else {
ctrlOther += "\u00a7" + c1;
}
}
}
Location loc = getSignLocation(x, y);
Debugger.getInstance().debug(3, "drawText: substr = [" + sub + "] @" + x + "," + y + loc + " line=" + line % SIGN_LINES);
pendingUpdate(loc, line % SIGN_LINES, sub);
begin += sub.length() - ctrl.length();
}
}
}
/* (non-Javadoc)
* @see me.desht.scrollingmenusign.views.SMSView#getType()
*/
@Override
public String getType() {
return "multisign";
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "multisign @ " + MiscUtil.formatLocation(topLeft.getLocation()) + " (" + width + "x" + height + ")";
}
/* (non-Javadoc)
* @see me.desht.scrollingmenusign.views.SMSView#addLocation(org.bukkit.Location)
*/
@Override
public void addLocation(Location loc) throws SMSException {
Block b = loc.getBlock();
if (b.getType() != Material.WALL_SIGN) {
throw new SMSException("Location " + MiscUtil.formatLocation(loc) + " does not contain a wall sign.");
}
super.addLocation(loc);
}
/**
* Get the Sign at position (x,y) in the view. (x, y) = (0, 0) is the top left sign.
* x increases to the right, y increases downward. This works regardless of sign orientation.
*
* @param x X co-ordinate
* @param y Y co-ordinate
* @return the Sign block retrieved
*/
public org.bukkit.block.Sign getSign(int x, int y) {
Block b = getSignLocation(x, y).getBlock();
if (b.getType() == Material.WALL_SIGN) {
return (org.bukkit.block.Sign) b.getState();
} else {
return null;
}
}
/**
* Get the location that position (x,y) in the view maps to. (x, y) = (0, 0) is the top left sign.
* x increases to the right, y increases downward. This works regardless of sign orientation.
*
* @param x the X position, increasing rightwards
* @param y the Y position, increasing downwards
* @return location of the sign at the given position
*/
public Location getSignLocation(int x, int y) {
Location tl = topLeft.getLocation();
BlockFace toLeft = BlockUtil.getLeft(facing);
int x1 = tl.getBlockX() + toLeft.getModX() * x;
int y1 = tl.getBlockY() - y;
int z1 = tl.getBlockZ() + toLeft.getModZ() * x;
return new Location(tl.getWorld(), x1, y1, z1);
}
/**
* Mark one line on a given sign as requiring an update.
*
* @param loc location of the sign needing an update
* @param line line number on the sign, in the range 0 .. 3
* @param text the text to be updated
*/
private void pendingUpdate(Location loc, int line, String text) {
if (!updates.containsKey(loc)) {
updates.put(loc, new String[SIGN_LINES]);
}
updates.get(loc)[line] = text;
}
/**
* Apply all the updates that have been marked as pending. Doing them all at once means
* we only need to send world updates for each sign once.
*/
private void applyUpdates() {
for (Entry<Location, String[]> e : updates.entrySet()) {
Block b = e.getKey().getBlock();
if (b.getType() != Material.WALL_SIGN) {
continue;
}
org.bukkit.block.Sign s = (org.bukkit.block.Sign) b.getState();
for (int i = 0; i < SIGN_LINES; i++) {
String line = e.getValue()[i];
if (line != null) {
s.setLine(i, line);
}
}
s.update();
}
updates.clear();
}
private void scanForSigns(Location startLoc) throws SMSException {
Block b = startLoc.getBlock();
if (b.getType() != Material.WALL_SIGN) {
throw new SMSException("Location " + MiscUtil.formatLocation(b.getLocation()) + " does not contain a sign.");
}
Sign s = (Sign) b.getState().getData();
facing = s.getFacing();
switch (facing) {
case NORTH:
scan(b, BlockFace.EAST);
break;
case EAST:
scan(b, BlockFace.SOUTH);
break;
case SOUTH:
scan(b, BlockFace.WEST);
break;
case WEST:
scan(b, BlockFace.NORTH);
break;
default:
throw new SMSException("Unexpected sign direction " + facing);
}
}
private void scan(Block b, BlockFace horizontal) throws SMSException {
Location tl = scan(b, horizontal, BlockFace.UP);
Location br = scan(b, horizontal.getOppositeFace(), BlockFace.DOWN);
topLeft = new PersistableLocation(tl);
bottomRight = new PersistableLocation(br);
validateSignArray();
height = (tl.getBlockY() - br.getBlockY()) + 1;
switch (horizontal) {
case NORTH:
case SOUTH:
width = Math.abs(tl.getBlockZ() - br.getBlockZ()) + 1;
break;
case EAST:
case WEST:
width = Math.abs(tl.getBlockX() - br.getBlockX()) + 1;
break;
default:
break;
}
Debugger.getInstance().debug(2, "multisign: topleft=" + topLeft + ", bottomright=" + bottomRight);
Debugger.getInstance().debug(2, "multisign: height=" + height + ", width=" + width);
}
private Location scan(Block b, BlockFace horizontal, BlockFace vertical) {
Debugger.getInstance().debug(2, "scan: " + b + " h=" + horizontal + " v=" + vertical);
Block b1 = scanOneDir(b, horizontal);
b1 = scanOneDir(b1, vertical);
return b1.getLocation();
}
private Block scanOneDir(Block b, BlockFace dir) {
while (b.getType() == Material.WALL_SIGN && ScrollingMenuSign.getInstance().getViewManager().getViewForLocation(b.getLocation()) == null) {
Sign s = (Sign) b.getState().getData();
if (s.getFacing() != facing) {
break;
}
b = b.getRelative(dir);
}
return b.getRelative(dir.getOppositeFace());
}
private List<Block> getBlocks() {
List<Block> res = new ArrayList<Block>();
Block tlb = topLeft.getLocation().getBlock();
Block brb = bottomRight.getLocation().getBlock();
int x1 = Math.min(tlb.getX(), brb.getX());
int x2 = Math.max(tlb.getX(), brb.getX());
int z1 = Math.min(tlb.getZ(), brb.getZ());
int z2 = Math.max(tlb.getZ(), brb.getZ());
int y1 = brb.getY();
int y2 = tlb.getY();
World w = tlb.getWorld();
for (int x = x1; x <= x2; x++) {
for (int y = y1; y <= y2; y++) {
for (int z = z1; z <= z2; z++) {
res.add(w.getBlockAt(x, y, z));
}
}
}
return res;
}
private void validateSignArray() throws SMSException {
for (Block b : getBlocks()) {
if (b.getType() != Material.WALL_SIGN) {
throw new SMSException("Sign array is not rectangular!");
}
}
}
private String formatLine(String prefix, String text, ViewJustification just) {
int l = SIGN_WIDTH * width - prefix.length();
String s = "";
// this regexp sadly doesn't work
// String reset = text.matches("\u00a7[mn]") ? "\u00a7r" : "";
String reset = "";
String textL = text.toLowerCase();
if (textL.contains("\u00a7m") || textL.contains("\u00a7n")) {
reset = "\u00a7r";
}
switch (just) {
case LEFT:
s = prefix + Str.padRight(text + reset, l);
break;
case CENTER:
s = prefix + Str.padCenter(text + reset, l);
break;
case RIGHT:
s = prefix + Str.padLeft(text + reset, l);
break;
default:
break;
}
return MiscUtil.parseColourSpec(s);
}
private List<String> formatTitle() {
List<String> lines = splitTitle(null);
for (int i = 0; i < lines.size(); i++) {
lines.set(i, formatLine("", lines.get(i), getTitleJustification()));
}
return lines;
}
private String formatItem(String prefix, String text) {
return formatLine(prefix, doVariableSubstitutions(null, text), getItemJustification());
}
private boolean isHexDigit(char c) {
return c >= '0' && c <= '9' || c >= 'a' && c <= 'f';
}
@Override
protected int getLineLength() {
return SIGN_WIDTH * width;
}
@Override
protected int getHardMaxTitleLines() {
return 4;
}
}