public class DepositInventoryEntry {
    private Player opener;
    private BlockBank targetBank;

    public DepositInventoryEntry(Player opener, BlockBank targetBank) {
        this.opener = opener;
        this.targetBank = targetBank;
    }

    public Player getOpener() {
        return opener;
    }

    public void setOpener(Player opener) {
        this.opener = opener;
    }

    public BlockBank getTargetBank() {
        return targetBank;
    }

    public void setTargetBank(BlockBank targetBank) {
        this.targetBank = targetBank;
    }
} public class DepositBoxCloseListener implements Listener {
    private Map<String, DepositInventoryEntry> openDepositInventories;

    public DepositBoxCloseListener(Map<String, DepositInventoryEntry> openDepositInventories) {
        this.openDepositInventories = openDepositInventories;
    }

    @EventHandler
    public void onInventoryClose(InventoryCloseEvent e) {
        DepositInventoryEntry entry = openDepositInventories.remove(e.getPlayer().getName());
        if(entry == null) {
            return;
        }

        for(ItemStack i : e.getInventory()) {
            if(i == null || i.getType() == null || i.getType() == Material.AIR) {
                continue;
            }
            entry.getOpener().sendMessage(ChatColor.BLUE + "Depositing " + i.getAmount() + " of " + i.getType().toString());

            for(ItemStack overflow : entry.getTargetBank().getBankInventory().addItem(i).values()) {
                entry.getOpener().getInventory().addItem(overflow);
                entry.getOpener().sendMessage(ChatColor.AQUA + "The bank couldn't hold " + overflow.getAmount() + " of your " + overflow.getType().toString());
            }
        }

        entry.getOpener().sendMessage(ChatColor.GREEN + "Deposit complete.");
    }
} public class MCTPlayerListener implements Listener {

    private static final String FENCEREGION_SIGN_PREFIX = "mkreg";
    private final MCTownsPlugin plugin;
    private final TownManager townManager;
    private final TownJoinManager joinManager;
    private final HashMap<Player, ActiveSet> potentialPlotBuyers;

    /**
     *
     * @param plugin
     */
    public MCTPlayerListener(MCTownsPlugin plugin) {
        this.townManager = plugin.getTownManager();
        this.joinManager = plugin.getJoinManager();
        potentialPlotBuyers = plugin.getPotentialPlotBuyers();
        this.plugin = plugin;
    }

    /**
     * Informs the player of any towns they're invited to tells them the MOTD
     * for each town they're in, and if they're the mayor, tells them about
     * pending invites and requests
     *
     * @param event
     */
    @EventHandler(priority = EventPriority.NORMAL)
    public void onPlayerJoin(PlayerJoinEvent event) {
        Player p = event.getPlayer();
        List<Town> towns = townManager.matchPlayerToTowns(p);

        List<Town> townsInvitedTo = joinManager.getTownsPlayerIsInvitedTo(p.getName());
        if(!townsInvitedTo.isEmpty()) {
            p.sendMessage(INFO + "You are currently invited to join the following towns:");

            for(Town t : townsInvitedTo) {
                p.sendMessage(INFO + t.getName());
            }
        }

        for(Town t : towns) {
            p.sendMessage(INFO + "[" + t.getName() + "]: " + t.getTownMOTD());
            if(t.playerIsMayor(p)) {
                if(!joinManager.getPlayersRequestingMembershipToTown(t).isEmpty()) {
                    p.sendMessage(INFO + t.getName() + " has players requesting to join.");
                }
            }
        }
    }

    @EventHandler(priority = EventPriority.NORMAL)
    public void onPlayerClickPurchaseSign(PlayerInteractEvent event) {

        if(!MCTConfig.ECONOMY_ENABLED.getBoolean()) //if economy isn't enabled, do nothing
        {
            return;
        }

        if(event.getClickedBlock() == null || event.getClickedBlock().getType() != Material.SIGN_POST) //If there is no block, or the block is not a sign, do nothing
        {
            return;
        }

        //if the first line of the sign isn't "[mct]" then do nothing
        if(!((Sign) event.getClickedBlock().getState()).getLine(0).equalsIgnoreCase("[mct]")) {
            return;
        }

        ActiveSet plotToBuy = townManager.getPlotFromSignLocation(event.getClickedBlock().getLocation());

        if(plotToBuy == null) {
            MCTowns.logSevere("Sign was an MCT plot sign, but no matching plot was found.");
            MCTowns.logSevere("Sign's location was: " + event.getClickedBlock().getLocation().toString());
            return;
        }

        if(!plotToBuy.getActivePlot().isForSale()) {
            event.getPlayer().sendMessage(ChatColor.DARK_AQUA + "That plot is not for sale.");
            return;
        }

        if(!MCTowns.getEconomy().has(event.getPlayer().getName(), plotToBuy.getActivePlot().getPrice().floatValue())) {
            event.getPlayer().sendMessage(ChatColor.RED + "Insufficient funds (costs " + plotToBuy.getActivePlot().getPrice() + ").");
            return;
        }

        //if it succeeds...
        potentialPlotBuyers.put(event.getPlayer(), plotToBuy);
        event.getPlayer().sendMessage(ChatColor.YELLOW + "Type \"/mct confirm\" to finish buying this plot.)");
        event.getPlayer().sendMessage(ChatColor.YELLOW + "Please note, this plot costs " + ChatColor.DARK_RED.toString() + MCTowns.getEconomy().format(plotToBuy.getActivePlot().getPrice().floatValue()) + " and typing \"/mct confirm\" will deduct this amount from your holdings.");
    }

    @EventHandler
    public void onPlayerTriggerFenceRegionCreation(org.bukkit.event.block.SignChangeEvent e) {
        if(e.getBlock().getType() != Material.SIGN_POST) {
            return;
        }

        Sign sign = (Sign) e.getBlock().getState();

        if(!e.getLine(0).equals(FENCEREGION_SIGN_PREFIX)) {
            return;
        }

        Player p = e.getPlayer();

        ActiveSet pActive = plugin.getActiveSets().get(p.getName());

        if(pActive == null) {
            p.sendMessage(ChatColor.RED + "Your active town is not set.");
            return;
        }

        Town t = pActive.getActiveTown();

        if(t == null) {
            p.sendMessage(ChatColor.RED + "Your active town is not set.");
            return;
        }

        boolean isMayor;
        try {
            isMayor = t.playerIsMayor(p);
        } catch(NullPointerException npe) {
            isMayor = false;
        }

        if(!isMayor) {
            p.sendMessage(ChatColor.RED + "Insufficient permission.");
            return;
        }

        if(pActive.getActiveTerritory() == null) {
            p.sendMessage(ChatColor.RED + "You need to set your active territory if you want to add a plot.");
            return;
        }

        String nuName;
        try {
            nuName = e.getLine(1);

            if(nuName.isEmpty()) {
                throw new IndexOutOfBoundsException();
            }

        } catch(IndexOutOfBoundsException ioobe) {
            p.sendMessage(ChatColor.RED + "Error: The second line must contain a name for the new plot.");
            return;
        }

        //Plot plot = new Plot(MCTownsRegion.formatRegionName(t, TownLevel.PLOT, nuName), p.getWorld().getName());
        //now, prepare the WG region
        Location signLoc = sign.getLocation();

        Vector deltaVector = signLoc.getDirection().multiply(-1);

        int count = 0;
        while(signLoc.getBlock().getType() != Material.FENCE && count < 100) {
            signLoc = signLoc.add(deltaVector);
            count++;
        }

        if(count >= 100) {
            p.sendMessage(ChatColor.RED + "Error: couldn't find a fence within 100 blocks, aborting.");
            return;
        }

        ProtectedFenceRegion fencedReg;
        try {
            fencedReg = ProtectedFenceRegion.assembleSelectionFromFenceOrigin(MCTownsRegion.formatRegionName(t, TownLevel.PLOT, nuName), signLoc);
        } catch(IncompleteFenceException ex) {
            p.sendMessage(ChatColor.RED + "Error: Fence was not complete. Fence must be a complete polygon.");
            return;
        } catch(ProtectedFenceRegion.MalformedFenceRegionException ifle) {
            p.sendMessage(ChatColor.RED + "Error: " + ifle.getLocalizedMessage());
            return;
        }

        if(!CommandHandler.selectionIsWithinParent(fencedReg, pActive.getActiveTerritory())) {
            p.sendMessage(ChatColor.RED + "Error: The selected region is not within your active territory.");
            return;
        }
        try {
            townManager.addPlot(MCTownsRegion.formatRegionName(t, TownLevel.PLOT, nuName), p.getWorld(), fencedReg, t, pActive.getActiveTerritory());
        } catch(TownManager.InvalidWorldGuardRegionNameException | TownManager.RegionAlreadyExistsException ex) {
            p.sendMessage(ChatColor.RED + ex.getLocalizedMessage());
            return;
        }

        p.sendMessage(ChatColor.GREEN + "Plot created.");

        pActive.setActivePlot(townManager.getPlot(MCTownsRegion.formatRegionName(t, TownLevel.PLOT, nuName)));
        p.sendMessage(ChatColor.LIGHT_PURPLE + "Active plot set to newly created plot.");
    }

    @EventHandler
    public void onPlayerJoinAddToDefaultTown(PlayerJoinEvent e) {
        String tName = MCTConfig.DEFAULT_TOWN.getString();
        if(!e.getPlayer().hasPlayedBefore() && townManager.matchPlayerToTowns(e.getPlayer()).isEmpty()) {
            Town t = townManager.getTown(tName);
            if(t == null) {
                if(tName != null && !tName.isEmpty()) {
                    MCTowns.logWarning("Error: Default town specified in config.yml does not exist.");
                }
                return;
            }
            t.addPlayer(e.getPlayer());
            e.getPlayer().sendMessage(ChatColor.GREEN + "You've been automatically added to the town " + t.getName() + "!");
        }
    }

    @EventHandler
    public void onPlayerJoinSetupActiveSet(PlayerJoinEvent e) {
        HashMap<String, ActiveSet> activeSets = plugin.getActiveSets();
        Player player = e.getPlayer();

        if(!activeSets.containsKey(player.getName())) {
            System.out.println("Setting up active set for " + e.getPlayer().getName());
            activeSets.put(player.getName(), new ActiveSet());
            List<Town> towns = plugin.getTownManager().matchPlayerToTowns(player);
            activeSets.get(player.getName()).setActiveTown(towns.isEmpty() ? null : towns.get(0));
        }
    }
} public class QuickSelectToolListener implements Listener {

    private static Material SELECT_TOOL;
    private final WorldGuardPlugin wgp;
    private final MCTownsPlugin mctp;
    private final TownManager townMan;

    public QuickSelectToolListener(WorldGuardPlugin wgp, MCTownsPlugin mctp) {
        this.wgp = wgp;
        this.mctp = mctp;
        this.townMan = MCTowns.getTownManager();

        SELECT_TOOL = Material.getMaterial(MCTConfig.QUICKSELECT_TOOL.getString());
    }

    @EventHandler
    public void onToolUse(PlayerInteractEvent e) {
        if(SELECT_TOOL == null) {
            MCTowns.logWarning("Error: quickselect tool config value is invalid.");
            return;
        }

        if((e.getPlayer().getItemInHand().getType().compareTo(SELECT_TOOL)) != 0) {
            return;
        }

        Player player = e.getPlayer();

        ActiveSet actives = mctp.getActiveSets().get(e.getPlayer().getName());

        if(actives == null) {
            actives = new ActiveSet();
            mctp.getActiveSets().put(player.getName(), actives);
            List<Town> towns = townMan.matchPlayerToTowns(player);
            actives.setActiveTown(towns.isEmpty() ? null : towns.get(0));
        }

        e.setCancelled(true);
        if(actives.getActiveTown() == null) {
            e.getPlayer().sendMessage(ChatColor.RED + "You need to set your active town first.");
            return;
        }

        Block b = e.getClickedBlock();

        if(b == null) {
            return;
        }

        Location spotClicked = b.getLocation();

        ApplicableRegionSet regs = wgp.getRegionManager(e.getPlayer().getWorld()).getApplicableRegions(spotClicked);

        Town town = actives.getActiveTown();

        if(town == null) {
            player.sendMessage(ChatColor.RED + "You need to set your active town first.");
            return;
        }

        Territory territ = null;
        for(ProtectedRegion pr : regs) {
            territ = townMan.getTerritory(pr.getId());
            if(territ != null && territ.getParentTown().equals(town.getName())) {
                break;
            } else {
                territ = null;
            }
        }

        Plot plot = null;
        if(territ != null) {
            for(ProtectedRegion pr : regs) {
                plot = townMan.getPlot(pr.getId());
                if(plot != null && plot.getParentTerritoryName().equals(territ.getName())) {
                    break;
                } else {
                    plot = null;
                }
            }
        }

        actives.setActiveTown(town);
        actives.setActiveTerritory(territ);
        actives.setActivePlot(plot);
        e.getPlayer().sendMessage(net.jmhertlein.core.chat.ChatUtil.INFO + "Active set is now: " + actives);
    }
} public enum Perms {

    /**
     * The node for admin commands
     */
    ADMIN,
    /**
     * allows a mayor to delete their own town
     */
    REMOVE_TOWN,
    /**
     * Lets player create town
     */
    CREATE_TOWN,
    /**
     * allows the player to warp to their home town
     */
    WARP,
    /**
     * allow the player to warp to other towns' spawns
     */
    WARP_FOREIGN,
    /**
     * allow the player to withdraw from their town bank outside their town's
     * borders
     */
    WITHDRAW_BANK_OUTSIDE_BORDERS;

    /**
     * The message to be displayed when a player attempts to perform an action
     * for which they do not have permission.
     */
    @Override
    public String toString() {
        switch(this) {
            case ADMIN:
                return "mct.admin";
            case REMOVE_TOWN:
                return "mct.removetown";
            case WARP:
                return "mct.warp";
            case WARP_FOREIGN:
                return "mct.warpforeign";
            case CREATE_TOWN:
                return "mct.createtown";
            case WITHDRAW_BANK_OUTSIDE_BORDERS:
                return "mct.withdrawoutside";
            default:
                return null;
        }
    }

    /**
     * Registers all permissions in the plugin manager
     *
     * @param pm the plugin manager in which to register permissions
     */
    public static void registerPermNodes(PluginManager pm) {
        try {
            pm.addPermission(new Permission(REMOVE_TOWN.toString()));
            pm.addPermission(new Permission(WARP.toString()));
            pm.addPermission(new Permission(WARP_FOREIGN.toString()));
            pm.addPermission(new Permission(CREATE_TOWN.toString()));
            pm.addPermission(new Permission(ADMIN.toString()));
        } catch(Exception e) {
            Logger.getLogger("Minecraft").log(Level.WARNING, "Unable to register permissions, already registered.");
        }

    }
} public class TownJoinManager {

    /**
     * Key- Player name Value- Set of towns the player is currently invited to
     * join
     */
    private final Map<String, Set<Town>> joinInvitations;
    /**
     * Key- Town Value- Set of players who have requested membership to the town
     */
    private final Map<Town, Set<String>> joinRequests;

    public TownJoinManager() {
        joinInvitations = new ConcurrentHashMap<>();
        joinRequests = new ConcurrentHashMap<>();
    }

    public void invitePlayerToTown(final String playerName, final Town invitedTo) {
        Set<Town> towns = joinInvitations.get(playerName);

        if(towns == null) {
            joinInvitations.put(playerName, new HashSet<Town>() {
                {
                    this.add(invitedTo);
                }
            });
        } else {
            towns.add(invitedTo);
        }
    }

    public boolean invitationExists(final String playerName, final Town isInvitedTo) {
        Set<Town> towns = joinInvitations.get(playerName);

        return towns == null ? false : towns.contains(isInvitedTo);
    }

    public void addJoinRequest(final String playerName, final Town requestJoinTo) {
        Set<String> players = joinRequests.get(requestJoinTo);

        if(players == null) {
            joinRequests.put(requestJoinTo, new HashSet<String>() {
                {
                    this.add(playerName);
                }
            });
        } else {
            players.add(playerName);
        }
    }

    public boolean requestExists(final String playerName, final Town t) {
        Set<String> players = joinRequests.get(t);

        return players == null ? false : players.contains(playerName);
    }

    /**
     * Removes the player from the list of players who have requested membership
     * to the specified town
     *
     * @param t
     * @param playerName
     *
     * @return true if player was removed, false if player had not ever actually
     * requested membership
     */
    public boolean clearRequest(final String playerName, final Town t) {
        Set<String> playerNames = joinRequests.get(t);

        return playerNames == null ? false : playerNames.remove(playerName);
    }

    public void clearInvitationsForPlayer(final String playerName) {
        joinInvitations.remove(playerName);
    }

    /**
     *
     * @param playerName
     * @param t
     *
     * @return true if the invite was actually cleared, false if the player was
     * not ever actually invited
     */
    public boolean clearInvitationForPlayerFromTown(final String playerName, Town t) {
        Set<Town> towns = joinInvitations.get(playerName);
        return towns == null ? false : towns.remove(t);
    }

    public List<Town> getTownsPlayerIsInvitedTo(final String playerName) {
        ArrayList<Town> ret = new ArrayList<>();
        for(Entry<String, Set<Town>> e : joinInvitations.entrySet()) {
            if(e.getKey().equals(playerName)) {
                ret.addAll(e.getValue());
            }
        }

        return ret;
    }

    /**
     * Gets all current requests for the town.
     *
     * @param t
     *
     * @return A set of requests. Changes made to the set will be reflected in
     * later calls to this method
     */
    public Set<String> getPlayersRequestingMembershipToTown(final Town t) {
        Set<String> r = joinRequests.get(t);
        if(r == null) {
            r = new HashSet<>();
            joinRequests.put(t, r);
        }
        return r;
    }

    public Set<String> getIssuedInvitesForTown(final Town t) {
        HashSet<String> playersInvited = new HashSet<>();
        for(Entry<String, Set<Town>> e : joinInvitations.entrySet()) {
            if(e.getValue().contains(t)) {
                playersInvited.add(e.getKey());
            }
        }

        return playersInvited;
    }
} public abstract class ResourceUpgradePaths {
    public static void upgradeResources(File rootDir, MCTownsPlugin p) {
        String installedVersion = p.getConfig().getString("installedVersion");

        //pre-2.2.0 used config.txt instead of config.yml, so we should detect if that's there first.
        if(new File(p.getDataFolder(), "config.txt").exists()) {
            installedVersion = "2.1.0";
        }

        p.getLogger().log(Level.INFO, "Detected resources to be those generated by v{0}", installedVersion);

        /**
         * Intentional falling-through
         *
         * Order of cases is important
         */
        switch(installedVersion) {
            case "2.1.0":
                upgradeFrom210To220(rootDir, p);
            case "2.2.0":
                upgradeFrom220To230(rootDir, p);
            case "2.3.0":
                upgradeInstalledVersion(rootDir, p, installedVersion, "2.3.1");
            case "2.3.1":
                upgradeInstalledVersion(rootDir, p, installedVersion, "2.3.2");
            case "2.3.2":
                upgradeFrom232To240(rootDir, p);
                upgradeInstalledVersion(rootDir, p, installedVersion, "2.4.0");
            case "2.4.0":
                upgradeFrom240To250(rootDir, p);
                upgradeInstalledVersion(rootDir, p, installedVersion, "2.5.0");
            default:
                p.getLogger().info("Resources are up to date.");
                break;
        }
    }

    private static void upgradeFrom210To220(File rootDir, MCTownsPlugin p) {
        p.getLogger().warning("Beginning resource migration from v2.1.0 to v2.2.0");

        p.getLogger().info("Copying town data files...");
        TownManager tempManager;
        try {
            tempManager = TownManager.readYAML(rootDir);
        } catch(IOException | InvalidConfigurationException ex) {
            p.getLogger().severe("ERROR MIGRATING RESOURCES:");
            Logger.getLogger(ResourceUpgradePaths.class.getName()).log(Level.SEVERE, null, ex);
            return;
        }
        try {
            tempManager.writeYAML(p.getSavesDir().getAbsolutePath());
        } catch(IOException ex) {
            p.getLogger().severe("ERROR MIGRATING RESOURCES:");
            Logger.getLogger(ResourceUpgradePaths.class.getName()).log(Level.SEVERE, null, ex);
        }

        p.getLogger().info("Town data files copied.");

        p.getLogger().info("Deleting old data files...");
        for(File f : rootDir.listFiles()) {
            if(!p.getConfigFiles().contains(f) && !p.getDataDirs().contains(f)) {
                f.delete();
                p.getLogger().log(Level.INFO, "{0} deleted successfully.", f.getName());
            }
        }
        p.getLogger().info("Finished deleting old files.");

        p.getLogger().info(("Updating \"installedVersion\" field in config.yml from 2.1.0 to 2.2.0"));
        p.getConfig().set("installedVersion", "2.2.0");
        p.saveConfig();

        p.getLogger().warning("Completed resource migration from v2.1.0 to v2.2.0");
    }

    private static void upgradeFrom220To230(File rootDir, MCTownsPlugin p) {
        p.getLogger().warning("Beginning resource migration from v2.2.0 to v2.3.0");
        p.getLogger().info(("Updating \"installedVersion\" field in config.yml from 2.2.0 to 2.3.0"));
        p.getConfig().set("installedVersion", "2.3.0");
        p.getLogger().info("Setting bugReportHostname to services.jmhertlein.net");
        p.getConfig().set("bugReportHostname", "services.jmhertlein.net");
        p.saveConfig();
        p.getLogger().warning("Completed resource migration from v2.2.0 to v2.3.0");
    }

    private static void upgradeInstalledVersion(File rootDir, MCTownsPlugin p, String installedVer, String curVer) {
        p.getLogger().warning("Beginning resource migration from " + installedVer + " to " + curVer);
        p.getLogger().info("Updating \"installedVersion\" field in config.yml from " + installedVer + "  to " + curVer);
        p.getConfig().set("installedVersion", curVer);
        p.saveConfig();
        p.getLogger().warning("Completed resource migration from " + installedVer + " to " + curVer);
    }

    private static void upgradeFrom232To240(File rootDir, MCTownsPlugin p) {
        p.getLogger().info("Converting old tool ID into String...");
        int oldId = p.getConfig().getInt(MCTConfig.QUICKSELECT_TOOL.getKey());
        Material newValue = Material.getMaterial(oldId);
        MCTConfig.QUICKSELECT_TOOL.set(newValue.toString());
        p.getLogger().info("Converted old tool ID into String");
    }

    private static void upgradeFrom240To250(File rootDir, MCTownsPlugin p) {
        p.getConfig().set("wgRequirement", "([6-9]\\..*)|([0-9]{2,}\\..*)");
    }
} public class ActiveSet {

    private Town activeTown;
    private Territory activeTerritory;
    private Plot activePlot;

    /**
     *
     */
    public ActiveSet() {
        activeTown = null;
        activeTerritory = null;
        activePlot = null;
    }

    public ActiveSet(Town activeTown, Territory activeTerritory, Plot activePlot) {
        this.activeTown = activeTown;
        this.activeTerritory = activeTerritory;
        this.activePlot = activePlot;
    }

    /**
     *
     * @return the active plot
     */
    public Plot getActivePlot() {
        return activePlot;
    }

    /**
     *
     * @param activePlot the new active plot
     */
    public void setActivePlot(Plot activePlot) {
        this.activePlot = activePlot;
    }

    /**
     *
     * @return the active territory
     */
    public Territory getActiveTerritory() {
        return activeTerritory;
    }

    /**
     *
     * @param activeTerritory the new active territory
     */
    public void setActiveTerritory(Territory activeTerritory) {
        this.activeTerritory = activeTerritory;
    }

    /**
     *
     * @return the active town
     */
    public Town getActiveTown() {
        return activeTown;
    }

    /**
     *
     * @param activeTown the new active town
     */
    public void setActiveTown(Town activeTown) {
        this.activeTown = activeTown;
    }

    /**
     *
     * @return Town: <town name> Territ: <territ name> Plot: <plot name>
     */
    @Override
    public String toString() {
        return "Town: " + activeTown + " Territ: " + activeTerritory + " Plot: " + activePlot;
    }

    void clear() {
        this.activePlot = null;
        this.activeTerritory = null;
        this.activeTown = null;
    }
} public class MCTHandler extends CommandHandler implements CommandDefinition {

    public MCTHandler(MCTownsPlugin parent) {
        super(parent);
    }

    @CommandMethod(path = "mct bugs")
    public void printBugReportHelp(CommandSender s) {
        s.sendMessage("To report a bug in MCTowns, go to this link:");
        s.sendMessage(ChatColor.AQUA + "https://github.com/jmhertlein/MCTowns/issues");
        s.sendMessage("Log into your GitHub account and click \"new issue\".");
    }

    @CommandMethod(path = "mct exportgraph", requiredArgs = 1, permNodes = {"mctowns.export"})
    public void exportGraph(CommandSender s, String filename) {
        File f = new File(filename);
        try(DotWriter w = new DotWriter(f, true)) {
            MCTownsPlugin.getPlugin().getCommandExecutor().writeToGraph(w);
        } catch(FileNotFoundException ex) {
            Logger.getLogger(MCTHandler.class.getName()).log(Level.SEVERE, null, ex);
            s.sendMessage("Error saving graph: " + ex.getLocalizedMessage());
        }

        s.sendMessage("DOTfile saved as " + f.getAbsolutePath());
        int exitCode = 0;
        try {
            ProcessBuilder b = new ProcessBuilder("dot", "-Tpng", f.getAbsolutePath());
            b.redirectOutput(new File(f.getAbsolutePath() + ".png"));
            Process p = b.start();
            exitCode = p.waitFor();
        } catch(IOException | InterruptedException ex) {
            s.sendMessage(ChatColor.GREEN + "Graphviz returned " + exitCode);
            s.sendMessage("Error running graphviz: " + ex.getLocalizedMessage());
            s.sendMessage("Please make sure graphviz is installed.");
            return;
        }
        if(exitCode != 0) {
            s.sendMessage(ChatColor.GREEN + "Graphviz returned " + exitCode);
            s.sendMessage("Please make sure graphviz is installed.");
            return;
        }

        s.sendMessage(ChatColor.GREEN + "Exported DOTfile to PNG as " + f.getAbsolutePath() + ".png");
    }

    @CommandMethod(path = "mct addtown", requiredArgs = 2)
    public void createTown(CommandSender s, String[] args) {
        setNewCommand(s);
        String townName = args[0], mayorName = args[1];
        if(!localSender.canCreateTown()) {
            localSender.notifyInsufPermissions();
            return;
        }

        Player nuMayor = server.getPlayer(mayorName);

        if(nuMayor == null) {
            localSender.sendMessage(ERR + mayorName + " doesn't exist or is not online.");
            return;
        }

        if(!MCTConfig.PLAYERS_CAN_JOIN_MULTIPLE_TOWNS.getBoolean() && !townManager.matchPlayerToTowns(nuMayor).isEmpty()) {
            localSender.sendMessage(ERR + nuMayor.getName() + " is already a member of a town, and as such cannot be the mayor of a new one.");
            return;
        }

        Town t = townManager.addTown(townName, nuMayor);
        if(t != null) {
            localSender.sendMessage("Town " + townName + " has been created.");
            server.broadcastMessage(SUCC + "The town " + townName + " has been founded.");

            localSender.setActiveTown(t);
            localSender.sendMessage(INFO + "Active town set to newly created town.");

            localSender.sendMessage(INFO_ALT + "The town's spawn has been set to your current location. Change it with /town spawn set.");
        } else {
            localSender.sendMessage(ERR + "That town already exists!");
        }
    }

    @CommandMethod(path = "mct removetown", requiredArgs = 1)
    public void removeTown(CommandSender s, String[] args) {
        setNewCommand(s);
        if(!localSender.canDeleteTown()) {
            localSender.notifyInsufPermissions();
            return;
        }

        Town t = townManager.getTown(args[0]);
        if(t == null) {
            localSender.sendMessage(ERR + "The town \"" + args[0] + "\" does not exist.");
            return;
        }

        townManager.removeTown(args[0]);

        try {
            for(World w : Bukkit.getWorlds()) {
                MCTowns.getWorldGuardPlugin().getRegionManager(w).save();
            }
        } catch(StorageException ex) {
            MCTowns.logSevere("Error: unable to force a region manager save in WorldGuard. Details:");
            MCTowns.logSevere(ex.getMessage());
        } catch(NullPointerException npe) {
            MCTowns.logSevere("Couldn't force WG to save its regions. (null)");
            MCTowns.logSevere("Debug analysis:");
            MCTowns.logSevere("WG plugin was null: " + (MCTowns.getWorldGuardPlugin() == null));
            MCTowns.logSevere("Server was null: " + (MCTowns.getWorldGuardPlugin() == null));
        }

        t.getResidents().stream()
                .map(Bukkit::getOfflinePlayer)
                .filter(OfflinePlayer::isOnline)
                .map(p -> plugin.getActiveSets().get(p.getName()))
                .forEach(ActiveSet::clear);

        localSender.sendMessage("Town removed.");
        server.broadcastMessage(ChatColor.DARK_RED + args[0] + " has been disbanded.");

    }

    @CommandMethod(path = "mct info town", requiredArgs = 1)
    public void queryTownInfo(CommandSender s, String[] args) {
        setNewCommand(s);
        Town t = townManager.getTown(args[0]);

        if(t == null) {
            localSender.sendMessage(ERR + "The town \"" + args[0] + "\" does not exist.");
            return;
        }

        ChatColor c = ChatColor.AQUA;

        localSender.sendMessage(c + "Name: " + t.getName());
        localSender.sendMessage(c + "Mayor: " + t.getMayor());
        localSender.sendMessage(c + "Number of residents: " + t.getSize());
        localSender.sendMessage(c + "Plots are buyable: " + t.usesBuyablePlots());
        localSender.sendMessage(c + "Join method: " + (t.usesEconomyJoins() ? "Plot purchase" : "invitations"));
    }

    @CommandMethod(path = "mct info player", requiredArgs = 1)
    public void queryPlayerInfo(CommandSender s, String[] args) {
        setNewCommand(s);
        OfflinePlayer p = server.getOfflinePlayer(args[0]);

        if(!Players.playedHasEverLoggedIn(p)) {
            localSender.sendMessage(ERR + args[0] + " has never played on this server before.");
            return;
        }

        localSender.sendMessage(INFO + "Player: " + p.getName());

        List<Town> towns = townManager.matchPlayerToTowns(p);
        if(towns.isEmpty()) {
            localSender.sendMessage(INFO + p.getName() + " is not a member of any towns.");
        } else {
            for(Town t : towns) {
                localSender.sendMessage("Town: " + t.getName());
                localSender.sendMessage("Is Mayor: " + t.playerIsMayor(p));
                localSender.sendMessage("Is Assistant: " + t.playerIsAssistant(p));
            }
        }
    }

    @CommandMethod(path = "mct list towns")
    public void listTowns(CommandSender s) {
        setNewCommand(s);

        townManager.getTownsCollection().stream()
                .collect(Collectors.toMap(
                        t -> t.getName(),
                        t -> getOnlinePlayerCounts(t)))
                .forEach((town, counts) -> s.sendMessage(String.format("%s%s (%s/%s online)", ChatColor.YELLOW, town, counts[0], counts[1])));
    }

    @CommandMethod(path = "mct join", requiredArgs = 1)
    public void requestAdditionToTown(CommandSender s, String[] args) {
        setNewCommand(s);
        if(!MCTConfig.PLAYERS_CAN_JOIN_MULTIPLE_TOWNS.getBoolean() && townManager.playerIsAlreadyInATown(localSender.getPlayer())) {
            localSender.sendMessage(ERR + "You cannot be in more than one town at a time.");
            return;
        }

        Town addTo = townManager.getTown(args[0]);
        String pName = localSender.getPlayer().getName();

        if(addTo == null) {
            localSender.sendMessage(ERR + "\"" + args[0] + "\" doesn't exist.");
            return;
        }

        if(addTo.usesEconomyJoins()) {
            localSender.sendMessage(addTo.getName() + " doesn't use the invitation system.");
            return;
        }

        if(addTo.playerIsResident(localSender.getPlayer())) {
            localSender.sendMessage(ERR + "You are already a member of " + addTo.getName());
            return;
        }

        if(joinManager.getPlayersRequestingMembershipToTown(addTo).contains(localSender.getPlayer().getName())) {
            localSender.sendMessage(ERR + "You've already requested to join " + addTo.getName());
            return;
        }

        if(joinManager.invitationExists(pName, addTo)) {
            addTo.addPlayer(localSender.getPlayer());
            localSender.sendMessage("You have joined " + addTo.getName() + "!");
            broadcastTownJoin(addTo, localSender.getPlayer());

            joinManager.clearInvitationForPlayerFromTown(pName, addTo);
        } else {
            joinManager.addJoinRequest(pName, addTo);
            localSender.sendMessage("You have submitted a request to join " + args[0] + ".");
            addTo.broadcastMessageToTown(localSender.getPlayer().getName() + " has submitted a request to join the town.");
        }
    }

    @CommandMethod(path = "mct refuse", requiredArgs = 1)
    public void rejectInvitationFromTown(CommandSender s, String[] args) {
        setNewCommand(s);
        String pName = localSender.getPlayer().getName();

        Town t = townManager.getTown(args[0]);

        if(t == null) {
            localSender.sendMessage(ERR + "You're not invited to any towns right now.");
        } else {
            joinManager.clearInvitationForPlayerFromTown(pName, t);
            localSender.sendMessage(ChatColor.GOLD + "You have rejected the invitation to join " + t.getName());
            t.broadcastMessageToTown(ERR + pName + " has declined the invitation to join the town.");
        }

    }

    @CommandMethod(path = "mct cancel", requiredArgs = 1)
    public void cancelRequest(CommandSender s, String[] args) {
        setNewCommand(s);

        Town t = townManager.getTown(args[0]);

        if(t == null) {
            localSender.sendMessage(ERR + "That town doesn't exist.");
            return;
        }

        if(joinManager.clearRequest(localSender.getPlayer().getName(), t)) {
            localSender.sendMessage(ChatColor.GOLD + "You have withdrawn your request to join " + t.getName() + ".");
        } else {
            localSender.sendMessage(ERR + "You haven't submitted a request to join " + t.getName() + ".");
        }

    }

    @CommandMethod(path = "mct list invites")
    public void checkPendingInvite(CommandSender s) {
        setNewCommand(s);
        List<Town> towns = joinManager.getTownsPlayerIsInvitedTo(localSender.getPlayer().getName());

        localSender.sendMessage(INFO + "You are currently invited to the following towns:");
        for(Town t : towns) {
            localSender.sendMessage(INFO_ALT + t.getName());
        }
    }

    @CommandMethod(path = "mct confirm")
    public void confirmPlotPurchase(CommandSender s) {
        setNewCommand(s);
        if(!MCTConfig.ECONOMY_ENABLED.getBoolean()) {
            localSender.sendMessage(ERR + "The economy isn't enabled for your server.");
            return;
        }

        ActiveSet plotToBuy = plugin.getPotentialPlotBuyers().get(localSender.getPlayer());

        if(plotToBuy == null) {
            localSender.sendMessage(ERR + "You haven't selected a plot to buy yet.");
            return;
        }

        if(townManager.playerIsAlreadyInATown(localSender.getPlayer())) //if players can't join multiple towns AND the town they're buying from isn't their current town
        {
            if(!MCTConfig.PLAYERS_CAN_JOIN_MULTIPLE_TOWNS.getBoolean() && !townManager.matchPlayerToTowns(localSender.getPlayer()).get(0).equals(plotToBuy.getActiveTown())) {
                localSender.sendMessage(ERR + "You're already in a different town.");
                return;
            }
        }

        if(!plotToBuy.getActiveTown().playerIsResident(localSender.getPlayer())) {
            if(!plotToBuy.getActiveTown().usesEconomyJoins()) {
                localSender.sendMessage(ERR + "You aren't a member of this town.");
                return;
            }
        }

        if(!plotToBuy.getActiveTown().usesBuyablePlots()) {
            localSender.sendMessage(ERR + "This town's plots aren't buyable.");
            return;
        }

        Plot p = plotToBuy.getActivePlot();

        if(!p.isForSale()) {
            localSender.sendMessage(ERR + "This plot isn't for sale.");
            return;
        }

        if(!MCTowns.getEconomy().withdrawPlayer(localSender.getPlayer().getName(), p.getPrice().floatValue()).transactionSuccess()) {
            localSender.sendMessage(ERR + "Insufficient funds.");
            return;
        }

        ProtectedRegion plotReg = MCTowns.getWorldGuardPlugin().getRegionManager(server.getWorld(p.getWorldName())).getRegion(p.getName());
        if(plotReg == null) {
            localSender.sendMessage(ERR + "The WorldGuard region for the plot you're trying to buy seems to have been deleted. Please notify your mayor.");
            return;
        }

        plotToBuy.getActiveTown().getBank().depositCurrency(p.getPrice());

        p.setPrice(BigDecimal.ZERO);
        p.setForSale(false);
        p.demolishSign();

        plotReg.getOwners().addPlayer(localSender.getPlayer().getName());

        localSender.sendMessage(ChatColor.GREEN + "You are now the proud owner of this plot.");
        doRegManSave(MCTowns.getWorldGuardPlugin().getRegionManager(server.getWorld(p.getWorldName())));

        if(!townManager.matchPlayerToTowns(localSender.getPlayer()).contains(plotToBuy.getActiveTown())) {
            plotToBuy.getActiveTown().addPlayer(localSender.getPlayer());
            localSender.sendMessage(ChatColor.GREEN + "You have joined the town " + plotToBuy.getActiveTown().getName());
        }
    }

    private static long[] getOnlinePlayerCounts(Town t) {
        long online = t.getResidents().stream()
                .map(Bukkit::getOfflinePlayer)
                .filter(OfflinePlayer::isOnline)
                .count();
        long total = t.getSize();
        return new long[]{online, total};
    }
} public class PlotHandler extends CommandHandler implements CommandDefinition {

    public PlotHandler(MCTownsPlugin parent) {
        super(parent);
    }

    @CommandMethod(path = "plot info")
    public void printPlotInfo(CommandSender s) {
        setNewCommand(s);
        Plot p = localSender.getActivePlot();

        if(p == null) {
            localSender.notifyActivePlotNotSet();
            return;
        }
        ChatColor c = ChatColor.AQUA;
        localSender.sendMessage(c + "Plot name: " + p.getTerseName());
        localSender.sendMessage(c + "Corresponding WG Region name: " + p.getName());
        localSender.sendMessage(c + "World name: " + p.getWorldName());
        localSender.sendMessage(c + "Plot is for sale: " + p.isForSale());
        localSender.sendMessage(c + "Plot price: " + p.getPrice());

    }

    @CommandMethod(path = "plot remove player", requiredArgs = 1)
    public void removePlayerFromPlot(CommandSender s, String[] args) {
        setNewCommand(s);
        Plot plot = localSender.getActivePlot();

        if(plot == null) {
            localSender.notifyActivePlotNotSet();
            return;
        }

        RegionManager regMan = MCTowns.getWorldGuardPlugin().getRegionManager(server.getWorld(plot.getWorldName()));

        ProtectedRegion wgPlot = regMan.getRegion(plot.getName());

        //if they are neither mayor nor owner
        if(!(localSender.hasMayoralPermissions() || wgPlot.getOwners().contains(MCTowns.getWorldGuardPlugin().wrapPlayer(localSender.getPlayer())))) {
            localSender.notifyInsufPermissions();
            return;
        }

        if(plot.removePlayer(Bukkit.getOfflinePlayer(args[0]))) {
            localSender.sendMessage("Player removed from plot.");
        } else {
            localSender.sendMessage(ERR + "Unable to remove player from plot (either player is not in plot, world doesn't exist, or WorldGuard region doesn't exist.).");
        }
    }

    @CommandMethod(path = "plot add player", requiredArgs = 1, filters = {"mayoralPerms"})
    public void addPlayerToPlot(CommandSender s, String[] args) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        Plot plot = localSender.getActivePlot();
        if(plot == null) {
            localSender.notifyActivePlotNotSet();
            return;
        }

        OfflinePlayer player = server.getOfflinePlayer(args[0]);
        if(!Players.playedHasEverLoggedIn(player)) {
            localSender.sendMessage(ERR + args[0] + " has never played on this server.");
            return;
        }

        if(!t.playerIsResident(player)) {
            localSender.sendMessage(ERR + "That player is not a member of the town.");
            return;
        }

        if(plot.addPlayer(player)) {
            localSender.sendMessage("Player added to plot.");
        } else {
            localSender.sendMessage(ERR + "Unable to add player to plot (either player is already in plot, world doesn't exist, or WorldGuard region doesn't exist.).");
        }

    }

    @CommandMethod(path = "plot add guest", requiredArgs = 1)
    public void addPlayerToPlotAsGuest(CommandSender s, String[] args) {
        setNewCommand(s);
        Plot plot = localSender.getActivePlot();
        if(plot == null) {
            localSender.notifyActivePlotNotSet();
            return;
        }

        RegionManager regMan = MCTowns.getWorldGuardPlugin().getRegionManager(server.getWorld(plot.getWorldName()));

        ProtectedRegion wgPlot = regMan.getRegion(plot.getName());

        //if they are neither mayor nor owner
        if(!(localSender.hasMayoralPermissions() || wgPlot.getOwners().contains(MCTowns.getWorldGuardPlugin().wrapPlayer(localSender.getPlayer())))) {
            localSender.notifyInsufPermissions();
            return;
        }

        OfflinePlayer player = server.getOfflinePlayer(args[0]);
        if(!Players.playedHasEverLoggedIn(player)) {
            localSender.sendMessage(ChatColor.GOLD + args[0] + " has never played on this server.");
            return;
        }

        if(plot.addGuest(player)) {
            localSender.sendMessage(ChatColor.GREEN + "Successfully added " + player.getName() + " to the plot as a guest.");
        } else {
            localSender.sendMessage(ERR + "Unable to add player to plot as guest (either player is already guest in plot, world doesn't exist, or WorldGuard region doesn't exist.).");
        }
    }

    @CommandMethod(path = "plot economy forsale", requiredArgs = 1, filters = {"mayoralPerms"})
    public void setPlotBuyability(CommandSender s, String[] args) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(!t.usesBuyablePlots()) {
            localSender.sendMessage(ERR + t.getName() + " does not allow the sale of plots.");
            return;
        }

        boolean forSale;
        try {
            forSale = Boolean.parseBoolean(args[0]);
        } catch(Exception e) {
            localSender.sendMessage(ERR + "Error parsing boolean on token: " + args[0]);
            return;
        }

        Plot p = localSender.getActivePlot();

        if(p == null) {
            localSender.notifyActivePlotNotSet();
            return;
        }

        p.setForSale(forSale);
        localSender.sendMessage(ChatColor.GREEN + "The plot " + p.getName() + " is " + (forSale ? "now" : "no longer") + " for sale!");
    }

    @CommandMethod(path = "plot economy setprice", requiredArgs = 1, filters = {"mayoralPerms"})
    public void setPlotPrice(CommandSender s, String rawAmount) {
        setNewCommand(s);

        if(!localSender.hasMayoralPermissions()) {
            localSender.notifyInsufPermissions();
            return;
        }

        if(!Pattern.compile(MCTConfig.CURRENCY_INPUT_PATTERN.getString()).matcher(rawAmount).matches())
        {
            localSender.sendMessage(ERR + "Invalid currency input: " + rawAmount);
            return;
        }

        BigDecimal price;
        try {
            price = new BigDecimal(rawAmount);
        } catch(Exception e) {
            localSender.sendMessage(ERR + "Error parsing float on token: " + rawAmount);
            return;
        }

        Plot p = localSender.getActivePlot();

        if(p == null) {
            localSender.notifyActivePlotNotSet();
            return;
        }

        p.setPrice(price);
        p.buildSign(localSender.getPlayer().getLocation());
        localSender.sendMessage(ChatColor.GREEN + "Price of " + p.getName() + " set to " + p.getPrice() + ".");
    }

    @CommandMethod(path = "plot sign build", filters = {"mayoralPerms"})
    public void buildSign(CommandSender s) {
        setNewCommand(s);

        if(!MCTConfig.ECONOMY_ENABLED.getBoolean()) {
            localSender.sendMessage(ERR + "The economy isn't enabled for your server.");
            return;
        }

        Plot p = localSender.getActivePlot();

        if(p == null) {
            localSender.notifyActivePlotNotSet();
            return;
        }

        if(p.buildSign(localSender.getPlayer().getLocation())) {
            localSender.sendMessage(SUCC + "Sign built!");
        } else {
            localSender.sendMessage(ERR + "The sign wasn't built because its target location wasn't an air block. Please clear the spot and try again.");
        }
    }

    @CommandMethod(path = "plot sign demolish", filters = {"mayoralPerms"})
    public void demolishSign(CommandSender s) {
        setNewCommand(s);

        if(!MCTConfig.ECONOMY_ENABLED.getBoolean()) {
            localSender.sendMessage(ERR + "The economy isn't enabled for your server.");
            return;
        }

        Plot p = localSender.getActivePlot();

        if(p == null) {
            localSender.notifyActivePlotNotSet();
            return;
        }

        p.demolishSign();
        localSender.sendMessage("Sign demolished.");
    }

    @CommandMethod(path = "plot sign setpos", filters = {"mayoralPerms"})
    public void setPlotSignPosition(CommandSender s) {
        setNewCommand(s);

        Plot p = localSender.getActivePlot();

        if(p == null) {
            localSender.notifyActivePlotNotSet();
            return;
        }

        net.jmhertlein.core.location.Location mctLoc;

        Player player = localSender.getPlayer();

        mctLoc = net.jmhertlein.core.location.Location.convertFromBukkitLocation(player.getTargetBlock((Set<Material>) null, 5).getLocation());

        if(mctLoc == null) {
            localSender.sendMessage(ERR + "Couldn't get the location you're looking at.");
            return;
        }

        //use the block ABOVE the one the player is staring at.
        mctLoc.setY(mctLoc.getY() + 1);
        p.demolishSign();

        p.setSignLoc(mctLoc);

        p.buildSign(localSender.getPlayer().getLocation());

        localSender.sendMessage(ChatColor.GREEN + "Successfully set the location for the sign.");

    }

    // TODO: this was never made accessable, probably needs testing
    //@CommandMethod(path = "plot surrender")
    public void surrenderPlot() {

        Plot p = localSender.getActivePlot();
        if(p == null) {
            localSender.notifyActivePlotNotSet();
            return;
        }

        ProtectedRegion reg = MCTowns.getWorldGuardPlugin().getRegionManager(server.getWorld(p.getWorldName())).getRegion(p.getName());

        if(!reg.isOwner(MCTowns.getWorldGuardPlugin().wrapPlayer(localSender.getPlayer()))) {
            localSender.sendMessage(ERR + "You don't own this plot, so you can't surrender it!");
            return;
        }

        reg.getOwners().removePlayer(localSender.getPlayer().getName());

        for(String name : reg.getMembers().getPlayers()) {
            reg.getMembers().removePlayer(name);
        }

        p.setForSale(false);
        p.setPrice(BigDecimal.ZERO);
    }

    @CommandMethod(path = "plot active", requiredArgs = 1)
    public void setActivePlot(CommandSender s, String plotName) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        Territory te = localSender.getActiveTerritory();
        if(te == null) {
            localSender.notifyActiveTerritoryNotSet();
            return;
        }

        Plot nuActive = townManager.getPlot(plotName);
        if(nuActive == null) {
            nuActive = townManager.getPlot(MCTownsRegion.formatRegionName(t, TownLevel.PLOT, plotName));

            if(nuActive == null) {
                localSender.sendMessage(ERR + "The plot \"" + plotName + "\" does not exist.");
                return;
            }
        }

        if(!nuActive.getParentTownName().equals(t.getName())) {
            localSender.sendMessage(ERR + "The plot \"" + plotName + "\" does not exist in your town.");
            return;
        }

        localSender.setActivePlot(nuActive);
        if(!localSender.getActiveTerritory().getName().equals(nuActive.getParentTerritoryName())) {
            localSender.setActiveTerritory(townManager.getTerritory(nuActive.getParentTerritoryName()));
            localSender.sendMessage(ChatColor.YELLOW + "Also setting parent territory to " + nuActive.getParentTerritoryName());
        }
        localSender.sendMessage("Active plot set to " + nuActive.getName());
    }

    @CommandMethod(path = "plot redefine")
    public void redefine() {
        redefineActiveRegion(TownLevel.PLOT);
    }
} public class TerritoryHandler extends CommandHandler implements CommandDefinition {

    public TerritoryHandler(MCTownsPlugin parent) {
        super(parent);
    }

    @CommandMethod(path = "territory add plot", requiredArgs = 1, filters = {"mayoralPerms"})
    public void addPlotToTerritory(CommandSender s, String[] args) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        Territory parTerr = localSender.getActiveTerritory();
        if(parTerr == null) {
            localSender.notifyActiveTerritoryNotSet();
            return;
        }

        String plotName = MCTownsRegion.formatRegionName(t, TownLevel.PLOT, args[0]);

        World w = localSender.getPlayer().getWorld();

        if(w == null) {
            localSender.sendMessage(
                    ERR + "You are in an invalid World. (Player::getWorld() returned null)");
            return;
        }

        ProtectedRegion region = getSelectedRegion(plotName);

        if(region == null) {
            localSender.sendMessage(
                    ERR + "You need to make a WorldEdit selection first.");
            return;
        }

        if(!selectionIsWithinParent(region, localSender.getActiveTerritory())) {
            localSender.sendMessage(ERR + "Selection is not in territory!");
            return;
        }

        try {
            townManager.addPlot(plotName, w, region, t, parTerr);
        } catch(TownManager.InvalidWorldGuardRegionNameException | TownManager.RegionAlreadyExistsException ex) {
            localSender.sendMessage(ERR + ex.getLocalizedMessage());
            return;
        }

        localSender.sendMessage(SUCC + "Plot added.");

        localSender.setActivePlot(townManager.getPlot(plotName));
        localSender.sendMessage(INFO + "Active plot set to newly created plot.");

    }

    @CommandMethod(path = "territory remove plot", requiredArgs = 1, filters = {"mayoralPerms"})
    public void removePlotFromTerritory(CommandSender s, String[] args) {
        setNewCommand(s);

        Territory t = localSender.getActiveTerritory();

        if(t == null) {
            localSender.notifyActiveTerritoryNotSet();
            return;
        }

        if(!townManager.removePlot(args[0])) {
            localSender.sendMessage(
                    ERR + "That plot doesn't exist. Make sure you're using the full name of the plot (townname_plot_plotshortname).");
            return;
        }

        localSender.sendMessage(SUCC + "Plot removed.");
    }

    @CommandMethod(path = "territory add player", requiredArgs = 1, filters = {"mayoralPerms"})
    public void addPlayerToTerritory(CommandSender s, String[] args) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        Territory terr = localSender.getActiveTerritory();
        if(terr == null) {
            localSender.notifyActiveTerritoryNotSet();
            return;
        }

        OfflinePlayer player = server.getOfflinePlayer(args[0]);
        if(!Players.playedHasEverLoggedIn(player)) {
            localSender.sendMessage(ERR + args[0] + " has never played on this server before.");
            return;
        }

        if(!t.playerIsResident(player)) {
            localSender.sendMessage(ERR + "That player is not a member of the town.");
            return;
        }

        if(terr.addPlayer(Bukkit.getOfflinePlayer(args[0]))) {
            localSender.sendMessage("Player added to territory.");
        } else {
            localSender.sendMessage(ERR + "Unable to add player to territory. Either they are already in it, or the underlying World or WorldGuard Region has been deleted.");
        }
    }

    @CommandMethod(path = "territory remove player", requiredArgs = 1, filters = {"mayoralPerms"})
    public void removePlayerFromTerritory(CommandSender s, String playerName, Boolean recursive) {
        setNewCommand(s);

        Territory terr = localSender.getActiveTerritory();
        if(terr == null) {
            localSender.notifyActiveTerritoryNotSet();
            return;
        }

        OfflinePlayer player = server.getOfflinePlayer(playerName);
        if(!Players.playedHasEverLoggedIn(player)) {
            localSender.sendMessage(
                    ERR + playerName + " has never played on this server before.");
            return;
        }

        if(!terr.removePlayer(Bukkit.getOfflinePlayer(playerName))) {
            localSender.sendMessage(
                    ERR + "Unable to remove player from territory. Either they were not in it in the first place, or the underlying World or WorldGuard Region has been deleted.");
        } else {
            localSender.sendMessage(SUCC + "Player removed from territory.");
        }

        if(recursive != null && recursive) {
            localSender.sendMessage(
                    INFO + "Recursive mode was requested. Removing from child plots...");
            for(String plotName : terr.getPlotsCollection()) {
                if(townManager.getPlot(plotName).removePlayer(player)) {
                    localSender.sendMessage(
                            INFO + "Player removed from " + plotName);
                }
            }
        }
    }

    @CommandMethod(path = "territory active", requiredArgs = 1)
    public void setActiveTerritory(CommandSender s, String[] args) {
        setNewCommand(s);
        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        Territory nuActive = townManager.getTerritory(args[0]);

        if(nuActive == null) {
            nuActive = townManager.getTerritory(MCTownsRegion.formatRegionName(t, TownLevel.TERRITORY, args[0]));
        }

        if(nuActive == null) {
            localSender.sendMessage(
                    ERR + "The territory \"" + args[0] + "\" does not exist.");
            return;
        }

        if(!nuActive.getParentTown().equals(t.getName())) {
            localSender.sendMessage(
                    ERR + "The territory \"" + args[0] + "\" does not exist in your town.");
            return;
        }

        localSender.setActiveTerritory(nuActive);
        localSender.sendMessage("Active territory set to " + nuActive.getName());
    }

    private void listPlots(int page) {
        page--; //shift to 0-indexing

        if(page < 0) {
            localSender.sendMessage(ERR + "Invalid page.");
            return;
        }

        Territory t = localSender.getActiveTerritory();

        if(t == null) {
            localSender.notifyActiveTerritoryNotSet();
            return;
        }
        localSender.sendMessage(
                ChatColor.AQUA + "Existing plots (page " + page + "):");

        String[] plots = t.getPlotsCollection().toArray(
                new String[t.getPlotsCollection().size()]);

        for(int i = page * RESULTS_PER_PAGE; i < plots.length && i < page * RESULTS_PER_PAGE + RESULTS_PER_PAGE; i++) {
            localSender.sendMessage(ChatColor.YELLOW + plots[i]);
        }
    }

    @CommandMethod(path = "territory list plots")
    public void listPlots(CommandSender s, String[] args) {
        setNewCommand(s);
        int page;
        try {
            if(args.length > 0) {
                page = Integer.parseInt(args[0]);
            } else {
                page = 1;
            }
        } catch(NumberFormatException nfex) {
            localSender.sendMessage(
                    ERR + "Error parsing integer argument. Found \"" + args[0] + "\", expected integer.");
            return;
        }

        listPlots(page);
    }

    @CommandMethod(path = "territory redefine")
    public void redefine() {
        redefineActiveRegion(TownLevel.TERRITORY);
    }
} public class TownHandler extends CommandHandler implements CommandDefinition {

    public TownHandler(MCTownsPlugin parent) {
        super(parent);
    }

    @CommandMethod(path = "town active", requiredArgs = 1)
    public void setActiveTown(CommandSender s, String[] args) {
        setNewCommand(s);
        Town t = townManager.getTown(args[0]);
        if(t == null) {
            localSender.sendMessage(ERR + "The town \"" + args[0] + "\" does not exist.");
            return;
        }

        if(!localSender.hasExternalPermissions(Perms.ADMIN.toString()) && !t.playerIsResident(localSender.getPlayer())) {
            localSender.notifyInsufPermissions();
            return;
        }

        localSender.setActiveTown(townManager.getTown(args[0]));
        localSender.sendMessage("Active town set to " + args[0]);

    }

    @CommandMethod(path = "town active reset")
    public void resetActiveTown(CommandSender s) {
        setNewCommand(s);
        List<Town> t = townManager.matchPlayerToTowns((Player) localSender.getSender());

        if(t.isEmpty()) {
            localSender.sendMessage(ERR + "Unable to match you to a town. Are you sure you belong to one?");
            return;
        }

        localSender.setActiveTown(t.get(0));
        localSender.sendMessage(ChatColor.LIGHT_PURPLE + "Active town reset to " + t.get(0).getName() + ".");
    }

    @CommandMethod(path = "town spawn set", filters = {"mayoralPerms"})
    public void setTownSpawn(CommandSender s) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(!t.playerIsInsideTownBorders(localSender.getPlayer())) {
            localSender.sendMessage(ERR + "You need to be inside your town borders to do that.");
            return;
        }

        t.setSpawn(localSender.getPlayer().getLocation());
        localSender.sendMessage("Town spawn location updated.");
    }

    @CommandMethod(path = "town joinmethod", requiredArgs = 1, filters = {"mayoralPerms"})
    public void setTownJoinMethod(CommandSender s, String[] args) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        TownJoinMethod method;
        try {
            method = TownJoinMethod.parseMethod(args[0]);
        } catch(TownJoinMethodFormatException ex) {
            localSender.sendMessage(ERR + ex.getMessage());
            return;
        }

        if(method == TownJoinMethod.ECONOMY) {
            t.setEconomyJoins(true);
        } else if(method == TownJoinMethod.INVITATION) {
            t.setEconomyJoins(false);
        }

        localSender.sendMessage(SUCC + "Town join method updated.");

    }

    @CommandMethod(path = "town economy buyableplots", requiredArgs = 1, filters = {"mayoralPerms"})
    public void setTownPlotBuyability(CommandSender s, String[] args) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(!MCTConfig.ECONOMY_ENABLED.getBoolean()) {
            localSender.sendMessage(ERR + "The economy is not enabled for your server.");
            return;
        }

        boolean buyability;

        try {
            buyability = Boolean.parseBoolean(args[0]);
        } catch(Exception e) {
            localSender.sendMessage(ERR + "Error in parsing boolean: expected true/false, found " + args[0]);
            return;
        }

        t.setBuyablePlots(buyability);
        if(buyability) {
            localSender.sendMessage(ChatColor.GOLD + t.getName() + "'s plots can now be sold and new plots are buyable by default.");
        } else {
            localSender.sendMessage(ChatColor.GOLD + t.getName() + "'s plots are no longer for sale.");
        }

    }

    @CommandMethod(path = "town economy defaultplotprice", requiredArgs = 1, filters = {"mayoralPerms"})
    public void setDefaultPlotPrice(CommandSender s, String rawAmount) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(!Pattern.compile(MCTConfig.CURRENCY_INPUT_PATTERN.getString()).matcher(rawAmount).matches())
        {
            localSender.sendMessage(ERR + "Invalid currency input: " + rawAmount);
            return;
        }

        BigDecimal price;
        try {
            price = new BigDecimal(rawAmount);
        } catch(NumberFormatException nfe) {
            localSender.sendMessage(ERR + "Error parsing plot price: " + nfe.getMessage());
            return;
        }

        t.setDefaultPlotPrice(price);
        localSender.sendMessage(SUCC + "The default price of plots in " + t.getName() + " has been set to " + price);

    }

    @CommandMethod(path = "town add territory", requiredArgs = 1)
    public void addTerritorytoTown(CommandSender s, String[] args) {
        setNewCommand(s);
        boolean admin = localSender.hasExternalPermissions(Perms.ADMIN.toString());

        if(!(MCTConfig.MAYORS_CAN_BUY_TERRITORIES.getBoolean() || admin)) {
            localSender.sendMessage(ChatColor.BLUE + "Mayors are not allowed to add territories and you're not an admin.");
            return;
        }

        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if((t.getSize() < MCTConfig.MIN_NUM_PLAYERS_TO_BUY_TERRITORY.getInt()) && !admin) {
            localSender.sendMessage(ERR + "You don't have enough people in your town to buy territories yet.");
            localSender.sendMessage(ERR + "You have " + t.getSize() + " people, but you need a total of " + MCTConfig.MIN_NUM_PLAYERS_TO_BUY_TERRITORY.getInt() + "!");
            return;
        }

        String territName = MCTownsRegion.formatRegionName(t, TownLevel.TERRITORY, args[0]);

        World w = localSender.getPlayer().getWorld();

        if(w == null) {
            localSender.sendMessage(ERR + "You are in an invalid World. (Player#getWorld() returned null)");
            return;
        }

        ProtectedRegion region = this.getSelectedRegion(territName);

        if(region == null) {
            localSender.sendMessage(ERR + "No region selected!");
            return;
        }

        int max = MCTConfig.TERRITORY_XZ_SIZE_LIMIT.getInt(),
                cur = WGUtils.getNumXZBlocksInRegion(region);
        if(cur > max && !localSender.hasExternalPermissions(Perms.ADMIN.toString())) {
            localSender.sendMessageF("%sYou're not allowed to make a territory that big. (Current: %s, Limit: %s)", ERR, cur, max);
            return;
        }

        BigDecimal colPrice = new BigDecimal(MCTConfig.PRICE_PER_XZ_BLOCK.getString());
        //charge the player if they're not running this as an admin and buyable territories is enabled and the price is more than 0
        if(!admin && colPrice.compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal price = colPrice.multiply(new BigDecimal(WGUtils.getNumXZBlocksInRegion(region)));

            if(t.getBank().getCurrencyBalance().compareTo(price) < 0) {
                //If they can't afford it...
                localSender.sendMessage(ERR + "There is not enough money in your " + INFO + "town's bank account" + ERR + " to buy a region that large.");
                localSender.sendMessage(ERR + "Total Price: " + price);
                localSender.sendMessage(INFO + "Add money to your town's bank with: /town bank deposit currency <amount>");
                return;
            }

            //otherwise...
            t.getBank().withdrawCurrency(price);

            localSender.sendMessage(ChatColor.GREEN + "Purchase success! Total price was: " + price.toString());
        }

        try {
            townManager.addTerritory(territName, w, region, t);
        } catch(TownManager.InvalidWorldGuardRegionNameException | TownManager.RegionAlreadyExistsException ex) {
            localSender.sendMessage(ERR + ex.getLocalizedMessage());
            return;
        }

        region.getOwners().addPlayer(UUIDs.getNameForUUID(t.getMayor()));
        for(String assistantName : t.getAssistantNames()) {
            region.getOwners().addPlayer(assistantName);
        }

        localSender.sendMessage(SUCC + "Territory added.");

        localSender.setActiveTerritory(townManager.getTerritory(territName));
        localSender.sendMessage(ChatColor.LIGHT_PURPLE + "Active territory set to newly created territory.");

    }

    @CommandMethod(path = "town remove territory", requiredArgs = 1, filters = {"mayoralPerms"})
    public void removeTerritoryFromTown(CommandSender s, String[] args) {
        setNewCommand(s);

        Town to = localSender.getActiveTown();

        if(to == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        String territName = MCTownsRegion.formatRegionName(to, TownLevel.TERRITORY, args[0]);

        if(!townManager.removeTerritory(territName)) {
            localSender.sendMessage(ERR + "Error: Territory \"" + territName + "\" does not exist and was not removed (because it doesn't exist!)");
        } else {
            localSender.sendMessage(SUCC + "Territory removed.");
        }
    }

    @CommandMethod(path = "town add player", requiredArgs = 1, filters = {"mayoralPerms"})
    public void invitePlayerToTown(CommandSender s, String[] args) {
        setNewCommand(s);
        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(t.usesEconomyJoins()) {
            localSender.sendMessage(t.getName() + " doesn't use the invitation system.");
            return;
        }

        OfflinePlayer p = server.getOfflinePlayer(args[0]);
        if(!p.isOnline() && !Players.playedHasEverLoggedIn(p)) {
            localSender.sendMessage(ERR + args[0] + " has never played on this server before.");
            return;
        }

        if(!MCTConfig.PLAYERS_CAN_JOIN_MULTIPLE_TOWNS.getBoolean() && townManager.playerIsAlreadyInATown(p)) {
            localSender.sendMessage(ERR + p.getName() + " is already in a town.");
            return;
        }

        if(t.playerIsResident(p)) {
            localSender.sendMessage(ERR + p.getName() + " is already a member of " + t.getName());
            return;
        }

        if(joinManager.getIssuedInvitesForTown(t).contains(p.getName())) {
            localSender.sendMessage(ERR + p.getName() + " is already invited to join " + t.getName());
            return;
        }

        for(Player pl : Bukkit.getOnlinePlayers()) {
            if(pl.getName().equalsIgnoreCase(p.getName()) && !pl.getName().equals(p.getName())) {
                localSender.sendMessage(INFO + "NOTE: You invited " + p.getName() + ", did you mean to invite " + pl.getName() + "? (Names are CaSe SeNsItIvE!)");
            }
        }

        if(joinManager.requestExists(p.getName(), t)) {
            t.addPlayer(p);
            if(p.isOnline()) {
                p.getPlayer().sendMessage("You have joined " + t.getName() + "!");
            }
            broadcastTownJoin(t, p.getName());
            joinManager.clearRequest(p.getName(), t);
        } else {
            joinManager.invitePlayerToTown(p.getName(), t);
            localSender.sendMessage(SUCC + p.getName() + " has been invited to join " + t.getName() + ".");
            if(p.isOnline()) {
                p.getPlayer().sendMessage(ChatColor.DARK_GREEN + "You have been invited to join the town " + t.getName() + "!");
                p.getPlayer().sendMessage(ChatColor.DARK_GREEN + "To join, type /mct join " + t.getName());
            }
        }
    }

    @CommandMethod(path = "town add assistant", requiredArgs = 1)
    public void promoteToAssistant(CommandSender s, String[] args) {
        setNewCommand(s);
        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(!(localSender.hasExternalPermissions(Perms.ADMIN.toString()) || t.playerIsMayor(localSender.getPlayer()))) {
            localSender.notifyInsufPermissions();
            return;
        }

        OfflinePlayer p = server.getOfflinePlayer(args[0]);
        if(!Players.playedHasEverLoggedIn(p)) {
            localSender.sendMessage(ERR + args[0] + " has never played on this server before.");
            return;
        }

        if(t.playerIsMayor(p)) {
            localSender.sendMessage(ERR + "That player is already the mayor of the town.");
            return;
        }

        if(!t.playerIsResident(p)) {
            localSender.sendMessage(ERR + p.getName() + " is not a resident of " + t.getName() + ".");
            return;
        }

        if(t.addAssistant(p)) {
            for(String territName : t.getTerritoriesCollection()) {
                townManager.getTerritory(territName).addPlayer(p);
            }

            localSender.sendMessage(args[0] + " has been promoted to an assistant of " + t.getName() + ".");

            if(p.isOnline()) {
                p.getPlayer().sendMessage("You are now an Assistant Mayor of " + t.getName());
            }
        } else {
            localSender.sendMessage(ERR + args[0] + " is already an assistant in this town.");
        }
    }

    @CommandMethod(path = "town remove assistant", requiredArgs = 1)
    public void demoteFromAssistant(CommandSender s, String[] args) {
        setNewCommand(s);
        Town t = localSender.getActiveTown();
        OfflinePlayer p = server.getOfflinePlayer(args[0]);

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(!(localSender.hasExternalPermissions(Perms.ADMIN.toString()) || t.playerIsMayor(localSender.getPlayer()))) {
            localSender.notifyInsufPermissions();
            return;
        }

        if(!Players.playedHasEverLoggedIn(p)) {
            localSender.sendMessage(ERR + args[0] + " has never played on this server before.");
            return;
        }

        if(!t.playerIsResident(p)) {
            localSender.sendMessage(ERR + args[0] + " is not a resident of " + t.getName() + ".");
            return;
        }

        if(t.removeAssistant(p)) {
            localSender.sendMessage(p.getName() + " has been demoted.");
            if(p.isOnline()) {
                p.getPlayer().sendMessage(ChatColor.DARK_RED + "You are no longer an assistant mayor for " + t.getName());
            }

            for(String territName : t.getTerritoriesCollection()) {
                townManager.getTerritory(territName).removePlayer(p);
            }
        } else {
            localSender.sendMessage(ERR + p.getName() + " is not an assistant in this town.");
        }
    }

    @CommandMethod(path = "town mayor set", requiredArgs = 1)
    public void setMayor(CommandSender s, String[] args) {
        setNewCommand(s);
        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        Player p = server.getPlayerExact(args[0]);

        if(!(localSender.hasExternalPermissions("ADMIN") || t.getMayor().equals(localSender.getPlayer().getUniqueId()))) {
            localSender.notifyInsufPermissions();
            return;
        }

        if(p == null) {
            localSender.sendMessage(ERR + args[0] + " either does not exist or is not online.");
            return;
        }

        if(!t.playerIsResident(p)) {
            localSender.sendMessage(ERR + "That player is not a member of the town.");
            return;
        }

        localSender.getActiveTown().setMayor(p);
        t.broadcastMessageToTown("The mayor of " + t.getName() + " is now " + p.getName() + "!");
    }

    @CommandMethod(path = "town remove invite", requiredArgs = 1, filters = {"mayoralPerms"})
    public void cancelInvitation(CommandSender s, String[] args) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(joinManager.clearInvitationForPlayerFromTown(args[0], t)) {
            localSender.sendMessage(ChatColor.GOLD + "The invitation for " + args[0] + " has been withdrawn.");
        } else {
            localSender.sendMessage(ERR + args[0] + " does not have any pending invitations from " + t.getName() + ".");
        }
    }

    @CommandMethod(path = "town remove request", requiredArgs = 1, filters = {"mayoralPerms"})
    public void rejectRequest(CommandSender s, String[] args) {
        setNewCommand(s);
        String playerName = args[0];

        Player p = server.getPlayer(playerName);
        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(!joinManager.clearRequest((p == null ? playerName : p.getName()), t)) {
            localSender.sendMessage(ERR + "No matching request found.");
        } else {
            localSender.sendMessage(ChatColor.GOLD + (p == null ? playerName : p.getName()) + "'s request has been rejected.");

            if(p != null) {
                p.sendMessage(ChatColor.DARK_RED + "Your request to join " + t.getName() + " has been rejected.");
            }
        }

    }

    @CommandMethod(path = "town list requests", filters = {"mayoralPerms"})
    public void listRequestsForTown(CommandSender s) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }
        Set<String> playerNames = joinManager.getPlayersRequestingMembershipToTown(t);

        localSender.sendMessage(ChatColor.DARK_BLUE + "There are pending requests from:");

        for(String str : getOutputFriendlyTownJoinListMessages(playerNames)) {
            localSender.sendMessage(ChatColor.YELLOW + str);
        }

    }

    @CommandMethod(path = "town list invites", filters = {"mayoralPerms"})
    public void listInvitesForTown(CommandSender s) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        Set<String> invitedPlayers = joinManager.getIssuedInvitesForTown(t);

        localSender.sendMessage(ChatColor.DARK_BLUE + "There are pending invites for:");

        for(String str : getOutputFriendlyTownJoinListMessages(invitedPlayers)) {
            localSender.sendMessage(ChatColor.YELLOW + str);
        }
    }

    @CommandMethod(path = "town remove player", requiredArgs = 1, filters = {"mayoralPerms"})
    public void removePlayerFromTown(CommandSender s, String[] args) {
        setNewCommand(s);

        OfflinePlayer removeMe = server.getOfflinePlayer(args[0]);
        Town removeFrom = localSender.getActiveTown();

        if(!Players.playedHasEverLoggedIn(removeMe)) {
            localSender.sendMessage(ERR + "No player named '" + args[0] + "' has ever played on this server.");
            return;
        }

        if(removeFrom == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(!removeFrom.playerIsResident(removeMe)) {
            localSender.sendMessage(ERR + removeMe.getName() + " is not a resident of " + removeFrom.getName() + ".");
            return;
        }

        if(removeFrom.playerIsMayor(removeMe)) {
            localSender.sendMessage(ERR + "A mayor cannot be removed from his own town.");
            return;
        }

        if(removeFrom.playerIsAssistant(removeMe) && !localSender.hasExternalPermissions(Perms.ADMIN.toString()) && !removeFrom.playerIsMayor(localSender.getPlayer())) {
            localSender.sendMessage(ERR + "Only the mayor or admins can remove assistants from the town.");
            return;
        }

        localSender.getActiveTown().removePlayer(removeMe);

        Town.recursivelyRemovePlayerFromTown(removeMe, removeFrom);

        localSender.sendMessage("\"" + removeMe.getName() + "\" was removed from the town.");
        Player onlinePlayer = removeMe.getPlayer();
        if(onlinePlayer != null) {
            onlinePlayer.sendMessage(ChatColor.DARK_RED + "You have been removed from " + removeFrom.getName() + " by " + localSender.getPlayer().getName());
        }
    }

    @CommandMethod(path = "town remove self")
    public void removeSelfFromTown(CommandSender s) {
        setNewCommand(s);
        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.sendMessage(ERR + "You're either not a member of a town, or your active town isn't set.");
            localSender.sendMessage("To set your active town to your own town, use /town active reset");
            return;
        }

        if(t.playerIsMayor(localSender.getPlayer())) {
            localSender.sendMessage(ERR + "You're the mayor. You need to specify a new mayor before leaving your current town.");
            return;
        }

        t.removePlayer(localSender.getPlayer());

        localSender.sendMessage(ChatColor.DARK_RED + "You have left " + localSender.getActiveTown().getName() + ".");
    }

    @CommandMethod(path = "town pvp friendlyfire", requiredArgs = 1, filters = {"mayoralPerms"})
    public void setTownFriendlyFire(CommandSender s, String[] args) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        boolean friendlyFire;

        friendlyFire = args[0].equalsIgnoreCase("on");

        t.setFriendlyFire(friendlyFire);

        localSender.sendMessage(ChatColor.GREEN + "Friendly fire in " + t.getName() + " is now " + (friendlyFire ? "on" : "off") + ".");

    }

    @CommandMethod(path = "town motd set", requiredArgs = 1, filters = {"mayoralPerms"})
    public void setMOTD(CommandSender s, String[] args) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        t.setTownMOTD(String.join(" ", args));
        localSender.sendMessage("Town MOTD has been set.");
    }

    @CommandMethod(path = "town motd")
    public void printMOTD(CommandSender s) {
        setNewCommand(s);
        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        localSender.sendMessage(t.getTownMOTD());
    }

    @CommandMethod(path = "town list players")
    public void listResidents(CommandSender s, String[] args) {
        setNewCommand(s);
        int page;
        if(args.length > 0) {
            try {
                page = Integer.parseInt(args[0]);
            } catch(NumberFormatException nfe) {
                localSender.sendMessage(String.format("Couldn't parse \"%s\" into an integer.", args[0]));
                return;
            }
        } else {
            page = 1;
        }

        page--; //shift to 0-indexing

        if(page < 0) {
            localSender.sendMessage(ERR + "Invalid page.");
            return;
        }

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }
        localSender.sendMessage(ChatColor.AQUA + "Players in " + t.getName() + "(page " + page + "):");

        Set<String> names = t.getResidentNames();
        String[] players = names.toArray(new String[names.size()]);

        for(int i = page * RESULTS_PER_PAGE; i < players.length && i < page * RESULTS_PER_PAGE + RESULTS_PER_PAGE; i++) {
            localSender.sendMessage(ChatColor.YELLOW + players[i]);
        }
    }

    @CommandMethod(path = "town add warp", requiredArgs = 1, filters = {"mayoralPerms"})
    public void addWarp(Player p, String warpName) {
        setNewCommand(p);

        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(!t.playerIsInsideTownBorders(p)) {
            p.sendMessage(ChatColor.RED + "You need to be inside your town to do this.");
            return;
        }

        if(t.putWarp(warpName, p.getLocation()) == null) {
            p.sendMessage(ChatColor.GREEN + "Warp added.");
        } else {
            p.sendMessage(ChatColor.GREEN + "Warp updated.");
        }
    }

    @CommandMethod(path = "town remove warp", requiredArgs = 1, filters = {"mayoralPerms"})
    public void deleteWarp(Player s, String warpName) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        if(t.removeWarp(warpName) == null) {
            localSender.sendMessage(ChatColor.YELLOW + "No warp named \"" + warpName + "\" to delete.");
        } else {
            localSender.sendMessage(ChatColor.GREEN + "Warp \"" + warpName + "\" deleted.");
        }
    }

    @CommandMethod(path = "town list warps")
    public void listWarps(Player p) {
        setNewCommand(p);
        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        localSender.sendMessage(ChatColor.GOLD + "Warps in " + t.getName() + ":");
        t.getWarps().stream().forEach(w -> localSender.sendMessage(ChatColor.GOLD + w));
    }

    @CommandMethod(path = "town warp", requiredArgs = 1)
    public void useWarp(Player p, String warpName) {
        setNewCommand(p);
        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        Location dest = t.getWarp(warpName);
        if(dest == null) {
            p.sendMessage(ChatColor.RED + "No warp named " + warpName);
            p.sendMessage(ChatColor.RED + "Similar warps:");
            t.getWarps().stream()
                    .filter(w -> w.startsWith(warpName.substring(0, 1)))
                    .forEach(w -> p.sendMessage(w));
            return;
        }

        if(t.playerIsResident(p)) {
            p.teleport(dest);
        } else {
            p.sendMessage(ChatColor.RED + "You need to be a member of this town to use its warps.");
        }
    }

    @CommandMethod(path = "town spawn")
    public void warpToOtherSpawn(CommandSender s, String[] args) {
        setNewCommand(s);
        Town t;
        if(args.length > 0) {
            t = townManager.getTown(args[0]);
        } else {
            t = localSender.getActiveTown();
        }

        if(t == null) {
            if(args.length == 0) {
                localSender.sendMessage(ERR + "You don't have an active town.");
            } else {
                localSender.sendMessage(ERR + "That town doesn't exist.");
            }

            return;
        }

        if(!t.playerIsResident(localSender.getPlayer()) && !localSender.hasExternalPermissions(Perms.WARP_FOREIGN.toString())) {
            localSender.notifyInsufPermissions();
            return;
        }

        localSender.getPlayer().teleport(t.getSpawn());
        localSender.sendMessage(INFO + "Teleported to " + t.getName() + "! Welcome!");

    }

    @CommandMethod(path = "town bank withdraw blocks", filters = {"mayoralPerms"})
    public void openBlockBank(CommandSender s) {
        setNewCommand(s);

        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        localSender.getPlayer().openInventory(t.getBank().getBankInventory());
    }

    @CommandMethod(path = "town bank deposit blocks")
    public void openBankDepositBox(CommandSender s) {
        setNewCommand(s);
        Town t = localSender.getActiveTown();
        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        localSender.getPlayer().openInventory(t.getBank().getNewDepositBox(localSender.getPlayer()));
    }

    @CommandMethod(path = "town bank withdraw currency", requiredArgs = 1, filters = {"mayoralPerms"})
    public void withdrawCurrencyBank(CommandSender s, String rawAmount) {
        setNewCommand(s);

        if(!MCTConfig.ECONOMY_ENABLED.getBoolean()) {
            localSender.sendMessage(ERR + "The economy isn't enabled for your server.");
            return;
        }

        if(!Pattern.compile(MCTConfig.CURRENCY_INPUT_PATTERN.getString()).matcher(rawAmount).matches())
        {
            localSender.sendMessage(ERR + "Invalid currency input: " + rawAmount);
            return;
        }

        BigDecimal amt;
        try {
            amt = new BigDecimal(rawAmount);
        } catch(NumberFormatException nfe) {
            localSender.sendMessage(ERR + "Error parsing quantity \"" + rawAmount + "\" : " + nfe.getMessage());
            return;
        }

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        //Do the withdrawl from the town bank
        amt = t.getBank().withdrawCurrency(amt);

        MCTowns.getEconomy().depositPlayer(localSender.getPlayer().getName(), amt.doubleValue());
        localSender.sendMessage(amt + " was withdrawn from " + t.getName() + "'s town bank and deposited into your account.");
    }

    @CommandMethod(path = "town bank deposit currency")
    public void depositCurrencyBank(CommandSender s, String rawAmount) {
        setNewCommand(s);
        if(!MCTConfig.ECONOMY_ENABLED.getBoolean()) {
            localSender.sendMessage(ERR + "The economy isn't enabled for your server.");
            return;
        }

        if(!Pattern.compile(MCTConfig.CURRENCY_INPUT_PATTERN.getString()).matcher(rawAmount).matches())
        {
            localSender.sendMessage(ERR + "Invalid currency input: " + rawAmount);
            return;
        }

        BigDecimal amt;
        try {
            amt = new BigDecimal(rawAmount);
        } catch(NumberFormatException nfe) {
            localSender.sendMessage(ERR + "Error parsing quantity \"" + rawAmount + "\" : " + nfe.getMessage());
            return;
        }

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        EconomyResponse result = MCTowns.getEconomy().withdrawPlayer(localSender.getPlayer().getName(), amt.doubleValue());

        if(result.transactionSuccess()) {
            t.getBank().depositCurrency(amt);
            localSender.sendMessage(rawAmount + " was withdrawn from your account and deposited into " + t.getName() + "'s town bank.");
        } else {
            localSender.sendMessage(ERR + "Transaction failed; maybe you do not have enough money to do this?");
            localSender.sendMessage(ChatColor.GOLD + "Actual amount deposited: " + result.amount);
        }

    }

    @CommandMethod(path = "town bank currency check")
    public void checkCurrencyBank(CommandSender s) {
        setNewCommand(s);
        if(!MCTConfig.ECONOMY_ENABLED.getBoolean()) {
            localSender.sendMessage(ERR + "The economy isn't enabled for your server.");
            return;
        }

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }

        localSender.sendMessage(ChatColor.BLUE + "Amount of currency in bank: " + t.getBank().getCurrencyBalance());
    }

    @CommandMethod(path = "town list territories")
    public void listTerritories(CommandSender s, String[] args) {
        setNewCommand(s);
        int i;
        if(args.length > 0) {
            try {
                i = Integer.parseInt(args[0]);
            } catch(NumberFormatException ex) {
                localSender.sendMessage(ERR + "Error parsing token \"" + args[0] + "\":" + ex.getMessage());
                return;
            }
        } else {
            i = 1;
        }
        listTerritories(i);
    }

    private void listTerritories(int page) {
        page--; //shift to 0-indexing

        if(page < 0) {
            localSender.sendMessage(ERR + "Invalid page.");
            return;
        }

        Town t = localSender.getActiveTown();

        if(t == null) {
            localSender.notifyActiveTownNotSet();
            return;
        }
        localSender.sendMessage(ChatColor.AQUA + "Existing territories (page " + page + "):");

        String[] territs = t.getTerritoriesCollection().toArray(new String[t.getTerritoriesCollection().size()]);

        for(int i = page * RESULTS_PER_PAGE; i < territs.length && i < page * RESULTS_PER_PAGE + RESULTS_PER_PAGE; i++) {
            localSender.sendMessage(ChatColor.YELLOW + territs[i]);
        }
    }
} public enum MCTConfig {
    WG_VER_REGEX("wgRequirement", ".*"),
    DEFAULT_TOWN("defaultTown", null),
    ECONOMY_ENABLED("economyEnabled", false),
    MAYORS_CAN_BUY_TERRITORIES("mayorsCanBuyTerritories", false),
    PRICE_PER_XZ_BLOCK("pricePerXZBlock", 0),
    MIN_NUM_PLAYERS_TO_BUY_TERRITORY("minNumPlayersToBuyTerritory", 3),
    ALLOW_TOWN_FRIENDLY_FIRE_MANAGEMENT("allowTownFriendlyFireManagement", true),
    QUICKSELECT_TOOL("quickSelectTool", "WOOD_HOE"),
    LOG_COMMANDS("logCommands", true),
    PLAYERS_CAN_JOIN_MULTIPLE_TOWNS("playersCanJoinMultipleTowns", false),
    TERRITORY_XZ_SIZE_LIMIT("territoryXZSizeLimit", 800),
    DEBUG_MODE_ENABLED("debugModeEnabled", false),
    CURRENCY_INPUT_PATTERN("currencyInputPattern", "^\\d{1,10}(\\.\\d{1,10})?$");

    private static final MCTownsPlugin p = MCTownsPlugin.getPlugin();

    private final Object dflt;
    private final String key;

    private MCTConfig(String key, Object dflt) {
        this.dflt = dflt;
        this.key = key;
    }

    public void set(Object value) {
        MCTownsPlugin.getPlugin().getConfig().set(getKey(), value);
    }

    public Object getObject() {
        return p == null ? dflt : p.getConfig().get(this.getKey(), dflt);
    }

    public int getInt() {
        return p == null ? (int) dflt : p.getConfig().getInt(this.getKey(), (int) dflt);
    }

    public String getString() {
        String defString = dflt == null ? null : dflt.toString();
        return p == null ? defString : p.getConfig().getString(this.getKey(), defString);
    }

    public boolean getBoolean() {
        return p == null ? (boolean) dflt : p.getConfig().getBoolean(this.getKey(), (boolean) dflt);
    }

    public String getKey() {
        return key;
    }
}
import java.io.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import cafe.josh.mctowns.bank.DepositInventoryEntry;
import cafe.josh.mctowns.listeners.DepositBoxCloseListener;
import cafe.josh.mctowns.listeners.MCTPlayerListener;
import cafe.josh.mctowns.listeners.QuickSelectToolListener;
import cafe.josh.mctowns.permission.Perms;
import cafe.josh.mctowns.townjoin.TownJoinManager;
import cafe.josh.mctowns.upgrade.ResourceUpgradePaths;
import cafe.josh.reflective.TreeCommandExecutor;
import cafe.josh.reflective.TreeTabCompleter;
import cafe.josh.mctowns.command.ActiveSet;
import cafe.josh.mctowns.command.MCTHandler;
import cafe.josh.mctowns.command.PlotHandler;
import cafe.josh.mctowns.command.TerritoryHandler;
import cafe.josh.mctowns.command.TownHandler;
import cafe.josh.mctowns.util.MCTConfig;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.World;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;
import org.mcstats.Metrics;
/*
 * Copyright (C) 2013 Joshua Michael Hertlein <jmhertlein@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package cafe.josh.mctowns;



/**
 * The main class of the MCTowns plugin.
 *
 * @author Joshua Michael Hertlein <jmhertlein@gmail.com>
 */
public class MCTownsPlugin extends JavaPlugin {

    private static MCTownsPlugin singleton;
    private File savesDir, configFile,
            metaFile;
    private static TownManager townManager;
    private TownJoinManager joinManager;
    private HashMap<String, ActiveSet> activeSets;
    private HashMap<Player, ActiveSet> potentialPlotBuyers;
    private Map<String, DepositInventoryEntry> openDepositInventories;